voidcli 0.1.0__tar.gz

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.
@@ -0,0 +1,4 @@
1
+ __pycache__
2
+ .venv
3
+ myvenv/
4
+ dist/
voidcli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Nelson Urrutia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
voidcli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.5
2
+ Name: voidcli
3
+ Version: 0.1.0
4
+ Project-URL: Repository, https://github.com/NelsonUrrutia/void
5
+ Author-email: Nelson Urrutia <nelurrutia15@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) Nelson Urrutia
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: textual
30
+ Requires-Dist: textual-dev
31
+ Description-Content-Type: text/markdown
32
+
33
+ ```
34
+ █████ █████ ███████ █████ ██████████
35
+ ░░███ ░░███ ███░░░░░███ ░░███ ░░███░░░░███
36
+ ░███ ░███ ███ ░░███ ░███ ░███ ░░███
37
+ ░███ ░███ ░███ ░███ ░███ ░███ ░███
38
+ ░░███ ███ ░███ ░███ ░███ ░███ ░███
39
+ ░░░█████░ ░░███ ███ ░███ ░███ ███
40
+ ░░███ ░░░███████░ █████ ██████████
41
+ ░░░ ░░░░░░░ ░░░░░ ░░░░░░░░░░
42
+ ```
43
+
44
+ # **V**ital **O**ffline **I**nformation **D**iary
45
+
46
+ ---
47
+
48
+ > **VOID** is an offline diary to be used in the terminal.
49
+
50
+ I made VOID with these goals in mind:
51
+
52
+ 1. Make something useful for me, and feel accomplished delivering a usable piece of
53
+ software - at least for myself.
54
+ 2. Learn Python and SQLite. I wanted to do something different from my regular job and
55
+ grow my skills - starting with tooling, and now this more UI-heavy piece backed by
56
+ a database.
57
+ 3. Practice and get involved in the full life cycle of a project: identify a need or
58
+ something to improve, then design and develop a solution.
59
+
60
+ It took me a year to complete this project - this is the third and final repo - I pitched the idea on August 10th, 2025.
61
+ Life, doubts, and failures get in the way of personal projects. However, looking back
62
+ at my notes, I learned a lot, and a lot of myself changed along the way.
63
+
64
+ The project is in its final form, and I can proudly say it's finished.
65
+ I'll keep improving it, but the core things I wanted to do are delivered.
66
+
67
+ I made it public so there's proof on the Internet of my work.
68
+
69
+ ---
70
+
71
+ ### The pitch
72
+
73
+ > Track your life, thoughts, and growth without distractions. VOID is a focused personal logging tool built for the terminal. No cloud sync, no notifications, no feature bloat. Just you, your thoughts, and a clean interface to document what truly matters in your life.
74
+
75
+ ### How to install
76
+
77
+ The project is available on PyPI, so you can install it with `pipx install voidcli`.
@@ -0,0 +1,45 @@
1
+ ```
2
+ █████ █████ ███████ █████ ██████████
3
+ ░░███ ░░███ ███░░░░░███ ░░███ ░░███░░░░███
4
+ ░███ ░███ ███ ░░███ ░███ ░███ ░░███
5
+ ░███ ░███ ░███ ░███ ░███ ░███ ░███
6
+ ░░███ ███ ░███ ░███ ░███ ░███ ░███
7
+ ░░░█████░ ░░███ ███ ░███ ░███ ███
8
+ ░░███ ░░░███████░ █████ ██████████
9
+ ░░░ ░░░░░░░ ░░░░░ ░░░░░░░░░░
10
+ ```
11
+
12
+ # **V**ital **O**ffline **I**nformation **D**iary
13
+
14
+ ---
15
+
16
+ > **VOID** is an offline diary to be used in the terminal.
17
+
18
+ I made VOID with these goals in mind:
19
+
20
+ 1. Make something useful for me, and feel accomplished delivering a usable piece of
21
+ software - at least for myself.
22
+ 2. Learn Python and SQLite. I wanted to do something different from my regular job and
23
+ grow my skills - starting with tooling, and now this more UI-heavy piece backed by
24
+ a database.
25
+ 3. Practice and get involved in the full life cycle of a project: identify a need or
26
+ something to improve, then design and develop a solution.
27
+
28
+ It took me a year to complete this project - this is the third and final repo - I pitched the idea on August 10th, 2025.
29
+ Life, doubts, and failures get in the way of personal projects. However, looking back
30
+ at my notes, I learned a lot, and a lot of myself changed along the way.
31
+
32
+ The project is in its final form, and I can proudly say it's finished.
33
+ I'll keep improving it, but the core things I wanted to do are delivered.
34
+
35
+ I made it public so there's proof on the Internet of my work.
36
+
37
+ ---
38
+
39
+ ### The pitch
40
+
41
+ > Track your life, thoughts, and growth without distractions. VOID is a focused personal logging tool built for the terminal. No cloud sync, no notifications, no feature bloat. Just you, your thoughts, and a clean interface to document what truly matters in your life.
42
+
43
+ ### How to install
44
+
45
+ The project is available on PyPI, so you can install it with `pipx install voidcli`.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "voidcli"
7
+ version = "0.1.0"
8
+ description = ""
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.10"
12
+ dependencies = ["textual", "textual-dev"]
13
+ authors = [{ name = "Nelson Urrutia", email = "nelurrutia15@gmail.com" }]
14
+
15
+ [project.urls]
16
+ Repository = "https://github.com/NelsonUrrutia/void"
17
+
18
+ [project.scripts]
19
+ void = "void.main:run"
20
+
21
+ [tool.hatch.build.targets.sdist]
22
+ include = ["/void", "/LICENSE", "/README.md", "/pyproject.toml"]
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["void"]
26
+
27
+ [tool.basedpyright]
28
+ typeCheckingMode = "basic"
@@ -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
+ );
@@ -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)