Skip to content

Commit 89ec497

Browse files
authored
Merge pull request #746 from realpython/codex-cli
Codex CLI materials.
2 parents 890dd11 + 72eb615 commit 89ec497

File tree

10 files changed

+438
-0
lines changed

10 files changed

+438
-0
lines changed

codex-cli/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# How to Add Features to a Python Project With Codex CLI
2+
3+
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.
4+
Take a look at the tutorial to see how to finish this project using Codex CLI.

codex-cli/rpcontacts/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

codex-cli/rpcontacts/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# RP Contacts
2+
3+
RP Contacts is a contact book application built with Python and Textual.
4+
5+
## Run the Project
6+
7+
Using uv:
8+
9+
```sh
10+
$ uv run rpcontacts
11+
```
12+
13+
## About the Author
14+
15+
Real Python - Email: office@realpython.com
16+
17+
## License
18+
19+
Distributed under the MIT license. See `LICENSE` for more information.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[project]
2+
name = "rpcontacts"
3+
version = "0.1.0"
4+
description = "RP Contacts is a contact book application built with Python and Textual."
5+
readme = "README.md"
6+
authors = [
7+
{ name = "Real Python", email = "office@realpython.com" }
8+
]
9+
requires-python = ">=3.12"
10+
dependencies = [
11+
"textual==8.0.0",
12+
]
13+
14+
[project.scripts]
15+
rpcontacts = "rpcontacts.__main__:main"
16+
17+
[build-system]
18+
requires = ["uv_build>=0.10.6,<0.11.0"]
19+
build-backend = "uv_build"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.0"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from rpcontacts.database import Database
2+
from rpcontacts.tui import ContactsApp
3+
4+
5+
def main():
6+
app = ContactsApp(db=Database())
7+
app.run()
8+
9+
10+
if __name__ == "__main__":
11+
main()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pathlib
2+
import sqlite3
3+
4+
DATABASE_PATH = pathlib.Path().home() / "contacts.db"
5+
6+
7+
class Database:
8+
def __init__(self, db_path=DATABASE_PATH):
9+
self.db = sqlite3.connect(db_path)
10+
self.cursor = self.db.cursor()
11+
self._create_table()
12+
13+
def _create_table(self):
14+
query = """
15+
CREATE TABLE IF NOT EXISTS contacts(
16+
id INTEGER PRIMARY KEY,
17+
name TEXT,
18+
phone TEXT,
19+
email TEXT
20+
);
21+
"""
22+
self._run_query(query)
23+
24+
def _run_query(self, query, *query_args):
25+
result = self.cursor.execute(query, [*query_args])
26+
self.db.commit()
27+
return result
28+
29+
def get_all_contacts(self):
30+
result = self._run_query("SELECT * FROM contacts;")
31+
return result.fetchall()
32+
33+
def get_last_contact(self):
34+
result = self._run_query(
35+
"SELECT * FROM contacts ORDER BY id DESC LIMIT 1;"
36+
)
37+
return result.fetchone()
38+
39+
def add_contact(self, contact):
40+
self._run_query(
41+
"INSERT INTO contacts VALUES (NULL, ?, ?, ?);",
42+
*contact,
43+
)
44+
45+
def delete_contact(self, id):
46+
self._run_query(
47+
"DELETE FROM contacts WHERE id=(?);",
48+
id,
49+
)
50+
51+
def clear_all_contacts(self):
52+
self._run_query("DELETE FROM contacts;")
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
QuestionDialog {
2+
align: center middle;
3+
}
4+
5+
#question-dialog {
6+
grid-size: 2;
7+
grid-gutter: 1 2;
8+
grid-rows: 1fr 3;
9+
padding: 0 1;
10+
width: 60;
11+
height: 11;
12+
border: solid red;
13+
background: $surface;
14+
}
15+
16+
#question {
17+
column-span: 2;
18+
height: 1fr;
19+
width: 1fr;
20+
content-align: center middle;
21+
}
22+
23+
Button {
24+
width: 100%;
25+
}
26+
27+
.contacts-list {
28+
width: 3fr;
29+
padding: 0 1;
30+
border: solid green;
31+
}
32+
33+
.buttons-panel {
34+
align: center top;
35+
padding: 0 1;
36+
width: auto;
37+
border: solid red;
38+
}
39+
40+
.separator {
41+
height: 1fr;
42+
}
43+
44+
InputDialog {
45+
align: center middle;
46+
}
47+
48+
#title {
49+
column-span: 3;
50+
height: 1fr;
51+
width: 1fr;
52+
content-align: center middle;
53+
color: green;
54+
text-style: bold;
55+
}
56+
57+
#input-dialog {
58+
grid-size: 3 5;
59+
grid-gutter: 1 1;
60+
padding: 0 1;
61+
width: 50;
62+
height: 20;
63+
border: solid green;
64+
background: $surface;
65+
}
66+
67+
.label {
68+
height: 1fr;
69+
width: 1fr;
70+
content-align: right middle;
71+
}
72+
73+
.input {
74+
column-span: 2;
75+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from textual.app import App, on
2+
from textual.containers import Grid, Horizontal, Vertical
3+
from textual.screen import Screen
4+
from textual.widgets import (
5+
Button,
6+
DataTable,
7+
Footer,
8+
Header,
9+
Input,
10+
Label,
11+
Static,
12+
)
13+
14+
15+
class ContactsApp(App):
16+
CSS_PATH = "rpcontacts.tcss"
17+
BINDINGS = [
18+
("m", "toggle_dark", "Toggle dark mode"),
19+
("a", "add", "Add"),
20+
("d", "delete", "Delete"),
21+
("c", "clear_all", "Clear All"),
22+
("q", "request_quit", "Quit"),
23+
]
24+
25+
def __init__(self, db):
26+
super().__init__()
27+
self.db = db
28+
29+
def compose(self):
30+
yield Header()
31+
contacts_list = DataTable(classes="contacts-list")
32+
contacts_list.focus()
33+
contacts_list.add_columns("Name", "Phone", "Email")
34+
contacts_list.cursor_type = "row"
35+
contacts_list.zebra_stripes = True
36+
add_button = Button("Add", variant="success", id="add")
37+
add_button.focus()
38+
buttons_panel = Vertical(
39+
add_button,
40+
Button("Delete", variant="warning", id="delete"),
41+
Static(classes="separator"),
42+
Button("Clear All", variant="error", id="clear"),
43+
classes="buttons-panel",
44+
)
45+
yield Horizontal(contacts_list, buttons_panel)
46+
yield Footer()
47+
48+
def on_mount(self):
49+
self.title = "RP Contacts"
50+
self.sub_title = "A Contacts Book App With Textual & Python"
51+
self._load_contacts()
52+
53+
def _load_contacts(self):
54+
contacts_list = self.query_one(DataTable)
55+
for contact_data in self.db.get_all_contacts():
56+
id, *contact = contact_data
57+
contacts_list.add_row(*contact, key=id)
58+
59+
def action_toggle_dark(self):
60+
self.dark = not self.dark
61+
62+
def action_request_quit(self):
63+
def check_answer(accepted):
64+
if accepted:
65+
self.exit()
66+
67+
self.push_screen(QuestionDialog("Do you want to quit?"), check_answer)
68+
69+
@on(Button.Pressed, "#add")
70+
def action_add(self):
71+
def check_contact(contact_data):
72+
if contact_data:
73+
self.db.add_contact(contact_data)
74+
id, *contact = self.db.get_last_contact()
75+
self.query_one(DataTable).add_row(*contact, key=id)
76+
77+
self.push_screen(InputDialog(), check_contact)
78+
79+
80+
class QuestionDialog(Screen):
81+
def __init__(self, message, *args, **kwargs):
82+
super().__init__(*args, **kwargs)
83+
self.message = message
84+
85+
def compose(self):
86+
no_button = Button("No", variant="primary", id="no")
87+
no_button.focus()
88+
89+
yield Grid(
90+
Label(self.message, id="question"),
91+
Button("Yes", variant="error", id="yes"),
92+
no_button,
93+
id="question-dialog",
94+
)
95+
96+
def on_button_pressed(self, event):
97+
if event.button.id == "yes":
98+
self.dismiss(True)
99+
else:
100+
self.dismiss(False)
101+
102+
103+
class InputDialog(Screen):
104+
def compose(self):
105+
yield Grid(
106+
Label("Add Contact", id="title"),
107+
Label("Name:", classes="label"),
108+
Input(placeholder="Contact Name", classes="input", id="name"),
109+
Label("Phone:", classes="label"),
110+
Input(placeholder="Contact Phone", classes="input", id="phone"),
111+
Label("Email:", classes="label"),
112+
Input(placeholder="Contact Email", classes="input", id="email"),
113+
Static(),
114+
Button("Cancel", variant="warning", id="cancel"),
115+
Button("Ok", variant="success", id="ok"),
116+
id="input-dialog",
117+
)
118+
119+
def on_button_pressed(self, event):
120+
if event.button.id == "ok":
121+
name = self.query_one("#name", Input).value
122+
phone = self.query_one("#phone", Input).value
123+
email = self.query_one("#email", Input).value
124+
self.dismiss((name, phone, email))
125+
else:
126+
self.dismiss(())

0 commit comments

Comments
 (0)