diff --git a/directory-tree-generator-python/README.md b/directory-tree-generator-python/README.md new file mode 100644 index 0000000000000000000000000000000000000000..46a21ada0c25879ef208d6c6b75411516f7d7259 --- /dev/null +++ b/directory-tree-generator-python/README.md @@ -0,0 +1,65 @@ +# RP Tree + +RP Tree is a command-line tool to generate a directory tree diagram. + +## Run the App + +To run **RP Tree**, you need to download the source code. Then open a terminal or command-line window and run the following steps: + +1. Create and activate a Python virtual environment + +```sh +$ cd rptree_project/ +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Run the application + +```sh +(venv) $ python tree.py /path/to/directory/ +``` + +**Note:** The `-h` or `--help` option provides help on how to use RP Tree. + +To take a quick test on **RP Tree**, you can use the sample `home/` directory provided along with the application's code and run the following command: + +```sh +(venv) $ python tree.py ../hello/ +../hello/ +│ +├── hello/ +│ ├── __init__.py +│ └── hello.py +│ +├── tests/ +│ └── test_hello.py +│ +├── requirements.txt +├── setup.py +├── README.md +└── LICENSE +``` + +That's it! You've generated a nice directory tree diagram. + +## Current Features + +If you run RP Tree with a directory path as an argument, then you get the full directory tree printed on your screen. The default input directory is your current directory. + +RP Tree also provides the following options: + +- `-v`, `--version` shows the application version and exits +- `-h`, `--help` show a usage message +- `-d`, `--dir-only` generates a directory-only tree +- `-o`, `--output-file` generates a tree and save it to a file in markdown format + +## Release History + +- 0.1.0 + - A work in progress + +## About the Author + +Leodanis Pozo Ramos - Email: leodanis@realpython.com diff --git a/directory-tree-generator-python/hello/LICENSE b/directory-tree-generator-python/hello/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/hello/README.md b/directory-tree-generator-python/hello/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/hello/hello/__init__.py b/directory-tree-generator-python/hello/hello/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/hello/hello/hello.py b/directory-tree-generator-python/hello/hello/hello.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/hello/requirements.txt b/directory-tree-generator-python/hello/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/hello/setup.py b/directory-tree-generator-python/hello/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/hello/tests/test_hello.py b/directory-tree-generator-python/hello/tests/test_hello.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_final/README.md b/directory-tree-generator-python/source_code_final/README.md new file mode 100644 index 0000000000000000000000000000000000000000..cf58b4e017735e48b8e3691f7392b29686573ffb --- /dev/null +++ b/directory-tree-generator-python/source_code_final/README.md @@ -0,0 +1,65 @@ +# RP Tree + +RP Tree is a command-line tool to generate a directory tree diagram. + +## Run the App + +To run **RP Tree**, you need to download the source code. Then open a terminal or command-line window and run the following steps: + +1. Create and activate a Python virtual environment. + +```sh +$ cd rptree_project/ +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Run the application. + +```sh +(venv) $ python tree.py /path/to/directory/ +``` + +**Note:** The `-h` or `--help` option provides help on how to use RP Tree. + +To take a quick test on **RP Tree**, you can use the sample `home/` directory provided along with the application's code and run the following command: + +```sh +(venv) $ python tree.py ../hello/ +../hello/ +│ +├── hello/ +│ ├── __init__.py +│ └── hello.py +│ +├── tests/ +│ └── test_hello.py +│ +├── requirements.txt +├── setup.py +├── README.md +└── LICENSE +``` + +That's it! You've generated a nice directory tree diagram. + +## Current Features + +If you run RP Tree with a directory path as an argument, then you get the full directory tree printed on your screen. The default input directory is your current directory. + +RP Tree also provides the following options: + +- `-v`, `--version` shows the application version and exits +- `-h`, `--help` shows a usage message +- `-d`, `--dir-only` generates a directory-only tree +- `-o`, `--output-file` generates a tree and save it to a file in markdown format + +## Release History + +- 0.1.0 + - A work in progress + +## About the Author + +Leodanis Pozo Ramos - Email: leodanis@realpython.com diff --git a/directory-tree-generator-python/source_code_final/rptree/__init__.py b/directory-tree-generator-python/source_code_final/rptree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3122b64f071f6b113e71df19aafa54ad74cab9ca --- /dev/null +++ b/directory-tree-generator-python/source_code_final/rptree/__init__.py @@ -0,0 +1,3 @@ +"""Top-level package for RP Tree.""" + +__version__ = "0.1.0" diff --git a/directory-tree-generator-python/source_code_final/rptree/cli.py b/directory-tree-generator-python/source_code_final/rptree/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..d73381538bcb82c229f7fbacbaf0ff05326a51c0 --- /dev/null +++ b/directory-tree-generator-python/source_code_final/rptree/cli.py @@ -0,0 +1,52 @@ +"""This module provides the RP Tree CLI.""" + +import argparse +import pathlib +import sys + +from . import __version__ +from .rptree import DirectoryTree + + +def main(): + args = parse_cmd_line_arguments() + root_dir = pathlib.Path(args.root_dir) + if not root_dir.is_dir(): + print("The specified root directory doesn't exist") + sys.exit() + tree = DirectoryTree( + root_dir, dir_only=args.dir_only, output_file=args.output_file + ) + tree.generate() + + +def parse_cmd_line_arguments(): + parser = argparse.ArgumentParser( + prog="tree", + description="RP Tree, a directory tree generator", + epilog="Thanks for using RP Tree!", + ) + parser.version = f"RP Tree v{__version__}" + parser.add_argument("-v", "--version", action="version") + parser.add_argument( + "root_dir", + metavar="ROOT_DIR", + nargs="?", + default=".", + help="Generate a full directory tree starting at ROOT_DIR", + ) + parser.add_argument( + "-d", + "--dir-only", + action="store_true", + help="Generate a directory-only tree", + ) + parser.add_argument( + "-o", + "--output-file", + metavar="OUTPUT_FILE", + nargs="?", + default=sys.stdout, + help="Generate a full directory tree and save it to a file", + ) + return parser.parse_args() diff --git a/directory-tree-generator-python/source_code_final/rptree/rptree.py b/directory-tree-generator-python/source_code_final/rptree/rptree.py new file mode 100644 index 0000000000000000000000000000000000000000..832a850335849873f2e51bac42d322d52fcd8cd8 --- /dev/null +++ b/directory-tree-generator-python/source_code_final/rptree/rptree.py @@ -0,0 +1,83 @@ +"""This module provides RP Tree main module.""" + +import os +import pathlib +import sys + +PIPE = "│" +ELBOW = "└──" +TEE = "├──" +PIPE_PREFIX = "│ " +SPACE_PREFIX = " " + + +class DirectoryTree: + def __init__(self, root_dir, dir_only=False, output_file=sys.stdout): + self._output_file = output_file + self._generator = _TreeGenerator(root_dir, dir_only) + + def generate(self): + tree = self._generator.build_tree() + if self._output_file != sys.stdout: + # Wrap the tree in a markdown code block + tree.insert(0, "```") + tree.append("```") + self._output_file = open( + self._output_file, mode="w", encoding="UTF-8" + ) + with self._output_file as stream: + for entry in tree: + print(entry, file=stream) + + +class _TreeGenerator: + def __init__(self, root_dir, dir_only=False): + self._root_dir = pathlib.Path(root_dir) + self._dir_only = dir_only + self._tree = [] + + def build_tree(self): + self._tree_head() + self._tree_body(self._root_dir) + return self._tree + + def _tree_head(self): + self._tree.append(f"{self._root_dir}{os.sep}") + self._tree.append(PIPE) + + def _tree_body(self, directory, prefix=""): + entries = self._prepare_entries(directory) + entries_count = len(entries) + for index, entry in enumerate(entries): + connector = ELBOW if index == entries_count - 1 else TEE + if entry.is_dir(): + self._add_directory( + entry, index, entries_count, prefix, connector + ) + else: + self._add_file(entry, prefix, connector) + + def _prepare_entries(self, directory): + entries = directory.iterdir() + if self._dir_only: + entries = [entry for entry in entries if entry.is_dir()] + return entries + entries = sorted(entries, key=lambda entry: entry.is_file()) + return entries + + def _add_directory( + self, directory, index, entries_count, prefix, connector + ): + self._tree.append(f"{prefix}{connector} {directory.name}{os.sep}") + if index != entries_count - 1: + prefix += PIPE_PREFIX + else: + prefix += SPACE_PREFIX + self._tree_body( + directory=directory, + prefix=prefix, + ) + self._tree.append(prefix.rstrip()) + + def _add_file(self, file, prefix, connector): + self._tree.append(f"{prefix}{connector} {file.name}") diff --git a/directory-tree-generator-python/source_code_final/tree.py b/directory-tree-generator-python/source_code_final/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..bc5770a0e094e4cc5165dfc894b305a7bbf959d3 --- /dev/null +++ b/directory-tree-generator-python/source_code_final/tree.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +"""This module provides RP Tree entry point script.""" + +from rptree.cli import main + +if __name__ == "__main__": + main() diff --git a/directory-tree-generator-python/source_code_step_1/README.md b/directory-tree-generator-python/source_code_step_1/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_1/rptree/__init__.py b/directory-tree-generator-python/source_code_step_1/rptree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3122b64f071f6b113e71df19aafa54ad74cab9ca --- /dev/null +++ b/directory-tree-generator-python/source_code_step_1/rptree/__init__.py @@ -0,0 +1,3 @@ +"""Top-level package for RP Tree.""" + +__version__ = "0.1.0" diff --git a/directory-tree-generator-python/source_code_step_1/rptree/cli.py b/directory-tree-generator-python/source_code_step_1/rptree/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_1/rptree/rptree.py b/directory-tree-generator-python/source_code_step_1/rptree/rptree.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_1/tree.py b/directory-tree-generator-python/source_code_step_1/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_2/README.md b/directory-tree-generator-python/source_code_step_2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_2/rptree/__init__.py b/directory-tree-generator-python/source_code_step_2/rptree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3122b64f071f6b113e71df19aafa54ad74cab9ca --- /dev/null +++ b/directory-tree-generator-python/source_code_step_2/rptree/__init__.py @@ -0,0 +1,3 @@ +"""Top-level package for RP Tree.""" + +__version__ = "0.1.0" diff --git a/directory-tree-generator-python/source_code_step_2/rptree/cli.py b/directory-tree-generator-python/source_code_step_2/rptree/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_2/rptree/rptree.py b/directory-tree-generator-python/source_code_step_2/rptree/rptree.py new file mode 100644 index 0000000000000000000000000000000000000000..470933a95bc8cff16a77febea8846801e92c3eac --- /dev/null +++ b/directory-tree-generator-python/source_code_step_2/rptree/rptree.py @@ -0,0 +1,65 @@ +"""This module provides RP Tree main module.""" + +import os +import pathlib + +PIPE = "│" +ELBOW = "└──" +TEE = "├──" +PIPE_PREFIX = "│ " +SPACE_PREFIX = " " + + +class DirectoryTree: + def __init__(self, root_dir): + self._generator = _TreeGenerator(root_dir) + + def generate(self): + tree = self._generator.build_tree() + for entry in tree: + print(entry) + + +class _TreeGenerator: + def __init__(self, root_dir): + self._root_dir = pathlib.Path(root_dir) + self._tree = [] + + def build_tree(self): + self._tree_head() + self._tree_body(self._root_dir) + return self._tree + + def _tree_head(self): + self._tree.append(f"{self._root_dir}{os.sep}") + self._tree.append(PIPE) + + def _tree_body(self, directory, prefix=""): + entries = directory.iterdir() + entries = sorted(entries, key=lambda entry: entry.is_file()) + entries_count = len(entries) + for index, entry in enumerate(entries): + connector = ELBOW if index == entries_count - 1 else TEE + if entry.is_dir(): + self._add_directory( + entry, index, entries_count, prefix, connector + ) + else: + self._add_file(entry, prefix, connector) + + def _add_directory( + self, directory, index, entries_count, prefix, connector + ): + self._tree.append(f"{prefix}{connector} {directory.name}{os.sep}") + if index != entries_count - 1: + prefix += PIPE_PREFIX + else: + prefix += SPACE_PREFIX + self._tree_body( + directory=directory, + prefix=prefix, + ) + self._tree.append(prefix.rstrip()) + + def _add_file(self, file, prefix, connector): + self._tree.append(f"{prefix}{connector} {file.name}") diff --git a/directory-tree-generator-python/source_code_step_2/tree.py b/directory-tree-generator-python/source_code_step_2/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_3/README.md b/directory-tree-generator-python/source_code_step_3/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_3/rptree/__init__.py b/directory-tree-generator-python/source_code_step_3/rptree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3122b64f071f6b113e71df19aafa54ad74cab9ca --- /dev/null +++ b/directory-tree-generator-python/source_code_step_3/rptree/__init__.py @@ -0,0 +1,3 @@ +"""Top-level package for RP Tree.""" + +__version__ = "0.1.0" diff --git a/directory-tree-generator-python/source_code_step_3/rptree/cli.py b/directory-tree-generator-python/source_code_step_3/rptree/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..e1f0c9f3a4763808ce55e15e95b6c0a0012f293a --- /dev/null +++ b/directory-tree-generator-python/source_code_step_3/rptree/cli.py @@ -0,0 +1,36 @@ +"""This module provides the RP Tree CLI.""" + +import argparse +import pathlib +import sys + +from . import __version__ +from .rptree import DirectoryTree + + +def main(): + args = parse_cmd_line_arguments() + root_dir = pathlib.Path(args.root_dir) + if not root_dir.is_dir(): + print("The specified root directory doesn't exist") + sys.exit() + tree = DirectoryTree(root_dir) + tree.generate() + + +def parse_cmd_line_arguments(): + parser = argparse.ArgumentParser( + prog="tree", + description="RP Tree, a directory tree generator", + epilog="Thanks for using RP Tree!", + ) + parser.version = f"RP Tree v{__version__}" + parser.add_argument("-v", "--version", action="version") + parser.add_argument( + "root_dir", + metavar="ROOT_DIR", + nargs="?", + default=".", + help="Generate a full directory tree starting at ROOT_DIR", + ) + return parser.parse_args() diff --git a/directory-tree-generator-python/source_code_step_3/rptree/rptree.py b/directory-tree-generator-python/source_code_step_3/rptree/rptree.py new file mode 100644 index 0000000000000000000000000000000000000000..470933a95bc8cff16a77febea8846801e92c3eac --- /dev/null +++ b/directory-tree-generator-python/source_code_step_3/rptree/rptree.py @@ -0,0 +1,65 @@ +"""This module provides RP Tree main module.""" + +import os +import pathlib + +PIPE = "│" +ELBOW = "└──" +TEE = "├──" +PIPE_PREFIX = "│ " +SPACE_PREFIX = " " + + +class DirectoryTree: + def __init__(self, root_dir): + self._generator = _TreeGenerator(root_dir) + + def generate(self): + tree = self._generator.build_tree() + for entry in tree: + print(entry) + + +class _TreeGenerator: + def __init__(self, root_dir): + self._root_dir = pathlib.Path(root_dir) + self._tree = [] + + def build_tree(self): + self._tree_head() + self._tree_body(self._root_dir) + return self._tree + + def _tree_head(self): + self._tree.append(f"{self._root_dir}{os.sep}") + self._tree.append(PIPE) + + def _tree_body(self, directory, prefix=""): + entries = directory.iterdir() + entries = sorted(entries, key=lambda entry: entry.is_file()) + entries_count = len(entries) + for index, entry in enumerate(entries): + connector = ELBOW if index == entries_count - 1 else TEE + if entry.is_dir(): + self._add_directory( + entry, index, entries_count, prefix, connector + ) + else: + self._add_file(entry, prefix, connector) + + def _add_directory( + self, directory, index, entries_count, prefix, connector + ): + self._tree.append(f"{prefix}{connector} {directory.name}{os.sep}") + if index != entries_count - 1: + prefix += PIPE_PREFIX + else: + prefix += SPACE_PREFIX + self._tree_body( + directory=directory, + prefix=prefix, + ) + self._tree.append(prefix.rstrip()) + + def _add_file(self, file, prefix, connector): + self._tree.append(f"{prefix}{connector} {file.name}") diff --git a/directory-tree-generator-python/source_code_step_3/tree.py b/directory-tree-generator-python/source_code_step_3/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..bc5770a0e094e4cc5165dfc894b305a7bbf959d3 --- /dev/null +++ b/directory-tree-generator-python/source_code_step_3/tree.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +"""This module provides RP Tree entry point script.""" + +from rptree.cli import main + +if __name__ == "__main__": + main() diff --git a/directory-tree-generator-python/source_code_step_4/README.md b/directory-tree-generator-python/source_code_step_4/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_4/rptree/__init__.py b/directory-tree-generator-python/source_code_step_4/rptree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3122b64f071f6b113e71df19aafa54ad74cab9ca --- /dev/null +++ b/directory-tree-generator-python/source_code_step_4/rptree/__init__.py @@ -0,0 +1,3 @@ +"""Top-level package for RP Tree.""" + +__version__ = "0.1.0" diff --git a/directory-tree-generator-python/source_code_step_4/rptree/cli.py b/directory-tree-generator-python/source_code_step_4/rptree/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..d3912fe8a40b6a09ec65018221d1470c65fb38f6 --- /dev/null +++ b/directory-tree-generator-python/source_code_step_4/rptree/cli.py @@ -0,0 +1,42 @@ +"""This module provides the RP Tree CLI.""" + +import argparse +import pathlib +import sys + +from . import __version__ +from .rptree import DirectoryTree + + +def main(): + args = parse_cmd_line_arguments() + root_dir = pathlib.Path(args.root_dir) + if not root_dir.is_dir(): + print("The specified root directory doesn't exist") + sys.exit() + tree = DirectoryTree(root_dir, dir_only=args.dir_only) + tree.generate() + + +def parse_cmd_line_arguments(): + parser = argparse.ArgumentParser( + prog="tree", + description="RP Tree, a directory tree generator", + epilog="Thanks for using RP Tree!", + ) + parser.version = f"RP Tree v{__version__}" + parser.add_argument("-v", "--version", action="version") + parser.add_argument( + "root_dir", + metavar="ROOT_DIR", + nargs="?", + default=".", + help="Generate a full directory tree starting at ROOT_DIR", + ) + parser.add_argument( + "-d", + "--dir-only", + action="store_true", + help="Generate a directory-only tree", + ) + return parser.parse_args() diff --git a/directory-tree-generator-python/source_code_step_4/rptree/rptree.py b/directory-tree-generator-python/source_code_step_4/rptree/rptree.py new file mode 100644 index 0000000000000000000000000000000000000000..7ed4dca6e82a5cc769c4b4cf0c667daf80e25007 --- /dev/null +++ b/directory-tree-generator-python/source_code_step_4/rptree/rptree.py @@ -0,0 +1,73 @@ +"""This module provides RP Tree main module.""" + +import os +import pathlib + +PIPE = "│" +ELBOW = "└──" +TEE = "├──" +PIPE_PREFIX = "│ " +SPACE_PREFIX = " " + + +class DirectoryTree: + def __init__(self, root_dir, dir_only=False): + self._generator = _TreeGenerator(root_dir, dir_only) + + def generate(self): + tree = self._generator.build_tree() + for entry in tree: + print(entry) + + +class _TreeGenerator: + def __init__(self, root_dir, dir_only=False): + self._root_dir = pathlib.Path(root_dir) + self._dir_only = dir_only + self._tree = [] + + def build_tree(self): + self._tree_head() + self._tree_body(self._root_dir) + return self._tree + + def _tree_head(self): + self._tree.append(f"{self._root_dir}{os.sep}") + self._tree.append(PIPE) + + def _tree_body(self, directory, prefix=""): + entries = self._prepare_entries(directory) + entries_count = len(entries) + for index, entry in enumerate(entries): + connector = ELBOW if index == entries_count - 1 else TEE + if entry.is_dir(): + self._add_directory( + entry, index, entries_count, prefix, connector + ) + else: + self._add_file(entry, prefix, connector) + + def _prepare_entries(self, directory): + entries = directory.iterdir() + if self._dir_only: + entries = [entry for entry in entries if entry.is_dir()] + return entries + entries = sorted(entries, key=lambda entry: entry.is_file()) + return entries + + def _add_directory( + self, directory, index, entries_count, prefix, connector + ): + self._tree.append(f"{prefix}{connector} {directory.name}{os.sep}") + if index != entries_count - 1: + prefix += PIPE_PREFIX + else: + prefix += SPACE_PREFIX + self._tree_body( + directory=directory, + prefix=prefix, + ) + self._tree.append(prefix.rstrip()) + + def _add_file(self, file, prefix, connector): + self._tree.append(f"{prefix}{connector} {file.name}") diff --git a/directory-tree-generator-python/source_code_step_4/tree.py b/directory-tree-generator-python/source_code_step_4/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..bc5770a0e094e4cc5165dfc894b305a7bbf959d3 --- /dev/null +++ b/directory-tree-generator-python/source_code_step_4/tree.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +"""This module provides RP Tree entry point script.""" + +from rptree.cli import main + +if __name__ == "__main__": + main() diff --git a/directory-tree-generator-python/source_code_step_5/README.md b/directory-tree-generator-python/source_code_step_5/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/directory-tree-generator-python/source_code_step_5/output.md b/directory-tree-generator-python/source_code_step_5/output.md new file mode 100644 index 0000000000000000000000000000000000000000..722f12c04690c607143e00cdae74c7b679fbcd01 --- /dev/null +++ b/directory-tree-generator-python/source_code_step_5/output.md @@ -0,0 +1,78 @@ +``` +./ +│ +├── rptree/ +│ ├── __pycache__/ +│ │ ├── rptree.cpython-38.pyc +│ │ ├── __init__.cpython-38.pyc +│ │ └── cli.cpython-38.pyc +│ │ +│ ├── rptree.py +│ ├── __init__.py +│ └── cli.py +│ +├── .mypy_cache/ +│ ├── 3.8/ +│ │ ├── os/ +│ │ │ ├── path.meta.json +│ │ │ ├── __init__.meta.json +│ │ │ ├── __init__.data.json +│ │ │ └── path.data.json +│ │ │ +│ │ ├── importlib/ +│ │ │ ├── abc.data.json +│ │ │ ├── __init__.meta.json +│ │ │ ├── abc.meta.json +│ │ │ └── __init__.data.json +│ │ │ +│ │ ├── rptree/ +│ │ │ ├── cli.data.json +│ │ │ ├── rptree.meta.json +│ │ │ ├── __init__.meta.json +│ │ │ ├── rptree.data.json +│ │ │ ├── __init__.data.json +│ │ │ └── cli.meta.json +│ │ │ +│ │ ├── collections/ +│ │ │ ├── abc.data.json +│ │ │ ├── __init__.meta.json +│ │ │ ├── abc.meta.json +│ │ │ └── __init__.data.json +│ │ │ +│ │ ├── @plugins_snapshot.json +│ │ ├── argparse.data.json +│ │ ├── typing.data.json +│ │ ├── types.data.json +│ │ ├── abc.data.json +│ │ ├── ast.meta.json +│ │ ├── codecs.data.json +│ │ ├── codecs.meta.json +│ │ ├── builtins.data.json +│ │ ├── io.data.json +│ │ ├── ast.data.json +│ │ ├── argparse.meta.json +│ │ ├── builtins.meta.json +│ │ ├── mmap.meta.json +│ │ ├── posix.meta.json +│ │ ├── genericpath.data.json +│ │ ├── _importlib_modulespec.meta.json +│ │ ├── sys.data.json +│ │ ├── abc.meta.json +│ │ ├── mmap.data.json +│ │ ├── _ast.data.json +│ │ ├── typing.meta.json +│ │ ├── pathlib.meta.json +│ │ ├── _ast.meta.json +│ │ ├── io.meta.json +│ │ ├── types.meta.json +│ │ ├── posix.data.json +│ │ ├── sys.meta.json +│ │ ├── genericpath.meta.json +│ │ ├── _importlib_modulespec.data.json +│ │ └── pathlib.data.json +│ │ +│ └── .gitignore +│ +├── README.md +└── tree.py +``` diff --git a/directory-tree-generator-python/source_code_step_5/rptree/__init__.py b/directory-tree-generator-python/source_code_step_5/rptree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3122b64f071f6b113e71df19aafa54ad74cab9ca --- /dev/null +++ b/directory-tree-generator-python/source_code_step_5/rptree/__init__.py @@ -0,0 +1,3 @@ +"""Top-level package for RP Tree.""" + +__version__ = "0.1.0" diff --git a/directory-tree-generator-python/source_code_step_5/rptree/cli.py b/directory-tree-generator-python/source_code_step_5/rptree/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..d73381538bcb82c229f7fbacbaf0ff05326a51c0 --- /dev/null +++ b/directory-tree-generator-python/source_code_step_5/rptree/cli.py @@ -0,0 +1,52 @@ +"""This module provides the RP Tree CLI.""" + +import argparse +import pathlib +import sys + +from . import __version__ +from .rptree import DirectoryTree + + +def main(): + args = parse_cmd_line_arguments() + root_dir = pathlib.Path(args.root_dir) + if not root_dir.is_dir(): + print("The specified root directory doesn't exist") + sys.exit() + tree = DirectoryTree( + root_dir, dir_only=args.dir_only, output_file=args.output_file + ) + tree.generate() + + +def parse_cmd_line_arguments(): + parser = argparse.ArgumentParser( + prog="tree", + description="RP Tree, a directory tree generator", + epilog="Thanks for using RP Tree!", + ) + parser.version = f"RP Tree v{__version__}" + parser.add_argument("-v", "--version", action="version") + parser.add_argument( + "root_dir", + metavar="ROOT_DIR", + nargs="?", + default=".", + help="Generate a full directory tree starting at ROOT_DIR", + ) + parser.add_argument( + "-d", + "--dir-only", + action="store_true", + help="Generate a directory-only tree", + ) + parser.add_argument( + "-o", + "--output-file", + metavar="OUTPUT_FILE", + nargs="?", + default=sys.stdout, + help="Generate a full directory tree and save it to a file", + ) + return parser.parse_args() diff --git a/directory-tree-generator-python/source_code_step_5/rptree/rptree.py b/directory-tree-generator-python/source_code_step_5/rptree/rptree.py new file mode 100644 index 0000000000000000000000000000000000000000..832a850335849873f2e51bac42d322d52fcd8cd8 --- /dev/null +++ b/directory-tree-generator-python/source_code_step_5/rptree/rptree.py @@ -0,0 +1,83 @@ +"""This module provides RP Tree main module.""" + +import os +import pathlib +import sys + +PIPE = "│" +ELBOW = "└──" +TEE = "├──" +PIPE_PREFIX = "│ " +SPACE_PREFIX = " " + + +class DirectoryTree: + def __init__(self, root_dir, dir_only=False, output_file=sys.stdout): + self._output_file = output_file + self._generator = _TreeGenerator(root_dir, dir_only) + + def generate(self): + tree = self._generator.build_tree() + if self._output_file != sys.stdout: + # Wrap the tree in a markdown code block + tree.insert(0, "```") + tree.append("```") + self._output_file = open( + self._output_file, mode="w", encoding="UTF-8" + ) + with self._output_file as stream: + for entry in tree: + print(entry, file=stream) + + +class _TreeGenerator: + def __init__(self, root_dir, dir_only=False): + self._root_dir = pathlib.Path(root_dir) + self._dir_only = dir_only + self._tree = [] + + def build_tree(self): + self._tree_head() + self._tree_body(self._root_dir) + return self._tree + + def _tree_head(self): + self._tree.append(f"{self._root_dir}{os.sep}") + self._tree.append(PIPE) + + def _tree_body(self, directory, prefix=""): + entries = self._prepare_entries(directory) + entries_count = len(entries) + for index, entry in enumerate(entries): + connector = ELBOW if index == entries_count - 1 else TEE + if entry.is_dir(): + self._add_directory( + entry, index, entries_count, prefix, connector + ) + else: + self._add_file(entry, prefix, connector) + + def _prepare_entries(self, directory): + entries = directory.iterdir() + if self._dir_only: + entries = [entry for entry in entries if entry.is_dir()] + return entries + entries = sorted(entries, key=lambda entry: entry.is_file()) + return entries + + def _add_directory( + self, directory, index, entries_count, prefix, connector + ): + self._tree.append(f"{prefix}{connector} {directory.name}{os.sep}") + if index != entries_count - 1: + prefix += PIPE_PREFIX + else: + prefix += SPACE_PREFIX + self._tree_body( + directory=directory, + prefix=prefix, + ) + self._tree.append(prefix.rstrip()) + + def _add_file(self, file, prefix, connector): + self._tree.append(f"{prefix}{connector} {file.name}") diff --git a/directory-tree-generator-python/source_code_step_5/tree.py b/directory-tree-generator-python/source_code_step_5/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..bc5770a0e094e4cc5165dfc894b305a7bbf959d3 --- /dev/null +++ b/directory-tree-generator-python/source_code_step_5/tree.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +"""This module provides RP Tree entry point script.""" + +from rptree.cli import main + +if __name__ == "__main__": + main() diff --git a/pyqt-calculator-tutorial/pycalc/LICENSE.txt b/pyqt-calculator-tutorial/pycalc/LICENSE.txt deleted file mode 100644 index a06313d68a00d91c534ddf58709806af10d01d0e..0000000000000000000000000000000000000000 --- a/pyqt-calculator-tutorial/pycalc/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Prahlad Yeri - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/python-contact-book/README.md b/python-contact-book/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dcd9000726ce5ec1ee36dfacf7f53b92e91529ad --- /dev/null +++ b/python-contact-book/README.md @@ -0,0 +1,43 @@ +# RP Contacts + +**RP Contacts** is a Contact Book application built with Python, [PyQt5](https://www.riverbankcomputing.com/static/Docs/PyQt5/index.html), and [SQLite](https://www.sqlite.org/docs.html). + +## Running the Application + +To run **RP Contacts**, you need to download the source code. Then open a terminal or command-line window and run the following steps: + +1. Create and activate a Python virtual environment + +```sh +$ cd rpcontacts/ +$ python -m venv ./venv +$ source venv/bin/activate +(venv) $ +``` + +2. Install the dependencies + +```sh +(venv) $ python -m pip install -r requirements.txt +``` + +3. Run the application + +```sh +(venv) $ python rpcontacts.py +``` + +**Note:** This application was coded and tested using Python 3.8.5 and PyQt 5.15.2. + +## Release History + +- 0.1.0 + - A work in progress + +## About the Author + +Leodanis Pozo Ramos – [@lpozo78](https://twitter.com/lpozo78) – leodanis@realpython.com + +## License + +Distributed under the MIT license. diff --git a/python-contact-book/source_code_final/contacts.sqlite b/python-contact-book/source_code_final/contacts.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..af3a3886a0a728cc30fc5634ff92755897e749a7 Binary files /dev/null and b/python-contact-book/source_code_final/contacts.sqlite differ diff --git a/python-contact-book/source_code_final/requirements.txt b/python-contact-book/source_code_final/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ddfd54631ebdbcdc185ed330b813a711f895a788 --- /dev/null +++ b/python-contact-book/source_code_final/requirements.txt @@ -0,0 +1 @@ +PyQt5==5.15.2 diff --git a/python-contact-book/source_code_final/rpcontacts.py b/python-contact-book/source_code_final/rpcontacts.py new file mode 100644 index 0000000000000000000000000000000000000000..73634de5cf723711bbb8d50729279282590c3a99 --- /dev/null +++ b/python-contact-book/source_code_final/rpcontacts.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts entry point script.""" + +from rpcontacts.main import main + +if __name__ == "__main__": + main() diff --git a/python-contact-book/source_code_final/rpcontacts/__init__.py b/python-contact-book/source_code_final/rpcontacts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a532bfc8d76390edbe732df6c84d86bce153f6cf --- /dev/null +++ b/python-contact-book/source_code_final/rpcontacts/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +"""This module provides the rpcontacts package.""" + +__version__ = "0.1.0" diff --git a/python-contact-book/source_code_final/rpcontacts/database.py b/python-contact-book/source_code_final/rpcontacts/database.py new file mode 100644 index 0000000000000000000000000000000000000000..a705bcc2a8d3830ad37a1c5905fa5788d5279cc4 --- /dev/null +++ b/python-contact-book/source_code_final/rpcontacts/database.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +"""This module provides a database connection.""" + +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtSql import QSqlDatabase, QSqlQuery + + +def _createContactsTable(): + """Create the contacts table in the database.""" + createTableQuery = QSqlQuery() + return createTableQuery.exec( + """ + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + name VARCHAR(40) NOT NULL, + job VARCHAR(50), + email VARCHAR(40) NOT NULL + ) + """ + ) + + +def createConnection(databaseName): + """Create and open a database connection.""" + connection = QSqlDatabase.addDatabase("QSQLITE") + connection.setDatabaseName(databaseName) + + if not connection.open(): + QMessageBox.warning( + None, + "RP Contact", + f"Database Error: {connection.lastError().text()}", + ) + return False + + _createContactsTable() + return True diff --git a/python-contact-book/source_code_final/rpcontacts/main.py b/python-contact-book/source_code_final/rpcontacts/main.py new file mode 100644 index 0000000000000000000000000000000000000000..0ddac2d40861763e51528044dcd731db95dee7ae --- /dev/null +++ b/python-contact-book/source_code_final/rpcontacts/main.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts application.""" + +import sys + +from PyQt5.QtWidgets import QApplication + +from .database import createConnection +from .views import Window + + +def main(): + """RP Contacts main function.""" + # Create the application + app = QApplication(sys.argv) + # Connect to the database before creating any window + if not createConnection("contacts.sqlite"): + sys.exit(1) + # Create the main window if the connection succeeded + win = Window() + win.show() + # Run the event loop + sys.exit(app.exec_()) diff --git a/python-contact-book/source_code_final/rpcontacts/model.py b/python-contact-book/source_code_final/rpcontacts/model.py new file mode 100644 index 0000000000000000000000000000000000000000..df7df8e895e850c1d75b9ca00296d84e67e42181 --- /dev/null +++ b/python-contact-book/source_code_final/rpcontacts/model.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +"""This module provides a model to manage the contacts table.""" + +from PyQt5.QtCore import Qt +from PyQt5.QtSql import QSqlTableModel + + +class ContactsModel: + def __init__(self): + self.model = self._createModel() + + @staticmethod + def _createModel(): + """Create and set up the model.""" + tableModel = QSqlTableModel() + tableModel.setTable("contacts") + tableModel.setEditStrategy(QSqlTableModel.OnFieldChange) + tableModel.select() + headers = ("ID", "Name", "Job", "Email") + for columnIndex, header in enumerate(headers): + tableModel.setHeaderData(columnIndex, Qt.Horizontal, header) + return tableModel + + def addContact(self, data): + """Add a contact to the database.""" + rows = self.model.rowCount() + self.model.insertRows(rows, 1) + for column_index, field in enumerate(data): + self.model.setData(self.model.index(rows, column_index + 1), field) + self.model.submitAll() + self.model.select() + + def deleteContact(self, row): + """Remove a contact from the database.""" + self.model.removeRow(row) + self.model.submitAll() + self.model.select() + + def clearContacts(self): + """Remove all contacts in the database.""" + self.model.setEditStrategy(QSqlTableModel.OnManualSubmit) + self.model.removeRows(0, self.model.rowCount()) + self.model.submitAll() + self.model.setEditStrategy(QSqlTableModel.OnFieldChange) + self.model.select() diff --git a/python-contact-book/source_code_final/rpcontacts/views.py b/python-contact-book/source_code_final/rpcontacts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..592d1070aa018e7fee77f9f1e5e557e2562de757 --- /dev/null +++ b/python-contact-book/source_code_final/rpcontacts/views.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +"""This module provides views to manage the contacts table.""" + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QAbstractItemView, + QDialog, + QDialogButtonBox, + QFormLayout, + QHBoxLayout, + QLineEdit, + QMainWindow, + QMessageBox, + QPushButton, + QTableView, + QVBoxLayout, + QWidget, +) + +from .model import ContactsModel + + +class Window(QMainWindow): + """Main Window.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent) + self.setWindowTitle("RP Contacts") + self.resize(550, 250) + self.centralWidget = QWidget() + self.setCentralWidget(self.centralWidget) + self.layout = QHBoxLayout() + self.centralWidget.setLayout(self.layout) + + self.contactsModel = ContactsModel() + self.setupUI() + + def setupUI(self): + """Setup the main window's GUI.""" + # Create the table view widget + self.table = QTableView() + self.table.setModel(self.contactsModel.model) + self.table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table.resizeColumnsToContents() + # Create buttons + self.addButton = QPushButton("Add...") + self.addButton.clicked.connect(self.openAddDialog) + self.deleteButton = QPushButton("Delete") + self.deleteButton.clicked.connect(self.deleteContact) + self.clearAllButton = QPushButton("Clear All") + self.clearAllButton.clicked.connect(self.clearContacts) + # Lay out the GUI + layout = QVBoxLayout() + layout.addWidget(self.addButton) + layout.addWidget(self.deleteButton) + layout.addStretch() + layout.addWidget(self.clearAllButton) + self.layout.addWidget(self.table) + self.layout.addLayout(layout) + + def openAddDialog(self): + """Open the Add Contact dialog.""" + dialog = AddDialog(self) + if dialog.exec() == QDialog.Accepted: + self.contactsModel.addContact(dialog.data) + self.table.resizeColumnsToContents() + + def deleteContact(self): + """Delete the selected contact from the database.""" + row = self.table.currentIndex().row() + if row < 0: + return + + messageBox = QMessageBox.warning( + self, + "Warning!", + "Do you want to remove the selected contact?", + QMessageBox.Ok | QMessageBox.Cancel, + ) + + if messageBox == QMessageBox.Ok: + self.contactsModel.deleteContact(row) + + def clearContacts(self): + """Remove all contacts from the database.""" + messageBox = QMessageBox.warning( + self, + "Warning!", + "Do you want to remove all your contacts?", + QMessageBox.Ok | QMessageBox.Cancel, + ) + + if messageBox == QMessageBox.Ok: + self.contactsModel.clearContacts() + + +class AddDialog(QDialog): + """Add Contact dialog.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent=parent) + self.setWindowTitle("Add Contact") + self.layout = QVBoxLayout() + self.setLayout(self.layout) + self.data = None + + self.setupUI() + + def setupUI(self): + """Setup the Add Contact dialog's GUI.""" + # Create line edits for data fields + self.nameField = QLineEdit() + self.nameField.setObjectName("Name") + self.jobField = QLineEdit() + self.jobField.setObjectName("Job") + self.emailField = QLineEdit() + self.emailField.setObjectName("Email") + # Lay out the data fields + layout = QFormLayout() + layout.addRow("Name:", self.nameField) + layout.addRow("Job:", self.jobField) + layout.addRow("Email:", self.emailField) + self.layout.addLayout(layout) + # Add standard buttons to the dialog and connect them + self.buttonsBox = QDialogButtonBox(self) + self.buttonsBox.setOrientation(Qt.Horizontal) + self.buttonsBox.setStandardButtons( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel + ) + self.buttonsBox.accepted.connect(self.accept) + self.buttonsBox.rejected.connect(self.reject) + self.layout.addWidget(self.buttonsBox) + + def accept(self): + """Accept the data provided through the dialog.""" + self.data = [] + for field in (self.nameField, self.jobField, self.emailField): + if not field.text(): + QMessageBox.critical( + self, + "Error!", + f"You must provide a contact's {field.objectName()}", + ) + self.data = None # Reset .data + return + + self.data.append(field.text()) + + if not self.data: + return + + super().accept() diff --git a/python-contact-book/source_code_step_1/rpcontacts.py b/python-contact-book/source_code_step_1/rpcontacts.py new file mode 100644 index 0000000000000000000000000000000000000000..73634de5cf723711bbb8d50729279282590c3a99 --- /dev/null +++ b/python-contact-book/source_code_step_1/rpcontacts.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts entry point script.""" + +from rpcontacts.main import main + +if __name__ == "__main__": + main() diff --git a/python-contact-book/source_code_step_1/rpcontacts/__init__.py b/python-contact-book/source_code_step_1/rpcontacts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a532bfc8d76390edbe732df6c84d86bce153f6cf --- /dev/null +++ b/python-contact-book/source_code_step_1/rpcontacts/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +"""This module provides the rpcontacts package.""" + +__version__ = "0.1.0" diff --git a/python-contact-book/source_code_step_1/rpcontacts/main.py b/python-contact-book/source_code_step_1/rpcontacts/main.py new file mode 100644 index 0000000000000000000000000000000000000000..b0b7b5d9dec2d7dc070c6bfa3e28bbb91f2a94ff --- /dev/null +++ b/python-contact-book/source_code_step_1/rpcontacts/main.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts application.""" + +import sys + +from PyQt5.QtWidgets import QApplication + +from .views import Window + + +def main(): + """RP Contacts main function.""" + # Create the application + app = QApplication(sys.argv) + # Create the main window + win = Window() + win.show() + # Run the event loop + sys.exit(app.exec()) diff --git a/python-contact-book/source_code_step_1/rpcontacts/views.py b/python-contact-book/source_code_step_1/rpcontacts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..37d02e966b8b4195f4946f77a2492be7bb8f2ebb --- /dev/null +++ b/python-contact-book/source_code_step_1/rpcontacts/views.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +"""This module provides views to manage the contacts table.""" + +from PyQt5.QtWidgets import ( + QHBoxLayout, + QMainWindow, + QWidget, +) + + +class Window(QMainWindow): + """Main Window.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent) + self.setWindowTitle("RP Contacts") + self.resize(550, 250) + self.centralWidget = QWidget() + self.setCentralWidget(self.centralWidget) + self.layout = QHBoxLayout() + self.centralWidget.setLayout(self.layout) diff --git a/python-contact-book/source_code_step_2/rpcontacts.py b/python-contact-book/source_code_step_2/rpcontacts.py new file mode 100644 index 0000000000000000000000000000000000000000..73634de5cf723711bbb8d50729279282590c3a99 --- /dev/null +++ b/python-contact-book/source_code_step_2/rpcontacts.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts entry point script.""" + +from rpcontacts.main import main + +if __name__ == "__main__": + main() diff --git a/python-contact-book/source_code_step_2/rpcontacts/__init__.py b/python-contact-book/source_code_step_2/rpcontacts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a532bfc8d76390edbe732df6c84d86bce153f6cf --- /dev/null +++ b/python-contact-book/source_code_step_2/rpcontacts/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +"""This module provides the rpcontacts package.""" + +__version__ = "0.1.0" diff --git a/python-contact-book/source_code_step_2/rpcontacts/main.py b/python-contact-book/source_code_step_2/rpcontacts/main.py new file mode 100644 index 0000000000000000000000000000000000000000..b0b7b5d9dec2d7dc070c6bfa3e28bbb91f2a94ff --- /dev/null +++ b/python-contact-book/source_code_step_2/rpcontacts/main.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts application.""" + +import sys + +from PyQt5.QtWidgets import QApplication + +from .views import Window + + +def main(): + """RP Contacts main function.""" + # Create the application + app = QApplication(sys.argv) + # Create the main window + win = Window() + win.show() + # Run the event loop + sys.exit(app.exec()) diff --git a/python-contact-book/source_code_step_2/rpcontacts/views.py b/python-contact-book/source_code_step_2/rpcontacts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..31b9d0ffb0b755750e8b41bc2eea6d76d9f3bac8 --- /dev/null +++ b/python-contact-book/source_code_step_2/rpcontacts/views.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +"""This module provides views to manage the contacts table.""" + +from PyQt5.QtWidgets import ( + QAbstractItemView, + QHBoxLayout, + QMainWindow, + QPushButton, + QTableView, + QVBoxLayout, + QWidget, +) + + +class Window(QMainWindow): + """Main Window.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent) + self.setWindowTitle("RP Contacts") + self.resize(550, 250) + self.centralWidget = QWidget() + self.setCentralWidget(self.centralWidget) + self.layout = QHBoxLayout() + self.centralWidget.setLayout(self.layout) + + self.setupUI() + + def setupUI(self): + """Setup the main window's GUI.""" + # Create the table view widget + self.table = QTableView() + self.table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table.resizeColumnsToContents() + # Create buttons + self.addButton = QPushButton("Add...") + self.deleteButton = QPushButton("Delete") + self.clearAllButton = QPushButton("Clear All") + # Lay out the GUI + layout = QVBoxLayout() + layout.addWidget(self.addButton) + layout.addWidget(self.deleteButton) + layout.addStretch() + layout.addWidget(self.clearAllButton) + self.layout.addWidget(self.table) + self.layout.addLayout(layout) diff --git a/python-contact-book/source_code_step_3/contacts.sqlite b/python-contact-book/source_code_step_3/contacts.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..c1b526e424f33ac93a1467f07e82ac01eb913cef Binary files /dev/null and b/python-contact-book/source_code_step_3/contacts.sqlite differ diff --git a/python-contact-book/source_code_step_3/rpcontacts.py b/python-contact-book/source_code_step_3/rpcontacts.py new file mode 100644 index 0000000000000000000000000000000000000000..73634de5cf723711bbb8d50729279282590c3a99 --- /dev/null +++ b/python-contact-book/source_code_step_3/rpcontacts.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts entry point script.""" + +from rpcontacts.main import main + +if __name__ == "__main__": + main() diff --git a/python-contact-book/source_code_step_3/rpcontacts/__init__.py b/python-contact-book/source_code_step_3/rpcontacts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a532bfc8d76390edbe732df6c84d86bce153f6cf --- /dev/null +++ b/python-contact-book/source_code_step_3/rpcontacts/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +"""This module provides the rpcontacts package.""" + +__version__ = "0.1.0" diff --git a/python-contact-book/source_code_step_3/rpcontacts/database.py b/python-contact-book/source_code_step_3/rpcontacts/database.py new file mode 100644 index 0000000000000000000000000000000000000000..a705bcc2a8d3830ad37a1c5905fa5788d5279cc4 --- /dev/null +++ b/python-contact-book/source_code_step_3/rpcontacts/database.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +"""This module provides a database connection.""" + +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtSql import QSqlDatabase, QSqlQuery + + +def _createContactsTable(): + """Create the contacts table in the database.""" + createTableQuery = QSqlQuery() + return createTableQuery.exec( + """ + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + name VARCHAR(40) NOT NULL, + job VARCHAR(50), + email VARCHAR(40) NOT NULL + ) + """ + ) + + +def createConnection(databaseName): + """Create and open a database connection.""" + connection = QSqlDatabase.addDatabase("QSQLITE") + connection.setDatabaseName(databaseName) + + if not connection.open(): + QMessageBox.warning( + None, + "RP Contact", + f"Database Error: {connection.lastError().text()}", + ) + return False + + _createContactsTable() + return True diff --git a/python-contact-book/source_code_step_3/rpcontacts/main.py b/python-contact-book/source_code_step_3/rpcontacts/main.py new file mode 100644 index 0000000000000000000000000000000000000000..0ddac2d40861763e51528044dcd731db95dee7ae --- /dev/null +++ b/python-contact-book/source_code_step_3/rpcontacts/main.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts application.""" + +import sys + +from PyQt5.QtWidgets import QApplication + +from .database import createConnection +from .views import Window + + +def main(): + """RP Contacts main function.""" + # Create the application + app = QApplication(sys.argv) + # Connect to the database before creating any window + if not createConnection("contacts.sqlite"): + sys.exit(1) + # Create the main window if the connection succeeded + win = Window() + win.show() + # Run the event loop + sys.exit(app.exec_()) diff --git a/python-contact-book/source_code_step_3/rpcontacts/views.py b/python-contact-book/source_code_step_3/rpcontacts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..31b9d0ffb0b755750e8b41bc2eea6d76d9f3bac8 --- /dev/null +++ b/python-contact-book/source_code_step_3/rpcontacts/views.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +"""This module provides views to manage the contacts table.""" + +from PyQt5.QtWidgets import ( + QAbstractItemView, + QHBoxLayout, + QMainWindow, + QPushButton, + QTableView, + QVBoxLayout, + QWidget, +) + + +class Window(QMainWindow): + """Main Window.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent) + self.setWindowTitle("RP Contacts") + self.resize(550, 250) + self.centralWidget = QWidget() + self.setCentralWidget(self.centralWidget) + self.layout = QHBoxLayout() + self.centralWidget.setLayout(self.layout) + + self.setupUI() + + def setupUI(self): + """Setup the main window's GUI.""" + # Create the table view widget + self.table = QTableView() + self.table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table.resizeColumnsToContents() + # Create buttons + self.addButton = QPushButton("Add...") + self.deleteButton = QPushButton("Delete") + self.clearAllButton = QPushButton("Clear All") + # Lay out the GUI + layout = QVBoxLayout() + layout.addWidget(self.addButton) + layout.addWidget(self.deleteButton) + layout.addStretch() + layout.addWidget(self.clearAllButton) + self.layout.addWidget(self.table) + self.layout.addLayout(layout) diff --git a/python-contact-book/source_code_step_4/contacts.sqlite b/python-contact-book/source_code_step_4/contacts.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..af3a3886a0a728cc30fc5634ff92755897e749a7 Binary files /dev/null and b/python-contact-book/source_code_step_4/contacts.sqlite differ diff --git a/python-contact-book/source_code_step_4/rpcontacts.py b/python-contact-book/source_code_step_4/rpcontacts.py new file mode 100644 index 0000000000000000000000000000000000000000..73634de5cf723711bbb8d50729279282590c3a99 --- /dev/null +++ b/python-contact-book/source_code_step_4/rpcontacts.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts entry point script.""" + +from rpcontacts.main import main + +if __name__ == "__main__": + main() diff --git a/python-contact-book/source_code_step_4/rpcontacts/__init__.py b/python-contact-book/source_code_step_4/rpcontacts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a532bfc8d76390edbe732df6c84d86bce153f6cf --- /dev/null +++ b/python-contact-book/source_code_step_4/rpcontacts/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +"""This module provides the rpcontacts package.""" + +__version__ = "0.1.0" diff --git a/python-contact-book/source_code_step_4/rpcontacts/database.py b/python-contact-book/source_code_step_4/rpcontacts/database.py new file mode 100644 index 0000000000000000000000000000000000000000..a705bcc2a8d3830ad37a1c5905fa5788d5279cc4 --- /dev/null +++ b/python-contact-book/source_code_step_4/rpcontacts/database.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +"""This module provides a database connection.""" + +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtSql import QSqlDatabase, QSqlQuery + + +def _createContactsTable(): + """Create the contacts table in the database.""" + createTableQuery = QSqlQuery() + return createTableQuery.exec( + """ + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + name VARCHAR(40) NOT NULL, + job VARCHAR(50), + email VARCHAR(40) NOT NULL + ) + """ + ) + + +def createConnection(databaseName): + """Create and open a database connection.""" + connection = QSqlDatabase.addDatabase("QSQLITE") + connection.setDatabaseName(databaseName) + + if not connection.open(): + QMessageBox.warning( + None, + "RP Contact", + f"Database Error: {connection.lastError().text()}", + ) + return False + + _createContactsTable() + return True diff --git a/python-contact-book/source_code_step_4/rpcontacts/main.py b/python-contact-book/source_code_step_4/rpcontacts/main.py new file mode 100644 index 0000000000000000000000000000000000000000..0ddac2d40861763e51528044dcd731db95dee7ae --- /dev/null +++ b/python-contact-book/source_code_step_4/rpcontacts/main.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts application.""" + +import sys + +from PyQt5.QtWidgets import QApplication + +from .database import createConnection +from .views import Window + + +def main(): + """RP Contacts main function.""" + # Create the application + app = QApplication(sys.argv) + # Connect to the database before creating any window + if not createConnection("contacts.sqlite"): + sys.exit(1) + # Create the main window if the connection succeeded + win = Window() + win.show() + # Run the event loop + sys.exit(app.exec_()) diff --git a/python-contact-book/source_code_step_4/rpcontacts/model.py b/python-contact-book/source_code_step_4/rpcontacts/model.py new file mode 100644 index 0000000000000000000000000000000000000000..50316ecb866df959c41b0ee17f353797cd8b0018 --- /dev/null +++ b/python-contact-book/source_code_step_4/rpcontacts/model.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +"""This module provides a model to manage the contacts table.""" + +from PyQt5.QtCore import Qt +from PyQt5.QtSql import QSqlTableModel + + +class ContactsModel: + def __init__(self): + self.model = self._createModel() + + @staticmethod + def _createModel(): + """Create and set up the model.""" + tableModel = QSqlTableModel() + tableModel.setTable("contacts") + tableModel.setEditStrategy(QSqlTableModel.OnFieldChange) + tableModel.select() + headers = ("ID", "Name", "Job", "Email") + for columnIndex, header in enumerate(headers): + tableModel.setHeaderData(columnIndex, Qt.Horizontal, header) + return tableModel diff --git a/python-contact-book/source_code_step_4/rpcontacts/views.py b/python-contact-book/source_code_step_4/rpcontacts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..30ffce49e469a38cc6a32d8898844ac51bfbbb82 --- /dev/null +++ b/python-contact-book/source_code_step_4/rpcontacts/views.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +"""This module provides views to manage the contacts table.""" + +from PyQt5.QtWidgets import ( + QAbstractItemView, + QHBoxLayout, + QMainWindow, + QPushButton, + QTableView, + QVBoxLayout, + QWidget, +) + +from .model import ContactsModel + + +class Window(QMainWindow): + """Main Window.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent) + self.setWindowTitle("RP Contacts") + self.resize(550, 250) + self.centralWidget = QWidget() + self.setCentralWidget(self.centralWidget) + self.layout = QHBoxLayout() + self.centralWidget.setLayout(self.layout) + + self.contactsModel = ContactsModel() + self.setupUI() + + def setupUI(self): + """Setup the main window's GUI.""" + # Create the table view widget + self.table = QTableView() + self.table.setModel(self.contactsModel.model) + self.table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table.resizeColumnsToContents() + # Create buttons + self.addButton = QPushButton("Add...") + self.deleteButton = QPushButton("Delete") + self.clearAllButton = QPushButton("Clear All") + # Lay out the GUI + layout = QVBoxLayout() + layout.addWidget(self.addButton) + layout.addWidget(self.deleteButton) + layout.addStretch() + layout.addWidget(self.clearAllButton) + self.layout.addWidget(self.table) + self.layout.addLayout(layout) diff --git a/python-contact-book/source_code_step_5/contacts.sqlite b/python-contact-book/source_code_step_5/contacts.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..f21ffb7a53e884902aee27e4a3c404cfa2263e6d Binary files /dev/null and b/python-contact-book/source_code_step_5/contacts.sqlite differ diff --git a/python-contact-book/source_code_step_5/rpcontacts.py b/python-contact-book/source_code_step_5/rpcontacts.py new file mode 100644 index 0000000000000000000000000000000000000000..73634de5cf723711bbb8d50729279282590c3a99 --- /dev/null +++ b/python-contact-book/source_code_step_5/rpcontacts.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts entry point script.""" + +from rpcontacts.main import main + +if __name__ == "__main__": + main() diff --git a/python-contact-book/source_code_step_5/rpcontacts/__init__.py b/python-contact-book/source_code_step_5/rpcontacts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a532bfc8d76390edbe732df6c84d86bce153f6cf --- /dev/null +++ b/python-contact-book/source_code_step_5/rpcontacts/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +"""This module provides the rpcontacts package.""" + +__version__ = "0.1.0" diff --git a/python-contact-book/source_code_step_5/rpcontacts/database.py b/python-contact-book/source_code_step_5/rpcontacts/database.py new file mode 100644 index 0000000000000000000000000000000000000000..a705bcc2a8d3830ad37a1c5905fa5788d5279cc4 --- /dev/null +++ b/python-contact-book/source_code_step_5/rpcontacts/database.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +"""This module provides a database connection.""" + +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtSql import QSqlDatabase, QSqlQuery + + +def _createContactsTable(): + """Create the contacts table in the database.""" + createTableQuery = QSqlQuery() + return createTableQuery.exec( + """ + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + name VARCHAR(40) NOT NULL, + job VARCHAR(50), + email VARCHAR(40) NOT NULL + ) + """ + ) + + +def createConnection(databaseName): + """Create and open a database connection.""" + connection = QSqlDatabase.addDatabase("QSQLITE") + connection.setDatabaseName(databaseName) + + if not connection.open(): + QMessageBox.warning( + None, + "RP Contact", + f"Database Error: {connection.lastError().text()}", + ) + return False + + _createContactsTable() + return True diff --git a/python-contact-book/source_code_step_5/rpcontacts/main.py b/python-contact-book/source_code_step_5/rpcontacts/main.py new file mode 100644 index 0000000000000000000000000000000000000000..0ddac2d40861763e51528044dcd731db95dee7ae --- /dev/null +++ b/python-contact-book/source_code_step_5/rpcontacts/main.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts application.""" + +import sys + +from PyQt5.QtWidgets import QApplication + +from .database import createConnection +from .views import Window + + +def main(): + """RP Contacts main function.""" + # Create the application + app = QApplication(sys.argv) + # Connect to the database before creating any window + if not createConnection("contacts.sqlite"): + sys.exit(1) + # Create the main window if the connection succeeded + win = Window() + win.show() + # Run the event loop + sys.exit(app.exec_()) diff --git a/python-contact-book/source_code_step_5/rpcontacts/model.py b/python-contact-book/source_code_step_5/rpcontacts/model.py new file mode 100644 index 0000000000000000000000000000000000000000..9d74c123fe9411046af177eda129961c7fb77f1f --- /dev/null +++ b/python-contact-book/source_code_step_5/rpcontacts/model.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +"""This module provides a model to manage the contacts table.""" + +from PyQt5.QtCore import Qt +from PyQt5.QtSql import QSqlTableModel + + +class ContactsModel: + def __init__(self): + self.model = self._createModel() + + @staticmethod + def _createModel(): + """Create and set up the model.""" + tableModel = QSqlTableModel() + tableModel.setTable("contacts") + tableModel.setEditStrategy(QSqlTableModel.OnFieldChange) + tableModel.select() + headers = ("ID", "Name", "Job", "Email") + for columnIndex, header in enumerate(headers): + tableModel.setHeaderData(columnIndex, Qt.Horizontal, header) + return tableModel + + def addContact(self, data): + """Add a contact to the database.""" + rows = self.model.rowCount() + self.model.insertRows(rows, 1) + for column_index, field in enumerate(data): + self.model.setData(self.model.index(rows, column_index + 1), field) + self.model.submitAll() + self.model.select() diff --git a/python-contact-book/source_code_step_5/rpcontacts/views.py b/python-contact-book/source_code_step_5/rpcontacts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..b71bca80fa1af203badb0f50d0b18048bd0a6a92 --- /dev/null +++ b/python-contact-book/source_code_step_5/rpcontacts/views.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +"""This module provides views to manage the contacts table.""" + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QAbstractItemView, + QDialog, + QDialogButtonBox, + QFormLayout, + QHBoxLayout, + QLineEdit, + QMainWindow, + QMessageBox, + QPushButton, + QTableView, + QVBoxLayout, + QWidget, +) + +from .model import ContactsModel + + +class Window(QMainWindow): + """Main Window.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent) + self.setWindowTitle("RP Contacts") + self.resize(550, 250) + self.centralWidget = QWidget() + self.setCentralWidget(self.centralWidget) + self.layout = QHBoxLayout() + self.centralWidget.setLayout(self.layout) + + self.contactsModel = ContactsModel() + self.setupUI() + + def setupUI(self): + """Setup the main window's GUI.""" + # Create the table view widget + self.table = QTableView() + self.table.setModel(self.contactsModel.model) + self.table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table.resizeColumnsToContents() + # Create buttons + self.addButton = QPushButton("Add...") + self.addButton.clicked.connect(self.openAddDialog) + self.deleteButton = QPushButton("Delete") + self.clearAllButton = QPushButton("Clear All") + # Lay out the GUI + layout = QVBoxLayout() + layout.addWidget(self.addButton) + layout.addWidget(self.deleteButton) + layout.addStretch() + layout.addWidget(self.clearAllButton) + self.layout.addWidget(self.table) + self.layout.addLayout(layout) + + def openAddDialog(self): + """Open the Add Contact dialog.""" + dialog = AddDialog(self) + if dialog.exec() == QDialog.Accepted: + self.contactsModel.addContact(dialog.data) + self.table.resizeColumnsToContents() + + +class AddDialog(QDialog): + """Add Contact dialog.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent=parent) + self.setWindowTitle("Add Contact") + self.layout = QVBoxLayout() + self.setLayout(self.layout) + self.data = None + + self.setupUI() + + def setupUI(self): + """Setup the Add Contact dialog's GUI.""" + # Create line edits for data fields + self.nameField = QLineEdit() + self.nameField.setObjectName("Name") + self.jobField = QLineEdit() + self.jobField.setObjectName("Job") + self.emailField = QLineEdit() + self.emailField.setObjectName("Email") + # Lay out the data fields + layout = QFormLayout() + layout.addRow("Name:", self.nameField) + layout.addRow("Job:", self.jobField) + layout.addRow("Email:", self.emailField) + self.layout.addLayout(layout) + # Add standard buttons to the dialog and connect them + self.buttonsBox = QDialogButtonBox(self) + self.buttonsBox.setOrientation(Qt.Horizontal) + self.buttonsBox.setStandardButtons( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel + ) + self.buttonsBox.accepted.connect(self.accept) + self.buttonsBox.rejected.connect(self.reject) + self.layout.addWidget(self.buttonsBox) + + def accept(self): + """Accept the data provided through the dialog.""" + self.data = [] + for field in (self.nameField, self.jobField, self.emailField): + if not field.text(): + QMessageBox.critical( + self, + "Error!", + f"You must provide a contact's {field.objectName()}", + ) + self.data = None # Reset .data + return + + self.data.append(field.text()) + + if not self.data: + return + + super().accept() diff --git a/python-contact-book/source_code_step_6/contacts.sqlite b/python-contact-book/source_code_step_6/contacts.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..af3a3886a0a728cc30fc5634ff92755897e749a7 Binary files /dev/null and b/python-contact-book/source_code_step_6/contacts.sqlite differ diff --git a/python-contact-book/source_code_step_6/rpcontacts.py b/python-contact-book/source_code_step_6/rpcontacts.py new file mode 100644 index 0000000000000000000000000000000000000000..73634de5cf723711bbb8d50729279282590c3a99 --- /dev/null +++ b/python-contact-book/source_code_step_6/rpcontacts.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts entry point script.""" + +from rpcontacts.main import main + +if __name__ == "__main__": + main() diff --git a/python-contact-book/source_code_step_6/rpcontacts/__init__.py b/python-contact-book/source_code_step_6/rpcontacts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a532bfc8d76390edbe732df6c84d86bce153f6cf --- /dev/null +++ b/python-contact-book/source_code_step_6/rpcontacts/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +"""This module provides the rpcontacts package.""" + +__version__ = "0.1.0" diff --git a/python-contact-book/source_code_step_6/rpcontacts/database.py b/python-contact-book/source_code_step_6/rpcontacts/database.py new file mode 100644 index 0000000000000000000000000000000000000000..a705bcc2a8d3830ad37a1c5905fa5788d5279cc4 --- /dev/null +++ b/python-contact-book/source_code_step_6/rpcontacts/database.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +"""This module provides a database connection.""" + +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtSql import QSqlDatabase, QSqlQuery + + +def _createContactsTable(): + """Create the contacts table in the database.""" + createTableQuery = QSqlQuery() + return createTableQuery.exec( + """ + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + name VARCHAR(40) NOT NULL, + job VARCHAR(50), + email VARCHAR(40) NOT NULL + ) + """ + ) + + +def createConnection(databaseName): + """Create and open a database connection.""" + connection = QSqlDatabase.addDatabase("QSQLITE") + connection.setDatabaseName(databaseName) + + if not connection.open(): + QMessageBox.warning( + None, + "RP Contact", + f"Database Error: {connection.lastError().text()}", + ) + return False + + _createContactsTable() + return True diff --git a/python-contact-book/source_code_step_6/rpcontacts/main.py b/python-contact-book/source_code_step_6/rpcontacts/main.py new file mode 100644 index 0000000000000000000000000000000000000000..0ddac2d40861763e51528044dcd731db95dee7ae --- /dev/null +++ b/python-contact-book/source_code_step_6/rpcontacts/main.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +"""This module provides RP Contacts application.""" + +import sys + +from PyQt5.QtWidgets import QApplication + +from .database import createConnection +from .views import Window + + +def main(): + """RP Contacts main function.""" + # Create the application + app = QApplication(sys.argv) + # Connect to the database before creating any window + if not createConnection("contacts.sqlite"): + sys.exit(1) + # Create the main window if the connection succeeded + win = Window() + win.show() + # Run the event loop + sys.exit(app.exec_()) diff --git a/python-contact-book/source_code_step_6/rpcontacts/model.py b/python-contact-book/source_code_step_6/rpcontacts/model.py new file mode 100644 index 0000000000000000000000000000000000000000..df7df8e895e850c1d75b9ca00296d84e67e42181 --- /dev/null +++ b/python-contact-book/source_code_step_6/rpcontacts/model.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +"""This module provides a model to manage the contacts table.""" + +from PyQt5.QtCore import Qt +from PyQt5.QtSql import QSqlTableModel + + +class ContactsModel: + def __init__(self): + self.model = self._createModel() + + @staticmethod + def _createModel(): + """Create and set up the model.""" + tableModel = QSqlTableModel() + tableModel.setTable("contacts") + tableModel.setEditStrategy(QSqlTableModel.OnFieldChange) + tableModel.select() + headers = ("ID", "Name", "Job", "Email") + for columnIndex, header in enumerate(headers): + tableModel.setHeaderData(columnIndex, Qt.Horizontal, header) + return tableModel + + def addContact(self, data): + """Add a contact to the database.""" + rows = self.model.rowCount() + self.model.insertRows(rows, 1) + for column_index, field in enumerate(data): + self.model.setData(self.model.index(rows, column_index + 1), field) + self.model.submitAll() + self.model.select() + + def deleteContact(self, row): + """Remove a contact from the database.""" + self.model.removeRow(row) + self.model.submitAll() + self.model.select() + + def clearContacts(self): + """Remove all contacts in the database.""" + self.model.setEditStrategy(QSqlTableModel.OnManualSubmit) + self.model.removeRows(0, self.model.rowCount()) + self.model.submitAll() + self.model.setEditStrategy(QSqlTableModel.OnFieldChange) + self.model.select() diff --git a/python-contact-book/source_code_step_6/rpcontacts/views.py b/python-contact-book/source_code_step_6/rpcontacts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..592d1070aa018e7fee77f9f1e5e557e2562de757 --- /dev/null +++ b/python-contact-book/source_code_step_6/rpcontacts/views.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +"""This module provides views to manage the contacts table.""" + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QAbstractItemView, + QDialog, + QDialogButtonBox, + QFormLayout, + QHBoxLayout, + QLineEdit, + QMainWindow, + QMessageBox, + QPushButton, + QTableView, + QVBoxLayout, + QWidget, +) + +from .model import ContactsModel + + +class Window(QMainWindow): + """Main Window.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent) + self.setWindowTitle("RP Contacts") + self.resize(550, 250) + self.centralWidget = QWidget() + self.setCentralWidget(self.centralWidget) + self.layout = QHBoxLayout() + self.centralWidget.setLayout(self.layout) + + self.contactsModel = ContactsModel() + self.setupUI() + + def setupUI(self): + """Setup the main window's GUI.""" + # Create the table view widget + self.table = QTableView() + self.table.setModel(self.contactsModel.model) + self.table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table.resizeColumnsToContents() + # Create buttons + self.addButton = QPushButton("Add...") + self.addButton.clicked.connect(self.openAddDialog) + self.deleteButton = QPushButton("Delete") + self.deleteButton.clicked.connect(self.deleteContact) + self.clearAllButton = QPushButton("Clear All") + self.clearAllButton.clicked.connect(self.clearContacts) + # Lay out the GUI + layout = QVBoxLayout() + layout.addWidget(self.addButton) + layout.addWidget(self.deleteButton) + layout.addStretch() + layout.addWidget(self.clearAllButton) + self.layout.addWidget(self.table) + self.layout.addLayout(layout) + + def openAddDialog(self): + """Open the Add Contact dialog.""" + dialog = AddDialog(self) + if dialog.exec() == QDialog.Accepted: + self.contactsModel.addContact(dialog.data) + self.table.resizeColumnsToContents() + + def deleteContact(self): + """Delete the selected contact from the database.""" + row = self.table.currentIndex().row() + if row < 0: + return + + messageBox = QMessageBox.warning( + self, + "Warning!", + "Do you want to remove the selected contact?", + QMessageBox.Ok | QMessageBox.Cancel, + ) + + if messageBox == QMessageBox.Ok: + self.contactsModel.deleteContact(row) + + def clearContacts(self): + """Remove all contacts from the database.""" + messageBox = QMessageBox.warning( + self, + "Warning!", + "Do you want to remove all your contacts?", + QMessageBox.Ok | QMessageBox.Cancel, + ) + + if messageBox == QMessageBox.Ok: + self.contactsModel.clearContacts() + + +class AddDialog(QDialog): + """Add Contact dialog.""" + + def __init__(self, parent=None): + """Initializer.""" + super().__init__(parent=parent) + self.setWindowTitle("Add Contact") + self.layout = QVBoxLayout() + self.setLayout(self.layout) + self.data = None + + self.setupUI() + + def setupUI(self): + """Setup the Add Contact dialog's GUI.""" + # Create line edits for data fields + self.nameField = QLineEdit() + self.nameField.setObjectName("Name") + self.jobField = QLineEdit() + self.jobField.setObjectName("Job") + self.emailField = QLineEdit() + self.emailField.setObjectName("Email") + # Lay out the data fields + layout = QFormLayout() + layout.addRow("Name:", self.nameField) + layout.addRow("Job:", self.jobField) + layout.addRow("Email:", self.emailField) + self.layout.addLayout(layout) + # Add standard buttons to the dialog and connect them + self.buttonsBox = QDialogButtonBox(self) + self.buttonsBox.setOrientation(Qt.Horizontal) + self.buttonsBox.setStandardButtons( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel + ) + self.buttonsBox.accepted.connect(self.accept) + self.buttonsBox.rejected.connect(self.reject) + self.layout.addWidget(self.buttonsBox) + + def accept(self): + """Accept the data provided through the dialog.""" + self.data = [] + for field in (self.nameField, self.jobField, self.emailField): + if not field.text(): + QMessageBox.critical( + self, + "Error!", + f"You must provide a contact's {field.objectName()}", + ) + self.data = None # Reset .data + return + + self.data.append(field.text()) + + if not self.data: + return + + super().accept() diff --git a/python-heapq-module/README.md b/python-heapq-module/README.md new file mode 100644 index 0000000000000000000000000000000000000000..00e34013326c20a6d190a3981edbf1e76e6f3dc5 --- /dev/null +++ b/python-heapq-module/README.md @@ -0,0 +1,28 @@ +# Finding the Shortest Path on a Map + +Code supplementing the [Using the Python "heapq" Module and Priority Queues](https://realpython.com/python-heapq-module/) article. + +## Usage + +Run the following program: + +```shell +$ python shortest-path.py +``` + +It will print out a map with the shortest path from the top-left corner to the bottom-right corner indicated with `@`. + +## Changing the Map + +In order to change the map the robot uses, modify the triple-quoted string that is assigned to `map` near the top of the file. Anything except `X` is interpreted as free from obstacles. Using `.` makes the map easier to read directly. You can make the map bigger, and a lot more complicated. + +If there are so many obstacles that make finding a path from the top-left corner to the bottom right corner impossible, the program will raise an exception. + +## Changing the Rules + +If you want to play around with the code, here are some changes that you can make: + +* How would you change the code so that the robot cannot go diagonally? +* How would you change the code so that the robot can only move right or down, but never left or up? +* (Harder) How would you change the code so that every step consumes energy, squares with `*` give energy, and the robot cannot move if it is out of energy? +* (Challenge) How would you change the code so that areas marked with `#` are not obstacles, but take twice as long to move through? diff --git a/python-heapq-module/shortest-path.py b/python-heapq-module/shortest-path.py new file mode 100644 index 0000000000000000000000000000000000000000..e5b2ce82de79d230f64107af7ff80fe1aed5de00 --- /dev/null +++ b/python-heapq-module/shortest-path.py @@ -0,0 +1,77 @@ +import heapq + + +map = """\ +.......X.. +.......X.. +....XXXX.. +.......... +.......... +""" + + +def parse_map(map): + lines = map.splitlines() + origin = 0, 0 + destination = len(lines[-1]) - 1, len(lines) - 1 + return lines, origin, destination + + +def is_valid(lines, position): + x, y = position + if not (0 <= y < len(lines) and 0 <= x < len(lines[y])): + return False + if lines[y][x] == "X": + return False + return True + + +def get_neighbors(lines, current): + x, y = current + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + if dx == 0 and dy == 0: + continue + position = x + dx, y + dy + if is_valid(lines, position): + yield position + + +def get_shorter_paths(tentative, positions, through): + path = tentative[through] + [through] + for position in positions: + if position in tentative and len(tentative[position]) <= len(path): + continue + yield position, path + + +def find_path(map): + lines, origin, destination = parse_map(map) + tentative = {origin: []} + candidates = [(0, origin)] + certain = set() + while destination not in certain and len(candidates) > 0: + _ignored, current = heapq.heappop(candidates) + if current in certain: + continue + certain.add(current) + neighbors = set(get_neighbors(lines, current)) - certain + shorter = get_shorter_paths(tentative, neighbors, current) + for neighbor, path in shorter: + tentative[neighbor] = path + heapq.heappush(candidates, (len(path), neighbor)) + if destination in tentative: + return tentative[destination] + [destination] + else: + raise ValueError("no path") + + +def show_path(path, map): + lines = map.splitlines() + for x, y in path: + lines[y] = lines[y][:x] + "@" + lines[y][x + 1 :] + return "\n".join(lines) + "\n" + + +path = find_path(map) +print(show_path(path, map))