Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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