From df874a42ebcfefa2b84a875469a4806eb9c402bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Tue, 6 Aug 2024 15:24:52 +0200 Subject: [PATCH 1/9] Materials for Structural Pattern Matching: Initial Commit --- structural-pattern-matching/README.md | 18 +++ structural-pattern-matching/fetcher.py | 37 +++++ structural-pattern-matching/guessing_game.py | 65 +++++++++ structural-pattern-matching/interpreter.py | 41 ++++++ structural-pattern-matching/issue_comments.py | 77 +++++++++++ structural-pattern-matching/optimizer.py | 55 ++++++++ structural-pattern-matching/repl.py | 46 +++++++ structural-pattern-matching/repl_enhanced.py | 130 ++++++++++++++++++ structural-pattern-matching/requirements.txt | 4 + 9 files changed, 473 insertions(+) create mode 100644 structural-pattern-matching/README.md create mode 100644 structural-pattern-matching/fetcher.py create mode 100644 structural-pattern-matching/guessing_game.py create mode 100644 structural-pattern-matching/interpreter.py create mode 100644 structural-pattern-matching/issue_comments.py create mode 100644 structural-pattern-matching/optimizer.py create mode 100644 structural-pattern-matching/repl.py create mode 100644 structural-pattern-matching/repl_enhanced.py create mode 100644 structural-pattern-matching/requirements.txt diff --git a/structural-pattern-matching/README.md b/structural-pattern-matching/README.md new file mode 100644 index 0000000000..9595e7ad91 --- /dev/null +++ b/structural-pattern-matching/README.md @@ -0,0 +1,18 @@ +# Structural Pattern Matching in Python + +This folder contains the code samples for the Real Python tutorial [Structural Pattern Matching in Python](https://realpython.com/structural-pattern-matching/). + +## Installation + +Create and activate a virtual environment: + +```sh +$ python -m venv venv/ +$ source venv/bin/activate +``` + +Install the required third-party dependencies: + +```sh +(venv) $ python -m pip install -r requirements.txt +``` diff --git a/structural-pattern-matching/fetcher.py b/structural-pattern-matching/fetcher.py new file mode 100644 index 0000000000..98d21ce9db --- /dev/null +++ b/structural-pattern-matching/fetcher.py @@ -0,0 +1,37 @@ +from http.client import HTTPConnection, HTTPResponse, HTTPSConnection +from sys import stderr +from urllib.parse import ParseResult, urlparse + + +def fetch(url): + print(f"Fetching URL: {url}", file=stderr) + connection = make_connection(url) + try: + connection.request("GET", "/") + match connection.getresponse(): + case HTTPResponse(status=code) if code >= 400: + raise ValueError("Failed to fetch URL") + case HTTPResponse(status=code) as resp if ( + code >= 300 and (redirect := resp.getheader("Location")) + ): + return fetch(redirect) + case HTTPResponse(status=code) as resp if code >= 200: + return resp.read() + case _: + raise ValueError("Unexpected response") + finally: + connection.close() + + +def make_connection(url): + match urlparse(url): + case ParseResult("http", netloc=host): + return HTTPConnection(host) + case ParseResult("https", netloc=host): + return HTTPSConnection(host) + case _: + raise ValueError("Unsupported URL scheme") + + +if __name__ == "__main__": + fetch("http://realpython.com/") diff --git a/structural-pattern-matching/guessing_game.py b/structural-pattern-matching/guessing_game.py new file mode 100644 index 0000000000..83530278b3 --- /dev/null +++ b/structural-pattern-matching/guessing_game.py @@ -0,0 +1,65 @@ +import random + +MIN, MAX = 1, 100 +MAX_TRIES = 5 +PROMPT_1 = f"\N{mage} Guess a number between {MIN} and {MAX}: " +PROMPT_2 = "\N{mage} Try again: " +BYE = "Bye \N{waving hand sign}" + + +def main(): + print("Welcome to the game! Type 'q' or 'quit' to exit.") + while True: + play_game() + if not want_again(): + bye() + + +def play_game(): + drawn_number = random.randint(MIN, MAX) + num_tries = MAX_TRIES + prompt = PROMPT_1 + while num_tries > 0: + match input(prompt): + case command if command.lower() in ("q", "quit"): + bye() + case user_input: + try: + user_number = int(user_input) + except ValueError: + print("That's not a number!") + else: + match user_number: + case number if number < drawn_number: + num_tries -= 1 + prompt = PROMPT_2 + print(f"Too low! {num_tries} tries left.") + case number if number > drawn_number: + num_tries -= 1 + prompt = PROMPT_2 + print(f"Too high! {num_tries} tries left.") + case _: + print("You won \N{party popper}") + return + print("You lost \N{pensive face}") + + +def want_again(): + while True: + match input("Do you want to play again? [Y/N] ").lower(): + case "y": + return True + case "n": + return False + + +def bye(): + print(BYE) + exit() + + +if __name__ == "__main__": + try: + main() + except (KeyboardInterrupt, EOFError): + print(f"\n{BYE}") diff --git a/structural-pattern-matching/interpreter.py b/structural-pattern-matching/interpreter.py new file mode 100644 index 0000000000..609e61a2fe --- /dev/null +++ b/structural-pattern-matching/interpreter.py @@ -0,0 +1,41 @@ +import sys + + +def interpret(code, num_bytes=2**10): + stack, brackets = [], {} + for i, instruction in enumerate(code): + match instruction: + case "[": + stack.append(i) + case "]": + brackets[i], brackets[j] = (j := stack.pop()), i + memory = bytearray(num_bytes) + pointer = ip = 0 + while ip < len(code): + match code[ip]: + case ">": + pointer += 1 + case "<": + pointer -= 1 + case "+": + memory[pointer] += 1 + case "-": + memory[pointer] -= 1 + case ".": + print(chr(memory[pointer]), end="") + case ",": + memory[pointer] = ord(sys.stdin.buffer.read(1)) + case "[" if memory[pointer] == 0: + ip = brackets[ip] + case "]" if memory[pointer] != 0: + ip = brackets[ip] + ip += 1 + + +if __name__ == "__main__": + interpret( + """ + +++++++++++[>++++++>+++++++++>++++++++>++++>+++>+<<<<<<-]>+++ + +++.>++.+++++++..+++.>>.>-.<<-.<.+++.------.--------.>>>+.>-. + """ + ) diff --git a/structural-pattern-matching/issue_comments.py b/structural-pattern-matching/issue_comments.py new file mode 100644 index 0000000000..ebc6a622ad --- /dev/null +++ b/structural-pattern-matching/issue_comments.py @@ -0,0 +1,77 @@ +import json +import urllib.request +from dataclasses import dataclass + +from rich.console import Console +from rich.markdown import Markdown +from rich.panel import Panel + + +@dataclass +class Comment: + when: str + body: str + url: str + user: str + user_url: str + issue_title: str + + @property + def footer(self): + return ( + f"Comment by [{self.user}]({self.user_url})" + f" on [{self.when}]({self.url})" + ) + + def render(self): + return Panel( + Markdown(f"{self.body}\n\n---\n_{self.footer}_"), + title=self.issue_title, + padding=1, + ) + + +def fetch_github_events(org, repo): + url = f"https://api.github.com/repos/{org}/{repo}/events" + with urllib.request.urlopen(url) as response: + return json.loads(response.read()) + + +def filter_comments(events): + for event in events: + match event: + case { + "type": "IssueCommentEvent", + "created_at": when, + "actor": { + "display_login": user, + }, + "payload": { + "action": "created", + "issue": { + "state": "open", + "title": issue_title, + }, + "comment": { + "body": body, + "html_url": url, + "user": { + "html_url": user_url, + }, + }, + }, + }: + yield Comment(when, body, url, user, user_url, issue_title) + + +def main(): + console = Console() + events = fetch_github_events("python", "cpython") + for comment in filter_comments(events): + console.clear() + console.print(comment.render()) + console.input("\nPress [b]ENTER[/b] for the next comment...") + + +if __name__ == "__main__": + main() diff --git a/structural-pattern-matching/optimizer.py b/structural-pattern-matching/optimizer.py new file mode 100644 index 0000000000..8074e84206 --- /dev/null +++ b/structural-pattern-matching/optimizer.py @@ -0,0 +1,55 @@ +import ast +import inspect +import textwrap + + +def main(): + source_code = inspect.getsource(sample_function) + source_tree = ast.parse(source_code) + target_tree = optimize(source_tree) + target_code = ast.unparse(target_tree) + + print("Before:") + print(ast.dump(source_tree)) + print(textwrap.indent(source_code, "| ")) + + print("After:") + print(ast.dump(target_tree)) + print(textwrap.indent(target_code, "| ")) + + +def sample_function(): + return 40 + 2 + + +def optimize(node): + match node: + case ast.Module(body, type_ignores): + return ast.Module([optimize(child) for child in body], type_ignores) + case ast.FunctionDef(): + return ast.FunctionDef( + name=node.name, + args=node.args, + body=[optimize(child) for child in node.body], + decorator_list=node.decorator_list, + returns=node.returns, + type_comment=node.type_comment, + type_params=node.type_params, + lineno=node.lineno, + ) + case ast.Return(value): + return ast.Return(value=optimize(value)) + case ast.BinOp(ast.Constant(left), op, ast.Constant(right)): + match op: + case ast.Add(): + return ast.Constant(left + right) + case ast.Sub(): + return ast.Constant(left - right) + case _: + return node + case _: + return node + + +if __name__ == "__main__": + main() diff --git a/structural-pattern-matching/repl.py b/structural-pattern-matching/repl.py new file mode 100644 index 0000000000..d15c3750ae --- /dev/null +++ b/structural-pattern-matching/repl.py @@ -0,0 +1,46 @@ +import ast +import sys +import traceback + +PROMPT = "\N{snake} " +COMMANDS = ("help", "exit", "quit") + + +def main(): + print('Type "help" for more information, "exit" or "quit" to finish.') + while True: + try: + match input(PROMPT): + case command if command.lower() in COMMANDS: + match command.lower(): + case "help": + print(f"Python {sys.version}") + case "exit" | "quit": + break + case expression if valid(expression, "eval"): + _ = eval(expression) + if _ is not None: + print(_) + case statement if valid(statement, "exec"): + exec(statement) + case _: + print("Please type a command or valid Python") + except KeyboardInterrupt: + print("\nKeyboardInterrupt") + except EOFError: + print() + exit() + except: + traceback.print_exc(file=sys.stdout) + + +def valid(code, mode): + try: + ast.parse(code, mode=mode) + return True + except SyntaxError: + return False + + +if __name__ == "__main__": + main() diff --git a/structural-pattern-matching/repl_enhanced.py b/structural-pattern-matching/repl_enhanced.py new file mode 100644 index 0000000000..b4cf383b08 --- /dev/null +++ b/structural-pattern-matching/repl_enhanced.py @@ -0,0 +1,130 @@ +import ast +import atexit +import readline +import rlcompleter +import sys +import traceback +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal + +STANDARD_PROMPT = ">>> " +INDENTED_PROMPT = "... " +TAB_WIDTH = 4 +TAB = TAB_WIDTH * " " +COMMANDS = ("help", "exit", "quit") +PYTHON_HISTORY = Path.home() / ".python_history" + + +@dataclass +class Console: + indentation_level: int = 0 + + def __post_init__(self) -> None: + readline.parse_and_bind("tab: complete") + readline.set_completer(rlcompleter.Completer().complete) + if PYTHON_HISTORY.exists(): + readline.read_history_file(PYTHON_HISTORY) + atexit.register(readline.write_history_file, PYTHON_HISTORY) + + @property + def prompt(self) -> str: + if self.indentation_level > 0: + return INDENTED_PROMPT + else: + return STANDARD_PROMPT + + @property + def indentation(self) -> str: + return TAB * self.indentation_level + + def indent(self) -> None: + self.indentation_level += 1 + + def dedent(self) -> None: + if self.indentation_level > 0: + self.indentation_level -= 1 + + def reindent(self, line: str) -> None: + num_leading_spaces = len(line) - len(line.lstrip()) + new_indentation_level = num_leading_spaces // TAB_WIDTH + if new_indentation_level < self.indentation_level: + self.indentation_level = new_indentation_level + + def input(self) -> str: + def hook(): + readline.insert_text(self.indentation) + readline.redisplay() + + try: + readline.set_pre_input_hook(hook) + result = input(self.prompt) + return result + finally: + readline.set_pre_input_hook() + + +@dataclass +class CodeBlock: + lines: list[str] = field(default_factory=list) + + def execute(self) -> None: + exec("\n".join(self.lines), globals()) + self.lines = [] + + +def main() -> None: + print('Type "help" for more information, "exit" or "quit" to finish.') + console = Console() + block = CodeBlock() + while True: + try: + match console.input(): + case command if command.lower() in COMMANDS: + match command.lower(): + case "help": + print(f"Python {sys.version}") + case "exit" | "quit": + break + case line if line.endswith(":"): + block.lines.append(line) + console.reindent(line) + console.indent() + case line if line.lstrip() == "": + console.reindent(line) + console.dedent() + if console.indentation_level == 0 and block.lines: + block.execute() + case line if console.indentation_level > 0: + block.lines.append(line) + case expression if valid(expression, "eval"): + _ = eval(expression) + if _ is not None: + print(_) + case statement if valid(statement, "exec"): + exec(statement) + case _: + print("Please type a command or valid Python") + except KeyboardInterrupt: + print("\nKeyboardInterrupt") + console.indentation_level = 0 + block.lines = [] + except EOFError: + print() + sys.exit() + except: + traceback.print_exc(file=sys.stdout) + console.indentation_level = 0 + block.lines = [] + + +def valid(code: str, mode: Literal["eval", "exec"]) -> bool: + try: + ast.parse(code, mode=mode) + return True + except SyntaxError: + return False + + +if __name__ == "__main__": + main() diff --git a/structural-pattern-matching/requirements.txt b/structural-pattern-matching/requirements.txt new file mode 100644 index 0000000000..ffba812892 --- /dev/null +++ b/structural-pattern-matching/requirements.txt @@ -0,0 +1,4 @@ +markdown-it-py==3.0.0 +mdurl==0.1.2 +Pygments==2.18.0 +rich==13.7.1 From 63ce65cebbe7cbda71f34d07d939019ac9b81415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Tue, 6 Aug 2024 15:27:02 +0200 Subject: [PATCH 2/9] Add noqa to silence the linter --- structural-pattern-matching/repl.py | 2 +- structural-pattern-matching/repl_enhanced.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/structural-pattern-matching/repl.py b/structural-pattern-matching/repl.py index d15c3750ae..7c6bbae8fc 100644 --- a/structural-pattern-matching/repl.py +++ b/structural-pattern-matching/repl.py @@ -30,7 +30,7 @@ def main(): except EOFError: print() exit() - except: + except: # noqa traceback.print_exc(file=sys.stdout) diff --git a/structural-pattern-matching/repl_enhanced.py b/structural-pattern-matching/repl_enhanced.py index b4cf383b08..079a431d19 100644 --- a/structural-pattern-matching/repl_enhanced.py +++ b/structural-pattern-matching/repl_enhanced.py @@ -112,7 +112,7 @@ def main() -> None: except EOFError: print() sys.exit() - except: + except: # noqa traceback.print_exc(file=sys.stdout) console.indentation_level = 0 block.lines = [] From 7ffbbfc4c4b4e89b34bdd100dd47ea988de6d6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Tue, 6 Aug 2024 15:28:34 +0200 Subject: [PATCH 3/9] Reformat code --- structural-pattern-matching/optimizer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/structural-pattern-matching/optimizer.py b/structural-pattern-matching/optimizer.py index 8074e84206..431e3c0307 100644 --- a/structural-pattern-matching/optimizer.py +++ b/structural-pattern-matching/optimizer.py @@ -25,7 +25,9 @@ def sample_function(): def optimize(node): match node: case ast.Module(body, type_ignores): - return ast.Module([optimize(child) for child in body], type_ignores) + return ast.Module( + [optimize(child) for child in body], type_ignores + ) case ast.FunctionDef(): return ast.FunctionDef( name=node.name, From 76bad3832342fa3f360d74d44a97068192005975 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 30 Aug 2024 16:34:14 +0200 Subject: [PATCH 4/9] Sample code for the article on building a contact book with Textual --- .../source_code/README.md | 33 ++++ .../source_code/requirements.txt | 1 + .../source_code/rpcontacts/__init__.py | 1 + .../source_code/rpcontacts/__main__.py | 11 ++ .../source_code/rpcontacts/database.py | 52 ++++++ .../source_code/rpcontacts/rpcontacts.tcss | 75 +++++++++ .../source_code/rpcontacts/tui.py | 156 ++++++++++++++++++ .../source_code_step_1/README.md | 34 ++++ .../source_code_step_1/requirements.txt | 2 + .../source_code_step_1/rpcontacts/__init__.py | 1 + .../source_code_step_1/rpcontacts/__main__.py | 10 ++ .../source_code_step_1/rpcontacts/database.py | 0 .../rpcontacts/rpcontacts.tcss | 25 +++ .../source_code_step_1/rpcontacts/tui.py | 53 ++++++ .../source_code_step_2/README.md | 34 ++++ .../source_code_step_2/requirements.txt | 1 + .../source_code_step_2/rpcontacts/__init__.py | 1 + .../source_code_step_2/rpcontacts/__main__.py | 10 ++ .../source_code_step_2/rpcontacts/database.py | 0 .../rpcontacts/rpcontacts.tcss | 42 +++++ .../source_code_step_2/rpcontacts/tui.py | 71 ++++++++ .../source_code_step_3/README.md | 34 ++++ .../source_code_step_3/requirements.txt | 1 + .../source_code_step_3/rpcontacts/__init__.py | 1 + .../source_code_step_3/rpcontacts/__main__.py | 10 ++ .../source_code_step_3/rpcontacts/database.py | 61 +++++++ .../rpcontacts/rpcontacts.tcss | 42 +++++ .../source_code_step_3/rpcontacts/tui.py | 71 ++++++++ .../source_code_step_4/README.md | 34 ++++ .../source_code_step_4/requirements.txt | 1 + .../source_code_step_4/rpcontacts/__init__.py | 1 + .../source_code_step_4/rpcontacts/__main__.py | 11 ++ .../source_code_step_4/rpcontacts/database.py | 61 +++++++ .../rpcontacts/rpcontacts.tcss | 42 +++++ .../source_code_step_4/rpcontacts/tui.py | 82 +++++++++ .../source_code_step_5/README.md | 34 ++++ .../source_code_step_5/requirements.txt | 1 + .../source_code_step_5/rpcontacts/__init__.py | 1 + .../source_code_step_5/rpcontacts/__main__.py | 11 ++ .../source_code_step_5/rpcontacts/database.py | 61 +++++++ .../rpcontacts/rpcontacts.tcss | 75 +++++++++ .../source_code_step_5/rpcontacts/tui.py | 126 ++++++++++++++ .../source_code_step_6/README.md | 33 ++++ .../source_code_step_6/requirements.txt | 1 + .../source_code_step_6/rpcontacts/__init__.py | 1 + .../source_code_step_6/rpcontacts/__main__.py | 11 ++ .../source_code_step_6/rpcontacts/database.py | 52 ++++++ .../rpcontacts/rpcontacts.tcss | 75 +++++++++ .../source_code_step_6/rpcontacts/tui.py | 156 ++++++++++++++++++ 49 files changed, 1703 insertions(+) create mode 100755 contact-book-python-textual/source_code/README.md create mode 100755 contact-book-python-textual/source_code/requirements.txt create mode 100755 contact-book-python-textual/source_code/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_1/README.md create mode 100755 contact-book-python-textual/source_code_step_1/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_1/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_1/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_1/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_1/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_1/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_2/README.md create mode 100755 contact-book-python-textual/source_code_step_2/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_2/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_2/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_2/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_2/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_2/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_3/README.md create mode 100755 contact-book-python-textual/source_code_step_3/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_3/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_3/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_3/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_3/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_3/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_4/README.md create mode 100755 contact-book-python-textual/source_code_step_4/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_4/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_4/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_4/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_4/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_4/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_5/README.md create mode 100755 contact-book-python-textual/source_code_step_5/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_5/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_5/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_5/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_5/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_5/rpcontacts/tui.py create mode 100755 contact-book-python-textual/source_code_step_6/README.md create mode 100755 contact-book-python-textual/source_code_step_6/requirements.txt create mode 100755 contact-book-python-textual/source_code_step_6/rpcontacts/__init__.py create mode 100644 contact-book-python-textual/source_code_step_6/rpcontacts/__main__.py create mode 100644 contact-book-python-textual/source_code_step_6/rpcontacts/database.py create mode 100644 contact-book-python-textual/source_code_step_6/rpcontacts/rpcontacts.tcss create mode 100644 contact-book-python-textual/source_code_step_6/rpcontacts/tui.py diff --git a/contact-book-python-textual/source_code/README.md b/contact-book-python-textual/source_code/README.md new file mode 100755 index 0000000000..0058b1886a --- /dev/null +++ b/contact-book-python-textual/source_code/README.md @@ -0,0 +1,33 @@ +# RP Contacts + +**RP Contacts** is a contact book application built with Python, Textual, and SQLite. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the project's requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. diff --git a/contact-book-python-textual/source_code/requirements.txt b/contact-book-python-textual/source_code/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code/rpcontacts/__init__.py b/contact-book-python-textual/source_code/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code/rpcontacts/__main__.py b/contact-book-python-textual/source_code/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/__main__.py @@ -0,0 +1,11 @@ +from rpcontacts.database import Database +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp(db=Database()) + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code/rpcontacts/database.py b/contact-book-python-textual/source_code/rpcontacts/database.py new file mode 100644 index 0000000000..c88dacbf98 --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/database.py @@ -0,0 +1,52 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") diff --git a/contact-book-python-textual/source_code/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..02ea7df5c4 --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/rpcontacts.tcss @@ -0,0 +1,75 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} + +InputDialog { + align: center middle; +} + +#title { + column-span: 3; + height: 1fr; + width: 1fr; + content-align: center middle; + color: green; + text-style: bold; +} + +#input-dialog { + grid-size: 3 5; + grid-gutter: 1 1; + padding: 0 1; + width: 50; + height: 20; + border: solid green; + background: $surface; +} + +.label { + height: 1fr; + width: 1fr; + content-align: right middle; +} + +.input { + column-span: 2; +} diff --git a/contact-book-python-textual/source_code/rpcontacts/tui.py b/contact-book-python-textual/source_code/rpcontacts/tui.py new file mode 100644 index 0000000000..9c08587f5d --- /dev/null +++ b/contact-book-python-textual/source_code/rpcontacts/tui.py @@ -0,0 +1,156 @@ +from textual.app import App, on +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import ( + Button, + DataTable, + Footer, + Header, + Input, + Label, + Static, +) + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def __init__(self, db): + super().__init__() + self.db = db + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + self._load_contacts() + + def _load_contacts(self): + contacts_list = self.query_one(DataTable) + for contact_data in self.db.get_all_contacts(): + id, *contact = contact_data + contacts_list.add_row(*contact, key=id) + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + @on(Button.Pressed, "#add") + def action_add(self): + def check_contact(contact_data): + if contact_data: + self.db.add_contact(contact_data) + id, *contact = self.db.get_last_contact() + self.query_one(DataTable).add_row(*contact, key=id) + + self.push_screen(InputDialog(), check_contact) + + @on(Button.Pressed, "#delete") + def action_delete(self): + contacts_list = self.query_one(DataTable) + row_key, _ = contacts_list.coordinate_to_cell_key( + contacts_list.cursor_coordinate + ) + + def check_answer(accepted): + if accepted and row_key: + self.db.delete_contact(id=row_key.value) + contacts_list.remove_row(row_key) + + name = contacts_list.get_row(row_key)[0] + self.push_screen( + QuestionDialog(f"Do you want to delete {name}'s contact?"), + check_answer, + ) + + @on(Button.Pressed, "#clear") + def action_clear_all(self): + def check_answer(accepted): + if accepted: + self.db.clear_all_contacts() + self.query_one(DataTable).clear() + + self.push_screen( + QuestionDialog("Are you sure you want te remove all contacts?"), + check_answer, + ) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) + + +class InputDialog(Screen): + def compose(self): + yield Grid( + Label("Add Contact", id="title"), + Label("Name:", classes="label"), + Input(placeholder="Contact Name", classes="input", id="name"), + Label("Phone:", classes="label"), + Input(placeholder="Contact Phone", classes="input", id="phone"), + Label("Email:", classes="label"), + Input(placeholder="Contact Email", classes="input", id="email"), + Static(), + Button("Cancel", variant="warning", id="cancel"), + Button("Ok", variant="success", id="ok"), + id="input-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "ok": + name = self.query_one("#name", Input).value + phone = self.query_one("#phone", Input).value + email = self.query_one("#email", Input).value + self.dismiss((name, phone, email)) + else: + self.dismiss(()) diff --git a/contact-book-python-textual/source_code_step_1/README.md b/contact-book-python-textual/source_code_step_1/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_1/requirements.txt b/contact-book-python-textual/source_code_step_1/requirements.txt new file mode 100755 index 0000000000..99f67d03a3 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/requirements.txt @@ -0,0 +1,2 @@ +textual==0.75.1 +textual-dev==1.5.1 diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_1/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_1/rpcontacts/__main__.py new file mode 100644 index 0000000000..6fbc6f4277 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/rpcontacts/__main__.py @@ -0,0 +1,10 @@ +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/database.py b/contact-book-python-textual/source_code_step_1/rpcontacts/database.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_1/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..9349b5ff46 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/rpcontacts/rpcontacts.tcss @@ -0,0 +1,25 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} diff --git a/contact-book-python-textual/source_code_step_1/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_1/rpcontacts/tui.py new file mode 100644 index 0000000000..2a0e155918 --- /dev/null +++ b/contact-book-python-textual/source_code_step_1/rpcontacts/tui.py @@ -0,0 +1,53 @@ +from textual.app import App +from textual.containers import Grid +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Label + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("q", "request_quit", "Quit"), + ] + + def compose(self): + yield Header() + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) diff --git a/contact-book-python-textual/source_code_step_2/README.md b/contact-book-python-textual/source_code_step_2/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_2/requirements.txt b/contact-book-python-textual/source_code_step_2/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_2/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_2/rpcontacts/__main__.py new file mode 100644 index 0000000000..6fbc6f4277 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/__main__.py @@ -0,0 +1,10 @@ +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/database.py b/contact-book-python-textual/source_code_step_2/rpcontacts/database.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_2/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..29d34d8a70 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/rpcontacts.tcss @@ -0,0 +1,42 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py new file mode 100644 index 0000000000..14e5a01597 --- /dev/null +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py @@ -0,0 +1,71 @@ +from textual.app import App +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Button, DataTable, Footer, Header, Label, Static + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) diff --git a/contact-book-python-textual/source_code_step_3/README.md b/contact-book-python-textual/source_code_step_3/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_3/requirements.txt b/contact-book-python-textual/source_code_step_3/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_3/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_3/rpcontacts/__main__.py new file mode 100644 index 0000000000..6fbc6f4277 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/__main__.py @@ -0,0 +1,10 @@ +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/database.py b/contact-book-python-textual/source_code_step_3/rpcontacts/database.py new file mode 100644 index 0000000000..accbb77c47 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/database.py @@ -0,0 +1,61 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") + + +data = [ + ("Linda", "111-2222-3333", "linda@example.com"), + ("Joe", "111-2222-3333", "joe@example.com"), + ("Lara", "111-2222-3333", "lara@example.com"), + ("David", "111-2222-3333", "david@example.com"), + ("Jane", "111-2222-3333", "jane@example.com"), +] diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_3/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..29d34d8a70 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/rpcontacts.tcss @@ -0,0 +1,42 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py new file mode 100644 index 0000000000..14e5a01597 --- /dev/null +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py @@ -0,0 +1,71 @@ +from textual.app import App +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Button, DataTable, Footer, Header, Label, Static + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) diff --git a/contact-book-python-textual/source_code_step_4/README.md b/contact-book-python-textual/source_code_step_4/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_4/requirements.txt b/contact-book-python-textual/source_code_step_4/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_4/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_4/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/__main__.py @@ -0,0 +1,11 @@ +from rpcontacts.database import Database +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp(db=Database()) + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/database.py b/contact-book-python-textual/source_code_step_4/rpcontacts/database.py new file mode 100644 index 0000000000..accbb77c47 --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/database.py @@ -0,0 +1,61 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") + + +data = [ + ("Linda", "111-2222-3333", "linda@example.com"), + ("Joe", "111-2222-3333", "joe@example.com"), + ("Lara", "111-2222-3333", "lara@example.com"), + ("David", "111-2222-3333", "david@example.com"), + ("Jane", "111-2222-3333", "jane@example.com"), +] diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_4/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..29d34d8a70 --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/rpcontacts.tcss @@ -0,0 +1,42 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py new file mode 100644 index 0000000000..b0b441cb5a --- /dev/null +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py @@ -0,0 +1,82 @@ +from textual.app import App +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Button, DataTable, Footer, Header, Label, Static + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def __init__(self, db): + super().__init__() + self.db = db + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + self._load_contacts() + + def _load_contacts(self): + contacts_list = self.query_one(DataTable) + for contact_data in self.db.get_all_contacts(): + id, *contact = contact_data + contacts_list.add_row(*contact, key=id) + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) diff --git a/contact-book-python-textual/source_code_step_5/README.md b/contact-book-python-textual/source_code_step_5/README.md new file mode 100755 index 0000000000..e29a346fef --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/README.md @@ -0,0 +1,34 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m pip install -e . +(venv) $ rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. \ No newline at end of file diff --git a/contact-book-python-textual/source_code_step_5/requirements.txt b/contact-book-python-textual/source_code_step_5/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_5/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_5/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/__main__.py @@ -0,0 +1,11 @@ +from rpcontacts.database import Database +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp(db=Database()) + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/database.py b/contact-book-python-textual/source_code_step_5/rpcontacts/database.py new file mode 100644 index 0000000000..accbb77c47 --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/database.py @@ -0,0 +1,61 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") + + +data = [ + ("Linda", "111-2222-3333", "linda@example.com"), + ("Joe", "111-2222-3333", "joe@example.com"), + ("Lara", "111-2222-3333", "lara@example.com"), + ("David", "111-2222-3333", "david@example.com"), + ("Jane", "111-2222-3333", "jane@example.com"), +] diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_5/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..02ea7df5c4 --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/rpcontacts.tcss @@ -0,0 +1,75 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} + +InputDialog { + align: center middle; +} + +#title { + column-span: 3; + height: 1fr; + width: 1fr; + content-align: center middle; + color: green; + text-style: bold; +} + +#input-dialog { + grid-size: 3 5; + grid-gutter: 1 1; + padding: 0 1; + width: 50; + height: 20; + border: solid green; + background: $surface; +} + +.label { + height: 1fr; + width: 1fr; + content-align: right middle; +} + +.input { + column-span: 2; +} diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py new file mode 100644 index 0000000000..bd69c739cc --- /dev/null +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py @@ -0,0 +1,126 @@ +from textual.app import App, on +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import ( + Button, + DataTable, + Footer, + Header, + Input, + Label, + Static, +) + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def __init__(self, db): + super().__init__() + self.db = db + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + self._load_contacts() + + def _load_contacts(self): + contacts_list = self.query_one(DataTable) + for contact_data in self.db.get_all_contacts(): + id, *contact = contact_data + contacts_list.add_row(*contact, key=id) + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + @on(Button.Pressed, "#add") + def action_add(self): + def check_contact(contact_data): + if contact_data: + self.db.add_contact(contact_data) + id, *contact = self.db.get_last_contact() + self.query_one(DataTable).add_row(*contact, key=id) + + self.push_screen(InputDialog(), check_contact) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) + + +class InputDialog(Screen): + def compose(self): + yield Grid( + Label("Add Contact", id="title"), + Label("Name:", classes="label"), + Input(placeholder="Contact Name", classes="input", id="name"), + Label("Phone:", classes="label"), + Input(placeholder="Contact Phone", classes="input", id="phone"), + Label("Email:", classes="label"), + Input(placeholder="Contact Email", classes="input", id="email"), + Static(), + Button("Cancel", variant="warning", id="cancel"), + Button("Ok", variant="success", id="ok"), + id="input-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "ok": + name = self.query_one("#name", Input).value + phone = self.query_one("#phone", Input).value + email = self.query_one("#email", Input).value + self.dismiss((name, phone, email)) + else: + self.dismiss(()) diff --git a/contact-book-python-textual/source_code_step_6/README.md b/contact-book-python-textual/source_code_step_6/README.md new file mode 100755 index 0000000000..0058b1886a --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/README.md @@ -0,0 +1,33 @@ +# RP Contacts + +**RP Contacts** is a contact book application built with Python, Textual, and SQLite. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the project's requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. diff --git a/contact-book-python-textual/source_code_step_6/requirements.txt b/contact-book-python-textual/source_code_step_6/requirements.txt new file mode 100755 index 0000000000..1c521a5f56 --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/requirements.txt @@ -0,0 +1 @@ +textual==0.75.1 diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/__init__.py b/contact-book-python-textual/source_code_step_6/rpcontacts/__init__.py new file mode 100755 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/__main__.py b/contact-book-python-textual/source_code_step_6/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/__main__.py @@ -0,0 +1,11 @@ +from rpcontacts.database import Database +from rpcontacts.tui import ContactsApp + + +def main(): + app = ContactsApp(db=Database()) + app.run() + + +if __name__ == "__main__": + main() diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/database.py b/contact-book-python-textual/source_code_step_6/rpcontacts/database.py new file mode 100644 index 0000000000..c88dacbf98 --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/database.py @@ -0,0 +1,52 @@ +import pathlib +import sqlite3 + +DATABASE_PATH = pathlib.Path().home() / "contacts.db" + + +class Database: + def __init__(self, db_path=DATABASE_PATH): + self.db = sqlite3.connect(db_path) + self.cursor = self.db.cursor() + self._create_table() + + def _create_table(self): + query = """ + CREATE TABLE IF NOT EXISTS contacts( + id INTEGER PRIMARY KEY, + name TEXT, + phone TEXT, + email TEXT + ); + """ + self._run_query(query) + + def _run_query(self, query, *query_args): + result = self.cursor.execute(query, [*query_args]) + self.db.commit() + return result + + def get_all_contacts(self): + result = self._run_query("SELECT * FROM contacts;") + return result.fetchall() + + def get_last_contact(self): + result = self._run_query( + "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;" + ) + return result.fetchone() + + def add_contact(self, contact): + self._run_query( + "INSERT INTO contacts VALUES (NULL, ?, ?, ?);", + *contact, + ) + + def delete_contact(self, id): + self._run_query( + "DELETE FROM contacts WHERE id=(?);", + id, + ) + + def clear_all_contacts(self): + self._run_query("DELETE FROM contacts;") diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/rpcontacts.tcss b/contact-book-python-textual/source_code_step_6/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..02ea7df5c4 --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/rpcontacts.tcss @@ -0,0 +1,75 @@ +QuestionDialog { + align: center middle; +} + +#question-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: solid red; + background: $surface; +} + +#question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} + +Button { + width: 100%; +} + +.contacts-list { + width: 3fr; + padding: 0 1; + border: solid green; +} + +.buttons-panel { + align: center top; + padding: 0 1; + width: auto; + border: solid red; +} + +.separator { + height: 1fr; +} + +InputDialog { + align: center middle; +} + +#title { + column-span: 3; + height: 1fr; + width: 1fr; + content-align: center middle; + color: green; + text-style: bold; +} + +#input-dialog { + grid-size: 3 5; + grid-gutter: 1 1; + padding: 0 1; + width: 50; + height: 20; + border: solid green; + background: $surface; +} + +.label { + height: 1fr; + width: 1fr; + content-align: right middle; +} + +.input { + column-span: 2; +} diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py new file mode 100644 index 0000000000..9c08587f5d --- /dev/null +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py @@ -0,0 +1,156 @@ +from textual.app import App, on +from textual.containers import Grid, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import ( + Button, + DataTable, + Footer, + Header, + Input, + Label, + Static, +) + + +class ContactsApp(App): + CSS_PATH = "rpcontacts.tcss" + BINDINGS = [ + ("m", "toggle_dark", "Toggle dark mode"), + ("a", "add", "Add"), + ("d", "delete", "Delete"), + ("c", "clear", "Clear All"), + ("q", "request_quit", "Quit"), + ] + + def __init__(self, db): + super().__init__() + self.db = db + + def compose(self): + yield Header() + contacts_list = DataTable(classes="contacts-list") + contacts_list.focus() + contacts_list.add_columns("Name", "Phone", "Email") + contacts_list.cursor_type = "row" + contacts_list.zebra_stripes = True + add_button = Button("Add", variant="success", id="add") + add_button.focus() + buttons_panel = Vertical( + add_button, + Button("Delete", variant="warning", id="delete"), + Static(classes="separator"), + Button("Clear All", variant="error", id="clear"), + classes="buttons-panel", + ) + yield Horizontal(contacts_list, buttons_panel) + yield Footer() + + def on_mount(self): + self.title = "RP Contacts" + self.sub_title = "A Contacts Book App With Textual & Python" + self._load_contacts() + + def _load_contacts(self): + contacts_list = self.query_one(DataTable) + for contact_data in self.db.get_all_contacts(): + id, *contact = contact_data + contacts_list.add_row(*contact, key=id) + + def action_toggle_dark(self): + self.dark = not self.dark + + def action_request_quit(self): + def check_answer(accepted): + if accepted: + self.exit() + + self.push_screen(QuestionDialog("Do you want to quit?"), check_answer) + + @on(Button.Pressed, "#add") + def action_add(self): + def check_contact(contact_data): + if contact_data: + self.db.add_contact(contact_data) + id, *contact = self.db.get_last_contact() + self.query_one(DataTable).add_row(*contact, key=id) + + self.push_screen(InputDialog(), check_contact) + + @on(Button.Pressed, "#delete") + def action_delete(self): + contacts_list = self.query_one(DataTable) + row_key, _ = contacts_list.coordinate_to_cell_key( + contacts_list.cursor_coordinate + ) + + def check_answer(accepted): + if accepted and row_key: + self.db.delete_contact(id=row_key.value) + contacts_list.remove_row(row_key) + + name = contacts_list.get_row(row_key)[0] + self.push_screen( + QuestionDialog(f"Do you want to delete {name}'s contact?"), + check_answer, + ) + + @on(Button.Pressed, "#clear") + def action_clear_all(self): + def check_answer(accepted): + if accepted: + self.db.clear_all_contacts() + self.query_one(DataTable).clear() + + self.push_screen( + QuestionDialog("Are you sure you want te remove all contacts?"), + check_answer, + ) + + +class QuestionDialog(Screen): + def __init__(self, message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message + + def compose(self): + no_button = Button("No", variant="primary", id="no") + no_button.focus() + + yield Grid( + Label(self.message, id="question"), + Button("Yes", variant="error", id="yes"), + no_button, + id="question-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "yes": + self.dismiss(True) + else: + self.dismiss(False) + + +class InputDialog(Screen): + def compose(self): + yield Grid( + Label("Add Contact", id="title"), + Label("Name:", classes="label"), + Input(placeholder="Contact Name", classes="input", id="name"), + Label("Phone:", classes="label"), + Input(placeholder="Contact Phone", classes="input", id="phone"), + Label("Email:", classes="label"), + Input(placeholder="Contact Email", classes="input", id="email"), + Static(), + Button("Cancel", variant="warning", id="cancel"), + Button("Ok", variant="success", id="ok"), + id="input-dialog", + ) + + def on_button_pressed(self, event): + if event.button.id == "ok": + name = self.query_one("#name", Input).value + phone = self.query_one("#phone", Input).value + email = self.query_one("#email", Input).value + self.dismiss((name, phone, email)) + else: + self.dismiss(()) From 73606334d9071b52fe26576543649a2e5fec42fe Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 30 Aug 2024 16:36:39 +0200 Subject: [PATCH 5/9] Copy the `README.md` file to the root --- contact-book-python-textual/README.md | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 contact-book-python-textual/README.md diff --git a/contact-book-python-textual/README.md b/contact-book-python-textual/README.md new file mode 100755 index 0000000000..0058b1886a --- /dev/null +++ b/contact-book-python-textual/README.md @@ -0,0 +1,33 @@ +# RP Contacts + +**RP Contacts** is a contact book application built with Python, Textual, and SQLite. + +## Installation + +1. Create a Python virtual environment + +```sh +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the project's requirements + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +## Run the Project + +```sh +(venv) $ python -m rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. From 2aa19cc68bdd13a9f3242612f2235e840b62c496 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Fri, 30 Aug 2024 16:38:43 +0200 Subject: [PATCH 6/9] Fix the `README.md` --- contact-book-python-textual/README.md | 34 ++------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/contact-book-python-textual/README.md b/contact-book-python-textual/README.md index 0058b1886a..36ef850a2f 100755 --- a/contact-book-python-textual/README.md +++ b/contact-book-python-textual/README.md @@ -1,33 +1,3 @@ -# RP Contacts +# Build a Contact Book App With Python, Textual, and SQLite -**RP Contacts** is a contact book application built with Python, Textual, and SQLite. - -## Installation - -1. Create a Python virtual environment - -```sh -$ python -m venv ./venv -$ source venv/bin/activate -(venv) $ -``` - -2. Install the project's requirements - -```sh -(venv) $ python -m pip install -r requirements.txt -``` - -## Run the Project - -```sh -(venv) $ python -m rpcontacts -``` - -## About the Author - -Real Python - Email: office@realpython.com - -## License - -Distributed under the MIT license. See `LICENSE` for more information. +This folder provides the code examples for the Real Python tutorial [Build a Contact Book App With Python, Textual, and SQLite](https://realpython.com/contact-book-python-textual/). \ No newline at end of file From 7d23cd3d2db82aa5ad18a51ec3d136816f2508ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Thu, 5 Sep 2024 12:36:21 +0200 Subject: [PATCH 7/9] [PATMA] TR Feedback --- structural-pattern-matching/guessing_game.py | 3 ++- structural-pattern-matching/repl.py | 2 +- structural-pattern-matching/repl_enhanced.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/structural-pattern-matching/guessing_game.py b/structural-pattern-matching/guessing_game.py index 83530278b3..333524320c 100644 --- a/structural-pattern-matching/guessing_game.py +++ b/structural-pattern-matching/guessing_game.py @@ -62,4 +62,5 @@ def bye(): try: main() except (KeyboardInterrupt, EOFError): - print(f"\n{BYE}") + print() + bye() diff --git a/structural-pattern-matching/repl.py b/structural-pattern-matching/repl.py index 7c6bbae8fc..7e4bb0e823 100644 --- a/structural-pattern-matching/repl.py +++ b/structural-pattern-matching/repl.py @@ -30,7 +30,7 @@ def main(): except EOFError: print() exit() - except: # noqa + except Exception: traceback.print_exc(file=sys.stdout) diff --git a/structural-pattern-matching/repl_enhanced.py b/structural-pattern-matching/repl_enhanced.py index 079a431d19..b5d5dda232 100644 --- a/structural-pattern-matching/repl_enhanced.py +++ b/structural-pattern-matching/repl_enhanced.py @@ -112,7 +112,7 @@ def main() -> None: except EOFError: print() sys.exit() - except: # noqa + except Exception: traceback.print_exc(file=sys.stdout) console.indentation_level = 0 block.lines = [] From 4272b56fc0effcd7fd0bc493f8d2548d5926e862 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Mon, 9 Sep 2024 13:41:22 +0200 Subject: [PATCH 8/9] TR updates, first round --- .../source_code/rpcontacts/tui.py | 2 +- .../source_code_step_2/rpcontacts/tui.py | 2 +- .../source_code_step_3/rpcontacts/database.py | 9 --------- .../source_code_step_3/rpcontacts/tui.py | 2 +- .../source_code_step_4/rpcontacts/database.py | 9 --------- .../source_code_step_4/rpcontacts/tui.py | 2 +- .../source_code_step_5/rpcontacts/database.py | 9 --------- .../source_code_step_5/rpcontacts/tui.py | 2 +- .../source_code_step_6/rpcontacts/tui.py | 2 +- 9 files changed, 6 insertions(+), 33 deletions(-) diff --git a/contact-book-python-textual/source_code/rpcontacts/tui.py b/contact-book-python-textual/source_code/rpcontacts/tui.py index 9c08587f5d..fd46db97ad 100644 --- a/contact-book-python-textual/source_code/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code/rpcontacts/tui.py @@ -18,7 +18,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py index 14e5a01597..37b9757b9c 100644 --- a/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_2/rpcontacts/tui.py @@ -10,7 +10,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/database.py b/contact-book-python-textual/source_code_step_3/rpcontacts/database.py index accbb77c47..c88dacbf98 100644 --- a/contact-book-python-textual/source_code_step_3/rpcontacts/database.py +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/database.py @@ -50,12 +50,3 @@ def delete_contact(self, id): def clear_all_contacts(self): self._run_query("DELETE FROM contacts;") - - -data = [ - ("Linda", "111-2222-3333", "linda@example.com"), - ("Joe", "111-2222-3333", "joe@example.com"), - ("Lara", "111-2222-3333", "lara@example.com"), - ("David", "111-2222-3333", "david@example.com"), - ("Jane", "111-2222-3333", "jane@example.com"), -] diff --git a/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py index 14e5a01597..37b9757b9c 100644 --- a/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_3/rpcontacts/tui.py @@ -10,7 +10,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/database.py b/contact-book-python-textual/source_code_step_4/rpcontacts/database.py index accbb77c47..c88dacbf98 100644 --- a/contact-book-python-textual/source_code_step_4/rpcontacts/database.py +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/database.py @@ -50,12 +50,3 @@ def delete_contact(self, id): def clear_all_contacts(self): self._run_query("DELETE FROM contacts;") - - -data = [ - ("Linda", "111-2222-3333", "linda@example.com"), - ("Joe", "111-2222-3333", "joe@example.com"), - ("Lara", "111-2222-3333", "lara@example.com"), - ("David", "111-2222-3333", "david@example.com"), - ("Jane", "111-2222-3333", "jane@example.com"), -] diff --git a/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py index b0b441cb5a..3087ba2300 100644 --- a/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_4/rpcontacts/tui.py @@ -10,7 +10,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/database.py b/contact-book-python-textual/source_code_step_5/rpcontacts/database.py index accbb77c47..c88dacbf98 100644 --- a/contact-book-python-textual/source_code_step_5/rpcontacts/database.py +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/database.py @@ -50,12 +50,3 @@ def delete_contact(self, id): def clear_all_contacts(self): self._run_query("DELETE FROM contacts;") - - -data = [ - ("Linda", "111-2222-3333", "linda@example.com"), - ("Joe", "111-2222-3333", "joe@example.com"), - ("Lara", "111-2222-3333", "lara@example.com"), - ("David", "111-2222-3333", "david@example.com"), - ("Jane", "111-2222-3333", "jane@example.com"), -] diff --git a/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py index bd69c739cc..ab422bba01 100644 --- a/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_5/rpcontacts/tui.py @@ -18,7 +18,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py index 9c08587f5d..fd46db97ad 100644 --- a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py @@ -18,7 +18,7 @@ class ContactsApp(App): ("m", "toggle_dark", "Toggle dark mode"), ("a", "add", "Add"), ("d", "delete", "Delete"), - ("c", "clear", "Clear All"), + ("c", "clear_all", "Clear All"), ("q", "request_quit", "Quit"), ] From e52d11bbe4aaea9f9a8bb9afbd760eaabdb73dcc Mon Sep 17 00:00:00 2001 From: Geir Arne Hjelle Date: Fri, 20 Sep 2024 22:48:48 +0200 Subject: [PATCH 9/9] Final QA (#584) --- contact-book-python-textual/source_code/rpcontacts/tui.py | 2 +- .../source_code_step_6/rpcontacts/tui.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contact-book-python-textual/source_code/rpcontacts/tui.py b/contact-book-python-textual/source_code/rpcontacts/tui.py index fd46db97ad..92a5a5a62f 100644 --- a/contact-book-python-textual/source_code/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code/rpcontacts/tui.py @@ -102,7 +102,7 @@ def check_answer(accepted): self.query_one(DataTable).clear() self.push_screen( - QuestionDialog("Are you sure you want te remove all contacts?"), + QuestionDialog("Are you sure you want to remove all contacts?"), check_answer, ) diff --git a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py index fd46db97ad..92a5a5a62f 100644 --- a/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py +++ b/contact-book-python-textual/source_code_step_6/rpcontacts/tui.py @@ -102,7 +102,7 @@ def check_answer(accepted): self.query_one(DataTable).clear() self.push_screen( - QuestionDialog("Are you sure you want te remove all contacts?"), + QuestionDialog("Are you sure you want to remove all contacts?"), check_answer, )