Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions codex-cli/README.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions codex-cli/rpcontacts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
19 changes: 19 additions & 0 deletions codex-cli/rpcontacts/README.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions codex-cli/rpcontacts/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions codex-cli/rpcontacts/src/rpcontacts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
11 changes: 11 additions & 0 deletions codex-cli/rpcontacts/src/rpcontacts/__main__.py
Original file line number Diff line number Diff line change
@@ -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()
52 changes: 52 additions & 0 deletions codex-cli/rpcontacts/src/rpcontacts/database.py
Original file line number Diff line number Diff line change
@@ -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;")
75 changes: 75 additions & 0 deletions codex-cli/rpcontacts/src/rpcontacts/rpcontacts.tcss
Original file line number Diff line number Diff line change
@@ -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;
}
126 changes: 126 additions & 0 deletions codex-cli/rpcontacts/src/rpcontacts/tui.py
Original file line number Diff line number Diff line change
@@ -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(())
Loading