From acb74775400b985acbf45464f85296f71561f6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:47:42 +0000 Subject: [PATCH] Codex CLI materials. --- codex-cli/README.md | 4 + codex-cli/rpcontacts/.gitignore | 1 + codex-cli/rpcontacts/README.md | 19 +++ codex-cli/rpcontacts/pyproject.toml | 19 +++ .../rpcontacts/src/rpcontacts/__init__.py | 1 + .../rpcontacts/src/rpcontacts/__main__.py | 11 ++ .../rpcontacts/src/rpcontacts/database.py | 52 +++++++ .../rpcontacts/src/rpcontacts/rpcontacts.tcss | 75 ++++++++++ codex-cli/rpcontacts/src/rpcontacts/tui.py | 126 +++++++++++++++++ codex-cli/rpcontacts/uv.lock | 130 ++++++++++++++++++ 10 files changed, 438 insertions(+) create mode 100644 codex-cli/README.md create mode 100644 codex-cli/rpcontacts/.gitignore create mode 100644 codex-cli/rpcontacts/README.md create mode 100644 codex-cli/rpcontacts/pyproject.toml create mode 100644 codex-cli/rpcontacts/src/rpcontacts/__init__.py create mode 100644 codex-cli/rpcontacts/src/rpcontacts/__main__.py create mode 100644 codex-cli/rpcontacts/src/rpcontacts/database.py create mode 100644 codex-cli/rpcontacts/src/rpcontacts/rpcontacts.tcss create mode 100644 codex-cli/rpcontacts/src/rpcontacts/tui.py create mode 100644 codex-cli/rpcontacts/uv.lock diff --git a/codex-cli/README.md b/codex-cli/README.md new file mode 100644 index 0000000000..a1969d5d9e --- /dev/null +++ b/codex-cli/README.md @@ -0,0 +1,4 @@ +# How to Add Features to a Python Project With Codex CLI + +This is a companion project to the ["How to Add Features to a Python Project With Codex CLI](https://realpython.com/codex-cli) tutorial on Real Python. +Take a look at the tutorial to see how to finish this project using Codex CLI. diff --git a/codex-cli/rpcontacts/.gitignore b/codex-cli/rpcontacts/.gitignore new file mode 100644 index 0000000000..bee8a64b79 --- /dev/null +++ b/codex-cli/rpcontacts/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/codex-cli/rpcontacts/README.md b/codex-cli/rpcontacts/README.md new file mode 100644 index 0000000000..6fb6e94747 --- /dev/null +++ b/codex-cli/rpcontacts/README.md @@ -0,0 +1,19 @@ +# RP Contacts + +RP Contacts is a contact book application built with Python and Textual. + +## Run the Project + +Using uv: + +```sh +(venv) $ uv run rpcontacts +``` + +## About the Author + +Real Python - Email: office@realpython.com + +## License + +Distributed under the MIT license. See `LICENSE` for more information. diff --git a/codex-cli/rpcontacts/pyproject.toml b/codex-cli/rpcontacts/pyproject.toml new file mode 100644 index 0000000000..65a403e4ed --- /dev/null +++ b/codex-cli/rpcontacts/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "rpcontacts" +version = "0.1.0" +description = "RP Contacts is a contact book application built with Python and Textual." +readme = "README.md" +authors = [ + { name = "Real Python", email = "office@realpython.com" } +] +requires-python = ">=3.14" +dependencies = [ + "textual==8.0.0", +] + +[project.scripts] +rpcontacts = "rpcontacts.__main__:main" + +[build-system] +requires = ["uv_build>=0.10.6,<0.11.0"] +build-backend = "uv_build" diff --git a/codex-cli/rpcontacts/src/rpcontacts/__init__.py b/codex-cli/rpcontacts/src/rpcontacts/__init__.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/codex-cli/rpcontacts/src/rpcontacts/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/codex-cli/rpcontacts/src/rpcontacts/__main__.py b/codex-cli/rpcontacts/src/rpcontacts/__main__.py new file mode 100644 index 0000000000..a0bdaead0e --- /dev/null +++ b/codex-cli/rpcontacts/src/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/codex-cli/rpcontacts/src/rpcontacts/database.py b/codex-cli/rpcontacts/src/rpcontacts/database.py new file mode 100644 index 0000000000..c88dacbf98 --- /dev/null +++ b/codex-cli/rpcontacts/src/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/codex-cli/rpcontacts/src/rpcontacts/rpcontacts.tcss b/codex-cli/rpcontacts/src/rpcontacts/rpcontacts.tcss new file mode 100644 index 0000000000..02ea7df5c4 --- /dev/null +++ b/codex-cli/rpcontacts/src/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/codex-cli/rpcontacts/src/rpcontacts/tui.py b/codex-cli/rpcontacts/src/rpcontacts/tui.py new file mode 100644 index 0000000000..ab422bba01 --- /dev/null +++ b/codex-cli/rpcontacts/src/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_all", "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/codex-cli/rpcontacts/uv.lock b/codex-cli/rpcontacts/uv.lock new file mode 100644 index 0000000000..e30510dcff --- /dev/null +++ b/codex-cli/rpcontacts/uv.lock @@ -0,0 +1,130 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rpcontacts" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "textual" }, +] + +[package.metadata] +requires-dist = [{ name = "textual", specifier = "==8.0.0" }] + +[[package]] +name = "textual" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/1e1f705825359590ddfaeda57653bd518c4ff7a96bb2c3239ba1b6fc4c51/textual-8.0.0.tar.gz", hash = "sha256:ce48f83a3d686c0fac0e80bf9136e1f8851c653aa6a4502e43293a151df18809", size = 1595895, upload-time = "2026-02-16T17:12:14.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/be/e191c2a15da20530fde03564564e3e4b4220eb9d687d4014957e5c6a5e85/textual-8.0.0-py3-none-any.whl", hash = "sha256:8908f4ebe93a6b4f77ca7262197784a52162bc88b05f4ecf50ac93a92d49bb8f", size = 718904, upload-time = "2026-02-16T17:12:11.962Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +]