voidcli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
void/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """ VOID APP SRC CODE """
@@ -0,0 +1 @@
1
+ """Controllers module"""
@@ -0,0 +1,44 @@
1
+ import sqlite3
2
+
3
+ from void.models.activity import Activity
4
+
5
+
6
+ class ActivityController:
7
+
8
+ def get_activities(self) -> list[tuple]:
9
+ with Activity() as ac:
10
+ raw_activities = ac.get_active_activities()
11
+ activities = []
12
+ for item in raw_activities:
13
+ activities.append((item['id'], item['activity'], item['category']))
14
+ return activities
15
+
16
+ def get_activities_by_category(self, category_id):
17
+ with Activity() as ac:
18
+ raw_activities = ac.get_activities_by_category(category_id)
19
+ activities = []
20
+ for item in raw_activities:
21
+ activities.append((item["id"], item['name']))
22
+ return activities
23
+
24
+ def create_activity(self, activity, category_id) -> bool:
25
+ """Return False when the name is already used in that category."""
26
+ with Activity() as ac:
27
+ try:
28
+ ac.create_activity(activity, category_id)
29
+ except sqlite3.IntegrityError:
30
+ return False
31
+ return True
32
+
33
+ def update_activity(self, activity_id, activity, category_id) -> bool:
34
+ """Return False when the name is already used in that category."""
35
+ with Activity() as ac:
36
+ try:
37
+ ac.update_activity(activity_id, activity, category_id)
38
+ except sqlite3.IntegrityError:
39
+ return False
40
+ return True
41
+
42
+ def suspend_activity(self, activity_id):
43
+ with Activity() as ac:
44
+ ac.suspend_activity(activity_id)
@@ -0,0 +1,30 @@
1
+ import sqlite3
2
+
3
+ from void.models.category import Category
4
+
5
+
6
+ class CategoryController:
7
+ def get_categories_for_activities(self) -> list[tuple]:
8
+ with Category() as ca:
9
+ raw_categories = ca.get_active_categories_for_activities()
10
+ categories = []
11
+ for item in raw_categories:
12
+ categories.append((item['category'], item['id']))
13
+ return categories
14
+
15
+ def get_categories(self) -> list[tuple]:
16
+ with Category() as ca:
17
+ raw_categories = ca.get_active_categories()
18
+ categories = []
19
+ for item in raw_categories:
20
+ categories.append((item['category'],))
21
+ return categories
22
+
23
+ def create_category(self, category) -> bool:
24
+ """Return False when a category with that name already exists."""
25
+ with Category() as ca:
26
+ try:
27
+ ca.create_category(category)
28
+ except sqlite3.IntegrityError:
29
+ return False
30
+ return True
@@ -0,0 +1,47 @@
1
+ from void.controllers.activity import ActivityController
2
+ from void.controllers.category import CategoryController
3
+ from void.models.note import Note
4
+
5
+
6
+ class NoteController:
7
+
8
+ def __init__(self):
9
+ self.category_ctrl = CategoryController()
10
+ self.activity_ctrl = ActivityController()
11
+
12
+
13
+ def get_activities_by_category(self):
14
+ active_categories = self.category_ctrl.get_categories_for_activities()
15
+ categories_and_activities = []
16
+ for cat in active_categories:
17
+ category, id = cat
18
+ activities = self.activity_ctrl.get_activities_by_category(category_id=id)
19
+ data = (id, category, activities)
20
+ categories_and_activities.append(data)
21
+ return categories_and_activities
22
+
23
+ def get_activities(self):
24
+ """Every active activity as (id, name, category)."""
25
+ return self.activity_ctrl.get_activities()
26
+
27
+ def get_activity_total(self):
28
+ return len(self.get_activities())
29
+
30
+ def save_note(self, date_str, activities):
31
+ with Note() as note:
32
+ note_id = note.save_note(date_str)
33
+ for activity in activities:
34
+ activity_info, activity_note = activity
35
+ self.save_note_details(note_id, activity_info, activity_note)
36
+
37
+ def save_note_details(self, note_id, activity_info, activity_note):
38
+ with Note() as note:
39
+ note.save_note_detail(note_id, activity_info, activity_note)
40
+
41
+ def get_day_note(self, date_str):
42
+ with Note() as note:
43
+ return note.get_day_note(date_str)
44
+
45
+ def get_notes(self):
46
+ with Note() as note:
47
+ return note.get_notes()
@@ -0,0 +1,16 @@
1
+ """Database module """
2
+
3
+ from void.database.connection import DB_PATH, Connection
4
+
5
+
6
+ def initialize_database():
7
+ """Create void.db and its schema the first time the app runs.
8
+
9
+ Does nothing once void.db already exists, so it's safe to call on
10
+ every startup.
11
+ """
12
+ if DB_PATH.exists():
13
+ return
14
+
15
+ with Connection() as db:
16
+ db.create_tables()
@@ -0,0 +1,59 @@
1
+ import sqlite3
2
+ from pathlib import Path
3
+
4
+ DB_PATH = Path(__file__).parent / "void.db"
5
+
6
+
7
+ class Connection:
8
+ def __init__(self):
9
+ """Connect to void.db and configure results as name-accessible rows."""
10
+ self.db_path = DB_PATH
11
+ self.tables_script = Path(__file__).parent / "init_tables.sql"
12
+ self.test_data_script = Path(__file__).parent / "test_data.sql"
13
+
14
+ self.connection = sqlite3.connect(self.db_path)
15
+ self.connection.row_factory = sqlite3.Row
16
+ self.cursor = self.connection.cursor()
17
+
18
+ def __enter__(self):
19
+ """Allow `with Connection() as db:`; returns this instance."""
20
+ return self
21
+
22
+ def __exit__(self, exc_type, exc_value, traceback):
23
+ """Close the connection when the `with` block ends, even on error."""
24
+ self.close()
25
+
26
+ def create_tables(self):
27
+ """Run init_tables.sql to create the schema. Safe to call more than
28
+ once, every statement uses IF NOT EXISTS."""
29
+ with open(self.tables_script) as f:
30
+ sql = f.read()
31
+ self.cursor.executescript(sql)
32
+ self.connection.commit()
33
+
34
+ def load_test_data(self):
35
+ """Run test_data.sql to populate sample rows."""
36
+ with open(self.test_data_script) as f:
37
+ sql = f.read()
38
+ self.cursor.executescript(sql)
39
+ self.connection.commit()
40
+
41
+ def querying(self, sql, params=()):
42
+ """Run a parameterized SELECT and return the matching rows.
43
+
44
+ params fills any `?` placeholders in sql, keeping values out of the
45
+ SQL string itself (avoids SQL injection).
46
+ """
47
+ self.cursor.execute(sql, params)
48
+ return self.cursor.fetchall()
49
+
50
+ def execute(self, sql, params=()):
51
+ """Run a parameterized INSERT/UPDATE/DELETE, commit, and return the
52
+ id of the inserted row (lastrowid; meaningless for UPDATE/DELETE)."""
53
+ self.cursor.execute(sql, params)
54
+ self.connection.commit()
55
+ return self.cursor.lastrowid
56
+
57
+ def close(self):
58
+ """Close the database connection."""
59
+ self.connection.close()
@@ -0,0 +1,47 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ CREATE TABLE IF NOT EXISTS categories(
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ name TEXT NOT NULL,
6
+ is_active INTEGER DEFAULT 1,
7
+ date_created TEXT DEFAULT (datetime('now'))
8
+ );
9
+
10
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_categories_name
11
+ ON categories(name) WHERE is_active = 1;
12
+
13
+ CREATE TABLE IF NOT EXISTS void_note(
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ note_date TEXT NOT NULL,
16
+ is_active INTEGER DEFAULT 1,
17
+ date_created TEXT DEFAULT (datetime('now'))
18
+ );
19
+
20
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_void_note_note_date
21
+ ON void_note(note_date) WHERE is_active = 1;
22
+
23
+ CREATE TABLE IF NOT EXISTS activities(
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ name TEXT NOT NULL,
26
+ is_active INTEGER DEFAULT 1,
27
+ date_created TEXT DEFAULT (datetime('now')),
28
+ category_id INTEGER NOT NULL,
29
+ FOREIGN KEY (category_id)
30
+ REFERENCES categories (id)
31
+ );
32
+
33
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_activities_name_category
34
+ ON activities(name, category_id) WHERE is_active = 1;
35
+
36
+ CREATE TABLE IF NOT EXISTS void_note_details (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ notes TEXT,
39
+ is_active INTEGER DEFAULT 1,
40
+ date_created TEXT DEFAULT (datetime('now')),
41
+ void_note_id INTEGER NOT NULL,
42
+ activity_id INTEGER NOT NULL,
43
+ FOREIGN KEY (void_note_id)
44
+ REFERENCES void_note (id),
45
+ FOREIGN KEY (activity_id)
46
+ REFERENCES activities(id)
47
+ );
void/main.py ADDED
@@ -0,0 +1,153 @@
1
+ from textual import on
2
+ from textual.app import App, ComposeResult
3
+ from textual.widgets import Footer, Header, TabbedContent, TabPane
4
+
5
+ from void.database import initialize_database
6
+ from void.views.activity import ActivitiesView
7
+ from void.views.collection import CollectionView
8
+ from void.views.day_note import DayNote
9
+ from void.views.note import NoteView
10
+ from void.views.note_form import NoteForm
11
+ from void.views.welcome import WelcomeScreen
12
+
13
+
14
+ class VoidApp(App):
15
+
16
+ CSS = """
17
+ /* LAYOUT */
18
+ .header{
19
+ dock: top;
20
+ height: auto;
21
+ padding: 1 2 0 2;
22
+ border-bottom: solid $primary;
23
+ }
24
+ .main_container{
25
+ padding: 1 2;
26
+ }
27
+ .card_grid{
28
+ grid-columns: 1fr;
29
+ height: auto;
30
+ }
31
+
32
+ /* TITLES */
33
+ .module_title{
34
+ text-style: bold;
35
+ }
36
+ .section_title{
37
+ text-style: bold;
38
+ color: $text-muted;
39
+ }
40
+ #counter{
41
+ color: $text-muted;
42
+ }
43
+ .empty_state{
44
+ color: $text-muted;
45
+ padding: 1 0;
46
+ }
47
+
48
+ /* CARDS */
49
+ .note_card{
50
+ height: auto;
51
+ padding: 0 2;
52
+ border: round $primary;
53
+ }
54
+ .note_card Label{
55
+ width: 100%;
56
+ }
57
+
58
+ /* LOGGED ACTIVITY ENTRY */
59
+ .entry_card{
60
+ height: auto;
61
+ margin: 0 0 1 0;
62
+ padding: 0 0 0 2;
63
+ border-left: thick $primary;
64
+ }
65
+ .entry_card Label{
66
+ width: 100%;
67
+ }
68
+ .entry_title{
69
+ text-style: bold;
70
+ }
71
+ .entry_note{
72
+ color: $text-muted;
73
+ }
74
+ .entry_empty{
75
+ color: $text-muted;
76
+ text-style: italic;
77
+ }
78
+
79
+ /* CENTERED STATE SCREENS */
80
+ .state_screen{
81
+ height: 1fr;
82
+ align: center middle;
83
+ }
84
+ .state_card{
85
+ width: 60;
86
+ max-width: 100%;
87
+ height: auto;
88
+ padding: 1 2;
89
+ border: round $panel;
90
+ }
91
+ .state_card Label{
92
+ width: 100%;
93
+ text-align: center;
94
+ }
95
+ .state_card_success{
96
+ border: round $success;
97
+ }
98
+ .state_title{
99
+ text-style: bold;
100
+ }
101
+ .state_success{
102
+ color: $success;
103
+ }
104
+ .state_muted{
105
+ color: $text-muted;
106
+ }
107
+ .state_hint{
108
+ color: $text-muted;
109
+ padding: 0 0 1 0;
110
+ }
111
+ """
112
+
113
+ def compose(self) -> ComposeResult:
114
+ yield Header(icon="🧠")
115
+
116
+ with TabbedContent(initial="void_note"):
117
+
118
+ with TabPane("VOID NOTE", id="void_note"):
119
+ yield NoteView()
120
+
121
+ with TabPane("VOID DAY", id="void_day_note"):
122
+ yield DayNote()
123
+
124
+ with TabPane("VOID COLLECTION", id="void_collection"):
125
+ yield CollectionView()
126
+
127
+ with TabPane("VOID ACTIVITIES", id="void_activities"):
128
+ yield ActivitiesView()
129
+
130
+
131
+ yield Footer()
132
+
133
+ @on(NoteForm.Saved)
134
+ async def on_note_saved(self) -> None:
135
+ await self.query_one(DayNote).recompose()
136
+ await self.query_one(CollectionView).recompose()
137
+ self.query_one(TabbedContent).active = "void_day_note"
138
+
139
+ @on(ActivitiesView.Changed)
140
+ async def on_activities_changed(self) -> None:
141
+ await self.query_one(NoteView).recompose()
142
+
143
+ def on_mount(self) -> None:
144
+ self.title = "VOID"
145
+ self.sub_title = "Vital Offline Information Diary"
146
+ self.push_screen(WelcomeScreen())
147
+
148
+ def run() -> None:
149
+ initialize_database()
150
+ VoidApp().run()
151
+
152
+ if __name__ == "__main__":
153
+ run()
@@ -0,0 +1 @@
1
+ """Models module"""
@@ -0,0 +1,68 @@
1
+ from void.database.connection import Connection
2
+
3
+
4
+ class Activity:
5
+
6
+ def __enter__(self):
7
+ """Allow `with Activity() as activity:`; returns this instance."""
8
+ return self
9
+
10
+ def __exit__(self, exc_type, exc_value, traceback):
11
+ """Close the model when the `with` block ends, even on error."""
12
+
13
+ def get_active_activities(self):
14
+ with Connection() as db:
15
+ sql = """
16
+ SELECT
17
+ activities.id as "id",
18
+ activities.name as "activity",
19
+ categories.name as "category"
20
+ FROM activities
21
+ INNER JOIN
22
+ categories on categories.id = activities.category_id
23
+ WHERE activities.is_active == 1
24
+ AND
25
+ categories.is_active == 1;"""
26
+ result = db.querying(sql)
27
+ return result
28
+
29
+ def get_activities_by_category(self, category_id):
30
+ with Connection() as db:
31
+ sql = """
32
+ SELECT
33
+ activities.id as "id",
34
+ activities.name as "name"
35
+ FROM activities
36
+ WHERE
37
+ activities.category_id = ?
38
+ AND
39
+ activities.is_active = 1
40
+ """
41
+ result = db.querying(sql, (category_id,))
42
+ return result
43
+
44
+ def create_activity(self, activity, category_id):
45
+ with Connection() as db:
46
+ data = (activity, category_id)
47
+ sql = "INSERT INTO activities (name, category_id) VALUES(?, ?)"
48
+ db.execute(sql, data)
49
+
50
+ def update_activity(self, activity_id, activity, category_id):
51
+ with Connection() as db:
52
+ data = (activity, category_id, activity_id)
53
+ sql = """
54
+ UPDATE activities
55
+ SET name = ?, category_id = ?
56
+ WHERE id = ?
57
+ """
58
+ db.execute(sql, data)
59
+
60
+ def suspend_activity(self, activity_id):
61
+ with Connection() as db:
62
+ data = (activity_id,)
63
+ sql = """
64
+ UPDATE activities
65
+ SET is_active = 0
66
+ WHERE id = ?
67
+ """
68
+ db.execute(sql, data)
@@ -0,0 +1,40 @@
1
+ from void.database.connection import Connection
2
+
3
+
4
+ class Category:
5
+
6
+ def __enter__(self):
7
+ """Allow `with Category() as category:`; returns this instance."""
8
+ return self
9
+
10
+ def __exit__(self, exc_type, exc_value, traceback):
11
+ """Close the model when the `with` block ends, even on error."""
12
+
13
+ def get_active_categories_for_activities(self):
14
+ with Connection() as db:
15
+ sql = """
16
+ SELECT
17
+ categories.id as "id",
18
+ categories.name as "category"
19
+ FROM categories
20
+ WHERE categories.is_active = 1
21
+ """
22
+ result = db.querying(sql)
23
+ return result
24
+
25
+ def get_active_categories(self):
26
+ with Connection() as db:
27
+ sql = """
28
+ SELECT
29
+ categories.name as "category"
30
+ FROM categories
31
+ WHERE categories.is_active = 1
32
+ """
33
+ result = db.querying(sql)
34
+ return result
35
+
36
+ def create_category(self, category):
37
+ with Connection() as db:
38
+ data = (category,)
39
+ sql = "INSERT INTO categories (name) VALUES(?)"
40
+ db.execute(sql, data)
void/models/note.py ADDED
@@ -0,0 +1,64 @@
1
+
2
+ from void.database.connection import Connection
3
+
4
+
5
+ class Note:
6
+
7
+ def __enter__(self):
8
+ """Allow `with Note() as note:`; returns this instance."""
9
+ return self
10
+
11
+ def __exit__(self, exc_type, exc_value, traceback):
12
+ """Close the model when the `with` block ends, even on error."""
13
+
14
+ def save_note(self, date_str):
15
+ with Connection() as db:
16
+ data = (date_str,)
17
+ sql = """
18
+ INSERT INTO
19
+ void_note(note_date)
20
+ VALUES(?)
21
+ """
22
+ return db.execute(sql, data)
23
+
24
+ def save_note_detail(self, note_id, activity_info, activity_note):
25
+ activity_id, _ = activity_info.split("::")
26
+ with Connection() as db:
27
+ data = (activity_note, note_id, activity_id)
28
+ sql = """
29
+ INSERT INTO
30
+ void_note_details(notes, void_note_id, activity_id)
31
+ VALUES(?, ?, ?)
32
+ """
33
+ return db.execute(sql, data)
34
+
35
+ NOTE_SQL = """
36
+ SELECT
37
+ void_note.id as "note_id",
38
+ void_note.note_date as "note_date",
39
+ void_note_details.id as "detail_id",
40
+ activities.id as "activity_id",
41
+ activities.name as "activity",
42
+ categories.name as "category",
43
+ void_note_details.notes as "notes"
44
+ FROM void_note
45
+ INNER JOIN
46
+ void_note_details
47
+ ON void_note_details.void_note_id = void_note.id
48
+ INNER JOIN
49
+ activities ON activities.id = void_note_details.activity_id
50
+ INNER JOIN
51
+ categories ON categories.id = activities.category_id
52
+ WHERE void_note.is_active = 1
53
+ AND void_note_details.is_active = 1
54
+ """
55
+
56
+ def get_day_note(self, date_str):
57
+ with Connection() as db:
58
+ sql = self.NOTE_SQL + " AND void_note.note_date = ? ORDER BY categories.name, activities.name"
59
+ return db.querying(sql, (date_str,))
60
+
61
+ def get_notes(self):
62
+ """Every saved note detail, newest day first."""
63
+ with Connection() as db:
64
+ return db.querying(self.NOTE_SQL + " ORDER BY void_note.note_date DESC, void_note_details.id")
void/views/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """ APP VIEWS"""