Skip to content

Commit

Permalink
Merge branch 'master' into syntactic-sugar-python
Browse files Browse the repository at this point in the history
  • Loading branch information
brendaweles authored Sep 20, 2024
2 parents 61d4786 + e52d11b commit ca54e62
Show file tree
Hide file tree
Showing 59 changed files with 2,155 additions and 0 deletions.
3 changes: 3 additions & 0 deletions contact-book-python-textual/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Build a Contact Book App With Python, Textual, and SQLite

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/).
33 changes: 33 additions & 0 deletions contact-book-python-textual/source_code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# RP Contacts

**RP Contacts** is a contact book application built with Python, Textual, and SQLite.

## Installation

1. Create a Python virtual environment

```sh
$ python -m venv ./venv
$ source venv/bin/activate
(venv) $
```

2. Install the project's requirements

```sh
(venv) $ python -m pip install -r requirements.txt
```

## Run the Project

```sh
(venv) $ python -m rpcontacts
```

## About the Author

Real Python - Email: [email protected]

## License

Distributed under the MIT license. See `LICENSE` for more information.
1 change: 1 addition & 0 deletions contact-book-python-textual/source_code/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
textual==0.75.1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
11 changes: 11 additions & 0 deletions contact-book-python-textual/source_code/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 contact-book-python-textual/source_code/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 contact-book-python-textual/source_code/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;
}
156 changes: 156 additions & 0 deletions contact-book-python-textual/source_code/rpcontacts/tui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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)

@on(Button.Pressed, "#delete")
def action_delete(self):
contacts_list = self.query_one(DataTable)
row_key, _ = contacts_list.coordinate_to_cell_key(
contacts_list.cursor_coordinate
)

def check_answer(accepted):
if accepted and row_key:
self.db.delete_contact(id=row_key.value)
contacts_list.remove_row(row_key)

name = contacts_list.get_row(row_key)[0]
self.push_screen(
QuestionDialog(f"Do you want to delete {name}'s contact?"),
check_answer,
)

@on(Button.Pressed, "#clear")
def action_clear_all(self):
def check_answer(accepted):
if accepted:
self.db.clear_all_contacts()
self.query_one(DataTable).clear()

self.push_screen(
QuestionDialog("Are you sure you want to remove all contacts?"),
check_answer,
)


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(())
34 changes: 34 additions & 0 deletions contact-book-python-textual/source_code_step_1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# RP Contacts

RP Contacts is a contact book application built with Python and Textual.

## Installation

1. Create a Python virtual environment

```sh
$ python -m venv ./venv
$ source venv/bin/activate
(venv) $
```

2. Install the requirements

```sh
(venv) $ python -m pip install -r requirements.txt
```

## Run the Project

```sh
(venv) $ python -m pip install -e .
(venv) $ rpcontacts
```

## About the Author

Real Python - Email: [email protected]

## License

Distributed under the MIT license. See `LICENSE` for more information.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
textual==0.75.1
textual-dev==1.5.1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from rpcontacts.tui import ContactsApp


def main():
app = ContactsApp()
app.run()


if __name__ == "__main__":
main()
Empty file.
Loading

0 comments on commit ca54e62

Please sign in to comment.