8000 Merge branch 'master' into python-closure · realpython/materials@625c2c1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 625c2c1

Browse files
authored
Merge branch 'master' into python-closure
2 parents 9d11980 + fe6ef2f commit 625c2c1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2301
-0
lines changed

contact-book-python-textual/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Build a Contact Book App With Python, Textual, and SQLite
2+
3+
This folder provides the code examples for the Real Python tutorial [Build a Contact Book App With Python, Textual, and SQLite](https://realpython.com/contact-book-python-textual/).
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# RP Contacts
2+
3+
**RP Contacts** is a contact book application built with Python, Textual, and SQLite.
4+
5+
## Installation
6+
7+
1. Create a Python virtual environment
8+
9+
```sh
10+
$ python -m venv ./venv
11+
$ source venv/bin/activate
12+
(venv) $
13+
```
14+
15+
2. Install the project's requirements
16+
17+
```sh
18+
(venv) $ python -m pip install -r requirements.txt
19+
```
20+
21+
## Run the Project
22+
23+
```sh
24+
(venv) $ python -m rpcontacts
25+
```
26+
27+
## About the Author
28+
29+
Real Python - Email: office@realpython.com
30+
31+
## License
32+
33+
Distributed under the MIT license. See `LICENSE` for more information.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
textual==0.75.1
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: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
@on(Button.Pressed, "#delete")
80+
def action_delete(self):
81+
contacts_list = self.query_one(DataTable)
82+
row_key, _ = contacts_list.coordinate_to_cell_key(
83+
contacts_list.cursor_coordinate
84+
)
85+
86+
def check_answer(accepted):
87+
if accepted and row_key:
88+
self.db.delete_contact(id=row_key.value)
89+
contacts_list.remove_row(row_key)
90+
91+
name = contacts_list.get_row(row_key)[0]
92+
self.push_screen(
93+
QuestionDialog(f"Do you want to delete {name}'s contact?"),
94+
check_answer,
95+
)
96+
97+
@on(Button.Pressed, "#clear")
98+
def action_clear_all(self):
99+
def check_answer(accepted):
100+
if accepted:
101+
self.db.clear_all_contacts()
102+
self.query_one(DataTable).clear()
103+
104+
self.push_screen(
105+
QuestionDialog("Are you sure you want to remove all contacts?"),
106+
check_answer,
107+
)
108+
109+
110+
class QuestionDialog(Screen):
111+
def __init__(self, message, *args, **kwargs):
112+
super().__init__(*args, **kwargs)
113+
self.message = message
114+
115+
def compose(self):
116+
no_button = Button("No", variant="primary", id="no")
117+
no_button.focus()
118+
119+
yield Grid(
120+
Label(self.message, id="question"),
121+
Button("Yes", variant="error", id="yes"),
122+
no_button,
123+
id="question-dialog",
124+
)
125+
126+
def on_button_pressed(self, event):
127+
if event.button.id == "yes":
128+
self.dismiss(True)
129+
else:
130+
self.dismiss(False)
131+
132+
133+
class InputDialog(Screen):
134+
def compose(self):
135+
yield Grid(
136+
Label("Add Contact", id="title"),
137+
Label("Name:", classes="label"),
138+
Input(placeholder="Contact Name", classes="input", id="name"),
139+
Label("Phone:", classes="label"),
140+
Input(placeholder="Contact Phone", classes="input", id="phone"),
141+
Label("Email:", classes="label"),
142+
Input(placeholder="Contact Email", classes="input", id="email"),
143+
Static(),
144+
Button("Cancel", variant="warning", id="cancel"),
145+
Button("Ok", variant="success", id="ok"),
146+
id="input-dialog",
147+
)
148+
149+
def on_button_pressed(self, event):
150+
if event.button.id == "ok":
151+
name = self.query_one("#name", Input).value
152+
phone = self.query_one("#phone", Input).value
153+
email = self.query_one("#email", Input).value
154+
self.dismiss((name, phone, email))
155+
else:
156+
self.dismiss(())
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# RP Contacts
2+
3+
RP Contacts is a contact book application built with Python and Textual.
4+
5+
## Installation
6+
7+
1. Create a Python virtual environment
8+
9+
```sh
10+
$ python -m venv ./venv
11+
$ source venv/bin/activate
12+
(venv) $
13+
```
14+
15+
2. Install the requirements
16+
17+
```sh
18+
(venv) $ python -m pip install -r requirements.txt
19+
```
20+
21+
## Run the Project
22+
23+
```sh
24+
(venv) $ python -m pip install -e .
25+
(venv) $ rpcontacts
26+
```
27+
28+
## About the Author
29+
30+
Real Python - Email: office@realpython.com
31+
32+
## License
33+
34+
Distributed under the MIT license. See `LICENSE` for more information.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
textual==0.75.1
2+
textual-dev==1.5.1

0 commit comments

Comments
 (0)
0