sqliter-py 0.1.0__py3-none-any.whl → 0.1.1__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.
Potentially problematic release.
This version of sqliter-py might be problematic. Click here for more details.
- sqliter/exceptions.py +114 -0
- sqliter/query/query.py +65 -21
- sqliter/sqliter.py +81 -37
- sqliter_py-0.1.1.dist-info/METADATA +204 -0
- sqliter_py-0.1.1.dist-info/RECORD +10 -0
- sqliter_py-0.1.0.dist-info/METADATA +0 -30
- sqliter_py-0.1.0.dist-info/RECORD +0 -9
- {sqliter_py-0.1.0.dist-info → sqliter_py-0.1.1.dist-info}/WHEEL +0 -0
sqliter/exceptions.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Define custom exceptions for the sqliter package."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SqliterError(Exception):
|
|
9
|
+
"""Base class for all exceptions raised by the sqliter package."""
|
|
10
|
+
|
|
11
|
+
message_template: str = "An error occurred in the SQLiter package."
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args: object) -> None:
|
|
14
|
+
"""Format the message using the provided arguments.
|
|
15
|
+
|
|
16
|
+
We also capture (and display) the current exception context and chain
|
|
17
|
+
any previous exceptions.
|
|
18
|
+
|
|
19
|
+
:param args: Arguments to format into the message template
|
|
20
|
+
"""
|
|
21
|
+
if args:
|
|
22
|
+
message = self.message_template.format(*args)
|
|
23
|
+
else:
|
|
24
|
+
message = (
|
|
25
|
+
self.message_template.replace("'{}'", "")
|
|
26
|
+
.replace(":", "")
|
|
27
|
+
.strip()
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Capture the current exception context
|
|
31
|
+
self.original_exception = sys.exc_info()[1]
|
|
32
|
+
|
|
33
|
+
# If there's an active exception, append its information to our message
|
|
34
|
+
if self.original_exception:
|
|
35
|
+
original_type = type(self.original_exception).__name__
|
|
36
|
+
original_module = type(self.original_exception).__module__
|
|
37
|
+
|
|
38
|
+
# Get the traceback of the original exception
|
|
39
|
+
tb = traceback.extract_tb(self.original_exception.__traceback__)
|
|
40
|
+
if tb:
|
|
41
|
+
last_frame = tb[-1]
|
|
42
|
+
file_path = os.path.relpath(last_frame.filename)
|
|
43
|
+
line_number = last_frame.lineno
|
|
44
|
+
location = f"{file_path}:{line_number}"
|
|
45
|
+
else:
|
|
46
|
+
location = "unknown location"
|
|
47
|
+
|
|
48
|
+
message += (
|
|
49
|
+
f"\n --> {original_module}.{original_type} "
|
|
50
|
+
f"from {location}: {self.original_exception}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Call the parent constructor with our formatted message
|
|
54
|
+
super().__init__(message)
|
|
55
|
+
|
|
56
|
+
# Explicitly chain exceptions if there's an active one
|
|
57
|
+
if self.original_exception:
|
|
58
|
+
self.__cause__ = self.original_exception
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DatabaseConnectionError(SqliterError):
|
|
62
|
+
"""Raised when the SQLite database connection fails."""
|
|
63
|
+
|
|
64
|
+
message_template = "Failed to connect to the database: '{}'"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class InvalidOffsetError(SqliterError):
|
|
68
|
+
"""Raised when an invalid offset value (0 or negative) is used."""
|
|
69
|
+
|
|
70
|
+
message_template = (
|
|
71
|
+
"Invalid offset value: '{}'. Offset must be a positive integer."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TableCreationError(SqliterError):
|
|
76
|
+
"""Raised when a table cannot be created in the database."""
|
|
77
|
+
|
|
78
|
+
message_template = "Failed to create the table: '{}'"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RecordInsertionError(SqliterError):
|
|
82
|
+
"""Raised when an error occurs during record insertion."""
|
|
83
|
+
|
|
84
|
+
message_template = "Failed to insert record into table: '{}'"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RecordUpdateError(SqliterError):
|
|
88
|
+
"""Raised when an error occurs during record update."""
|
|
89
|
+
|
|
90
|
+
message_template = "Failed to update record in table: '{}'"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RecordNotFoundError(SqliterError):
|
|
94
|
+
"""Raised when a record with the specified primary key is not found."""
|
|
95
|
+
|
|
96
|
+
message_template = "Failed to find a record for key '{}' "
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RecordFetchError(SqliterError):
|
|
100
|
+
"""Raised when an error occurs during record fetching."""
|
|
101
|
+
|
|
102
|
+
message_template = "Failed to fetch record from table: '{}'"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class RecordDeletionError(SqliterError):
|
|
106
|
+
"""Raised when an error occurs during record deletion."""
|
|
107
|
+
|
|
108
|
+
message_template = "Failed to delete record from table: '{}'"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class InvalidFilterError(SqliterError):
|
|
112
|
+
"""Raised when an invalid filter field is used in a query."""
|
|
113
|
+
|
|
114
|
+
message_template = "Failed to apply filter: invalid field '{}'"
|
sqliter/query/query.py
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import sqlite3
|
|
5
6
|
from typing import TYPE_CHECKING, Any, Optional
|
|
6
7
|
|
|
7
8
|
from typing_extensions import Self
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
from sqliter.exceptions import (
|
|
11
|
+
InvalidFilterError,
|
|
12
|
+
InvalidOffsetError,
|
|
13
|
+
RecordFetchError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
10
17
|
from sqliter import SqliterDB
|
|
11
18
|
from sqliter.model import BaseDBModel
|
|
12
19
|
|
|
@@ -20,52 +27,87 @@ class QueryBuilder:
|
|
|
20
27
|
self.model_class = model_class
|
|
21
28
|
self.table_name = model_class.get_table_name() # Use model_class method
|
|
22
29
|
self.filters: list[tuple[str, Any]] = []
|
|
30
|
+
self._limit: Optional[int] = None
|
|
31
|
+
self._offset: Optional[int] = None
|
|
32
|
+
self._order_by: Optional[str] = None
|
|
23
33
|
|
|
24
34
|
def filter(self, **conditions: str | float | None) -> Self:
|
|
25
35
|
"""Add filter conditions to the query."""
|
|
36
|
+
valid_fields = self.model_class.model_fields
|
|
37
|
+
|
|
26
38
|
for field, value in conditions.items():
|
|
39
|
+
if field not in valid_fields:
|
|
40
|
+
raise InvalidFilterError(field)
|
|
27
41
|
self.filters.append((field, value))
|
|
42
|
+
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def limit(self, limit_value: int) -> Self:
|
|
46
|
+
"""Limit the number of results returned by the query."""
|
|
47
|
+
self._limit = limit_value
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def offset(self, offset_value: int) -> Self:
|
|
51
|
+
"""Set an offset value for the query."""
|
|
52
|
+
if offset_value <= 0:
|
|
53
|
+
raise InvalidOffsetError(offset_value)
|
|
54
|
+
self._offset = offset_value
|
|
55
|
+
|
|
56
|
+
if self._limit is None:
|
|
57
|
+
self._limit = -1
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def order(self, order_by_field: str) -> Self:
|
|
61
|
+
"""Order the results by a specific field and optionally direction."""
|
|
62
|
+
self._order_by = order_by_field
|
|
28
63
|
return self
|
|
29
64
|
|
|
30
65
|
def _execute_query(
|
|
31
66
|
self,
|
|
32
|
-
limit: Optional[int] = None,
|
|
33
|
-
offset: Optional[int] = None,
|
|
34
|
-
order_by: Optional[str] = None,
|
|
35
67
|
*,
|
|
36
68
|
fetch_one: bool = False,
|
|
37
69
|
) -> list[tuple[Any, ...]] | Optional[tuple[Any, ...]]:
|
|
38
70
|
"""Helper function to execute the query with filters."""
|
|
39
71
|
fields = ", ".join(self.model_class.model_fields)
|
|
72
|
+
|
|
73
|
+
# Build the WHERE clause with special handling for None (NULL in SQL)
|
|
40
74
|
where_clause = " AND ".join(
|
|
41
|
-
[
|
|
75
|
+
[
|
|
76
|
+
f"{field} IS NULL" if value is None else f"{field} = ?"
|
|
77
|
+
for field, value in self.filters
|
|
78
|
+
]
|
|
42
79
|
)
|
|
80
|
+
|
|
43
81
|
sql = f"SELECT {fields} FROM {self.table_name}" # noqa: S608
|
|
44
82
|
|
|
45
83
|
if self.filters:
|
|
46
84
|
sql += f" WHERE {where_clause}"
|
|
47
85
|
|
|
48
|
-
if
|
|
49
|
-
sql += f" ORDER BY {
|
|
86
|
+
if self._order_by:
|
|
87
|
+
sql += f" ORDER BY {self._order_by}"
|
|
50
88
|
|
|
51
|
-
if
|
|
52
|
-
sql += f" LIMIT {
|
|
89
|
+
if self._limit is not None:
|
|
90
|
+
sql += f" LIMIT {self._limit}"
|
|
53
91
|
|
|
54
|
-
if
|
|
55
|
-
sql += f" OFFSET {
|
|
92
|
+
if self._offset is not None:
|
|
93
|
+
sql += f" OFFSET {self._offset}"
|
|
56
94
|
|
|
57
|
-
|
|
95
|
+
# Only include non-None values in the values list
|
|
96
|
+
values = [value for _, value in self.filters if value is not None]
|
|
58
97
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
98
|
+
try:
|
|
99
|
+
with self.db.connect() as conn:
|
|
100
|
+
cursor = conn.cursor()
|
|
101
|
+
cursor.execute(sql, values)
|
|
102
|
+
return cursor.fetchall() if not fetch_one else cursor.fetchone()
|
|
103
|
+
except sqlite3.Error as exc:
|
|
104
|
+
raise RecordFetchError(self.table_name) from exc
|
|
63
105
|
|
|
64
106
|
def fetch_all(self) -> list[BaseDBModel]:
|
|
65
107
|
"""Fetch all results matching the filters."""
|
|
66
108
|
results = self._execute_query()
|
|
67
109
|
|
|
68
|
-
if results
|
|
110
|
+
if not results:
|
|
69
111
|
return []
|
|
70
112
|
|
|
71
113
|
return [
|
|
@@ -92,7 +134,8 @@ class QueryBuilder:
|
|
|
92
134
|
|
|
93
135
|
def fetch_first(self) -> BaseDBModel | None:
|
|
94
136
|
"""Fetch the first result of the query."""
|
|
95
|
-
|
|
137
|
+
self._limit = 1
|
|
138
|
+
result = self._execute_query()
|
|
96
139
|
if not result:
|
|
97
140
|
return None
|
|
98
141
|
return self.model_class(
|
|
@@ -103,9 +146,10 @@ class QueryBuilder:
|
|
|
103
146
|
)
|
|
104
147
|
|
|
105
148
|
def fetch_last(self) -> BaseDBModel | None:
|
|
106
|
-
"""Fetch the last result of the query (based on the
|
|
107
|
-
|
|
108
|
-
|
|
149
|
+
"""Fetch the last result of the query (based on the insertion order)."""
|
|
150
|
+
self._limit = 1
|
|
151
|
+
self._order_by = "rowid DESC"
|
|
152
|
+
result = self._execute_query()
|
|
109
153
|
if not result:
|
|
110
154
|
return None
|
|
111
155
|
return self.model_class(
|
sqliter/sqliter.py
CHANGED
|
@@ -7,9 +7,18 @@ from typing import TYPE_CHECKING, Optional
|
|
|
7
7
|
|
|
8
8
|
from typing_extensions import Self
|
|
9
9
|
|
|
10
|
+
from sqliter.exceptions import (
|
|
11
|
+
DatabaseConnectionError,
|
|
12
|
+
RecordDeletionError,
|
|
13
|
+
RecordFetchError,
|
|
14
|
+
RecordInsertionError,
|
|
15
|
+
RecordNotFoundError,
|
|
16
|
+
RecordUpdateError,
|
|
17
|
+
TableCreationError,
|
|
18
|
+
)
|
|
10
19
|
from sqliter.query.query import QueryBuilder
|
|
11
20
|
|
|
12
|
-
if TYPE_CHECKING:
|
|
21
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
13
22
|
from types import TracebackType
|
|
14
23
|
|
|
15
24
|
from sqliter.model.model import BaseDBModel
|
|
@@ -27,7 +36,10 @@ class SqliterDB:
|
|
|
27
36
|
def connect(self) -> sqlite3.Connection:
|
|
28
37
|
"""Create or return a connection to the SQLite database."""
|
|
29
38
|
if not self.conn:
|
|
30
|
-
|
|
39
|
+
try:
|
|
40
|
+
self.conn = sqlite3.connect(self.db_filename)
|
|
41
|
+
except sqlite3.Error as exc:
|
|
42
|
+
raise DatabaseConnectionError(self.db_filename) from exc
|
|
31
43
|
return self.conn
|
|
32
44
|
|
|
33
45
|
def create_table(self, model_class: type[BaseDBModel]) -> None:
|
|
@@ -43,7 +55,7 @@ class SqliterDB:
|
|
|
43
55
|
if create_id:
|
|
44
56
|
create_table_sql = f"""
|
|
45
57
|
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
46
|
-
|
|
58
|
+
{primary_key} INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
59
|
{fields}
|
|
48
60
|
)
|
|
49
61
|
"""
|
|
@@ -55,10 +67,13 @@ class SqliterDB:
|
|
|
55
67
|
)
|
|
56
68
|
"""
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
try:
|
|
71
|
+
with self.connect() as conn:
|
|
72
|
+
cursor = conn.cursor()
|
|
73
|
+
cursor.execute(create_table_sql)
|
|
74
|
+
conn.commit()
|
|
75
|
+
except sqlite3.Error as exc:
|
|
76
|
+
raise TableCreationError(table_name) from exc
|
|
62
77
|
|
|
63
78
|
def _maybe_commit(self, conn: sqlite3.Connection) -> None:
|
|
64
79
|
"""Commit changes if auto_commit is True."""
|
|
@@ -77,14 +92,17 @@ class SqliterDB:
|
|
|
77
92
|
)
|
|
78
93
|
|
|
79
94
|
insert_sql = f"""
|
|
80
|
-
INSERT
|
|
95
|
+
INSERT INTO {table_name} ({fields})
|
|
81
96
|
VALUES ({placeholders})
|
|
82
97
|
""" # noqa: S608
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
try:
|
|
100
|
+
with self.connect() as conn:
|
|
101
|
+
cursor = conn.cursor()
|
|
102
|
+
cursor.execute(insert_sql, values)
|
|
103
|
+
self._maybe_commit(conn)
|
|
104
|
+
except sqlite3.Error as exc:
|
|
105
|
+
raise RecordInsertionError(table_name) from exc
|
|
88
106
|
|
|
89
107
|
def get(
|
|
90
108
|
self, model_class: type[BaseDBModel], primary_key_value: str
|
|
@@ -99,18 +117,22 @@ class SqliterDB:
|
|
|
99
117
|
SELECT {fields} FROM {table_name} WHERE {primary_key} = ?
|
|
100
118
|
""" # noqa: S608
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
try:
|
|
121
|
+
with self.connect() as conn:
|
|
122
|
+
cursor = conn.cursor()
|
|
123
|
+
cursor.execute(select_sql, (primary_key_value,))
|
|
124
|
+
result = cursor.fetchone()
|
|
125
|
+
|
|
126
|
+
if result:
|
|
127
|
+
result_dict = {
|
|
128
|
+
field: result[idx]
|
|
129
|
+
for idx, field in enumerate(model_class.model_fields)
|
|
130
|
+
}
|
|
131
|
+
return model_class(**result_dict)
|
|
132
|
+
except sqlite3.Error as exc:
|
|
133
|
+
raise RecordFetchError(table_name) from exc
|
|
134
|
+
else:
|
|
135
|
+
return None
|
|
114
136
|
|
|
115
137
|
def update(self, model_instance: BaseDBModel) -> None:
|
|
116
138
|
"""Update an existing record using the Pydantic model."""
|
|
@@ -131,13 +153,24 @@ class SqliterDB:
|
|
|
131
153
|
primary_key_value = getattr(model_instance, primary_key)
|
|
132
154
|
|
|
133
155
|
update_sql = f"""
|
|
134
|
-
UPDATE {table_name}
|
|
156
|
+
UPDATE {table_name}
|
|
157
|
+
SET {fields}
|
|
158
|
+
WHERE {primary_key} = ?
|
|
135
159
|
""" # noqa: S608
|
|
136
160
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
try:
|
|
162
|
+
with self.connect() as conn:
|
|
163
|
+
cursor = conn.cursor()
|
|
164
|
+
cursor.execute(update_sql, (*values, primary_key_value))
|
|
165
|
+
|
|
166
|
+
# Check if any rows were updated
|
|
167
|
+
if cursor.rowcount == 0:
|
|
168
|
+
raise RecordNotFoundError(primary_key_value)
|
|
169
|
+
|
|
170
|
+
self._maybe_commit(conn)
|
|
171
|
+
|
|
172
|
+
except sqlite3.Error as exc:
|
|
173
|
+
raise RecordUpdateError(table_name) from exc
|
|
141
174
|
|
|
142
175
|
def delete(
|
|
143
176
|
self, model_class: type[BaseDBModel], primary_key_value: str
|
|
@@ -150,10 +183,16 @@ class SqliterDB:
|
|
|
150
183
|
DELETE FROM {table_name} WHERE {primary_key} = ?
|
|
151
184
|
""" # noqa: S608
|
|
152
185
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
186
|
+
try:
|
|
187
|
+
with self.connect() as conn:
|
|
188
|
+
cursor = conn.cursor()
|
|
189
|
+
cursor.execute(delete_sql, (primary_key_value,))
|
|
190
|
+
|
|
191
|
+
if cursor.rowcount == 0:
|
|
192
|
+
raise RecordNotFoundError(primary_key_value)
|
|
193
|
+
self._maybe_commit(conn)
|
|
194
|
+
except sqlite3.Error as exc:
|
|
195
|
+
raise RecordDeletionError(table_name) from exc
|
|
157
196
|
|
|
158
197
|
def select(self, model_class: type[BaseDBModel]) -> QueryBuilder:
|
|
159
198
|
"""Start a query for the given model."""
|
|
@@ -173,7 +212,12 @@ class SqliterDB:
|
|
|
173
212
|
) -> None:
|
|
174
213
|
"""Exit the runtime context and close the connection."""
|
|
175
214
|
if self.conn:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
215
|
+
try:
|
|
216
|
+
if exc_type:
|
|
217
|
+
# Roll back the transaction if there was an exception
|
|
218
|
+
self.conn.rollback()
|
|
219
|
+
self._maybe_commit(self.conn)
|
|
220
|
+
finally:
|
|
221
|
+
# Close the connection and reset the instance variable
|
|
222
|
+
self.conn.close()
|
|
223
|
+
self.conn = None
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sqliter-py
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Interact with SQLite databases using Python and Pydantic
|
|
5
|
+
Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/seapagan/sqliter-py
|
|
8
|
+
Author-email: Grant Ramsay <grant@gnramsay.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: pydantic>=2.9.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# SQLiter
|
|
26
|
+
|
|
27
|
+
<!--  -->
|
|
28
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/testing.yml)
|
|
29
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/linting.yml)
|
|
30
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/mypy.yml)
|
|
31
|
+
<!--  -->
|
|
32
|
+
|
|
33
|
+
SQLiter is a lightweight Object-Relational Mapping (ORM) library for SQLite
|
|
34
|
+
databases in Python. It provides a simplified interface for interacting with
|
|
35
|
+
SQLite databases using Pydantic models.
|
|
36
|
+
|
|
37
|
+
It does not aim to be a full-fledged ORM like SQLAlchemy, but rather a simple
|
|
38
|
+
and easy-to-use library for basic database operations, especially for small
|
|
39
|
+
projects. It is NOT asynchronous and does not support complex queries (at this
|
|
40
|
+
time).
|
|
41
|
+
|
|
42
|
+
The ideal use case is more for Python CLI tools that need to store data in a
|
|
43
|
+
database-like format without needing to learn SQL or use a full ORM.
|
|
44
|
+
|
|
45
|
+
> [!NOTE]
|
|
46
|
+
> This project is still in the early stages of development and is lacking some
|
|
47
|
+
> planned functionality. Please use with caution.
|
|
48
|
+
>
|
|
49
|
+
> See the [TODO](TODO.md) for planned features and improvements.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- Table creation based on Pydantic models
|
|
54
|
+
- CRUD operations (Create, Read, Update, Delete)
|
|
55
|
+
- Basic query building with filtering, ordering, and pagination
|
|
56
|
+
- Transaction support
|
|
57
|
+
- Custom exceptions for better error handling
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
You can install SQLiter using pip:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install sqliter-py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
Here's a quick example of how to use SQLiter:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from sqliter import SqliterDB
|
|
73
|
+
from sqliter.model import BaseDBModel
|
|
74
|
+
|
|
75
|
+
# Define your model
|
|
76
|
+
class User(BaseDBModel):
|
|
77
|
+
name: str
|
|
78
|
+
age: int
|
|
79
|
+
|
|
80
|
+
class Meta:
|
|
81
|
+
table_name = "users"
|
|
82
|
+
|
|
83
|
+
# Create a database connection
|
|
84
|
+
db = SqliterDB("example.db", auto_commit=True)
|
|
85
|
+
|
|
86
|
+
# Create the table
|
|
87
|
+
db.create_table(User)
|
|
88
|
+
|
|
89
|
+
# Insert a record
|
|
90
|
+
user = User(name="John Doe", age=30)
|
|
91
|
+
db.insert(user)
|
|
92
|
+
|
|
93
|
+
# Query records
|
|
94
|
+
results = db.select(User).filter(name="John Doe").fetch_all()
|
|
95
|
+
for user in results:
|
|
96
|
+
print(f"User: {user.name}, Age: {user.age}")
|
|
97
|
+
|
|
98
|
+
# Update a record
|
|
99
|
+
user.age = 31
|
|
100
|
+
db.update(user)
|
|
101
|
+
|
|
102
|
+
# Delete a record
|
|
103
|
+
db.delete(User, "John Doe")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Detailed Usage
|
|
107
|
+
|
|
108
|
+
### Defining Models
|
|
109
|
+
|
|
110
|
+
Models in SQLiter are based on Pydantic's `BaseModel`. You can define your
|
|
111
|
+
models like this:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from sqliter.model import BaseDBModel
|
|
115
|
+
|
|
116
|
+
class User(BaseDBModel):
|
|
117
|
+
name: str
|
|
118
|
+
age: int
|
|
119
|
+
email: str
|
|
120
|
+
|
|
121
|
+
class Meta:
|
|
122
|
+
table_name = "users"
|
|
123
|
+
primary_key = "name" # Default is "id"
|
|
124
|
+
create_id = False # Set to True to auto-create an ID field
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Database Operations
|
|
128
|
+
|
|
129
|
+
#### Creating a Connection
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from sqliter import SqliterDB
|
|
133
|
+
|
|
134
|
+
db = SqliterDB("your_database.db", auto_commit=True)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Creating Tables
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
db.create_table(User)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Inserting Records
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
user = User(name="Jane Doe", age=25, email="jane@example.com")
|
|
147
|
+
db.insert(user)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### Querying Records
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# Fetch all users
|
|
154
|
+
all_users = db.select(User).fetch_all()
|
|
155
|
+
|
|
156
|
+
# Filter users
|
|
157
|
+
young_users = db.select(User).filter(age=25).fetch_all()
|
|
158
|
+
|
|
159
|
+
# Order users
|
|
160
|
+
ordered_users = db.select(User).order("age DESC").fetch_all()
|
|
161
|
+
|
|
162
|
+
# Limit and offset
|
|
163
|
+
paginated_users = db.select(User).limit(10).offset(20).fetch_all()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Note: The filtering in SQLiter is basic and supports exact matches. Complex
|
|
167
|
+
queries like `age__lt` are not supported in the current implementation.
|
|
168
|
+
|
|
169
|
+
#### Updating Records
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
user.age = 26
|
|
173
|
+
db.update(user)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Deleting Records
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
db.delete(User, "Jane Doe")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Transactions
|
|
183
|
+
|
|
184
|
+
SQLiter supports transactions using Python's context manager:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
with db:
|
|
188
|
+
db.insert(User(name="Alice", age=30, email="alice@example.com"))
|
|
189
|
+
db.insert(User(name="Bob", age=35, email="bob@example.com"))
|
|
190
|
+
# If an exception occurs, the transaction will be rolled back
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Contributing
|
|
194
|
+
|
|
195
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
This project is licensed under the MIT License.
|
|
200
|
+
|
|
201
|
+
## Acknowledgements
|
|
202
|
+
|
|
203
|
+
SQLiter was initially developed as an experiment using ChatGPT, with subsequent
|
|
204
|
+
manual refinements and improvements.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sqliter/__init__.py,sha256=L8R0uvCbbbACwaI5xtd3khtvpNhlPRgHJAaYZvqjzig,134
|
|
2
|
+
sqliter/exceptions.py,sha256=5NO9DwHMVKrDZP908G63J5-ANBJDCwbBkXL7h_Bit2Q,3610
|
|
3
|
+
sqliter/sqliter.py,sha256=R69oNusPEg0Q_zRPFetS9VUUCT5pjatXnUr3Tv6yzjE,7494
|
|
4
|
+
sqliter/model/__init__.py,sha256=Ovpkbyx2-T6Oee0qFNgUBBc2M0uwK-cdG0pigG3mkd8,179
|
|
5
|
+
sqliter/model/model.py,sha256=t1w38om37gma1gRk01Z_9II0h4g-l734ijN_8M1SYoY,1247
|
|
6
|
+
sqliter/query/__init__.py,sha256=BluNMJpuoo2PsYN-bL7fXlEc02O_8LgOMsvCmyv04ao,125
|
|
7
|
+
sqliter/query/query.py,sha256=BRKCdMBOlbcIU5CUxRyKBF4aFT2AboUPu7UwbJ1s5hs,5793
|
|
8
|
+
sqliter_py-0.1.1.dist-info/METADATA,sha256=ehe8KZXc08ftvn2i6FGiw3Qe8jK6QsqApDoDQm9p0XE,5477
|
|
9
|
+
sqliter_py-0.1.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
10
|
+
sqliter_py-0.1.1.dist-info/RECORD,,
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: sqliter-py
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Interact with SQLite databases using Python and Pydantic
|
|
5
|
-
Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
|
|
6
|
-
Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
|
|
7
|
-
Project-URL: Repository, https://github.com/seapagan/sqliter-py
|
|
8
|
-
Author-email: Grant Ramsay <grant@gnramsay.com>
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Topic :: Software Development
|
|
20
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
Requires-Python: >=3.9
|
|
22
|
-
Requires-Dist: pydantic>=2.9.0
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
|
|
25
|
-
# SQLiter
|
|
26
|
-
|
|
27
|
-
An SQLite wrapper in Python using Pydantic and written primarily using ChatGPT,
|
|
28
|
-
as an experiment in how viable it is to write working code using a LLM.
|
|
29
|
-
|
|
30
|
-
The code was then cleaned up, typed and linted by hand.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
sqliter/__init__.py,sha256=L8R0uvCbbbACwaI5xtd3khtvpNhlPRgHJAaYZvqjzig,134
|
|
2
|
-
sqliter/sqliter.py,sha256=FxWtYnjnfM2Tp1lc1b1AfrGia-G0IdSopZ2bSa-lPuo,5944
|
|
3
|
-
sqliter/model/__init__.py,sha256=Ovpkbyx2-T6Oee0qFNgUBBc2M0uwK-cdG0pigG3mkd8,179
|
|
4
|
-
sqliter/model/model.py,sha256=t1w38om37gma1gRk01Z_9II0h4g-l734ijN_8M1SYoY,1247
|
|
5
|
-
sqliter/query/__init__.py,sha256=BluNMJpuoo2PsYN-bL7fXlEc02O_8LgOMsvCmyv04ao,125
|
|
6
|
-
sqliter/query/query.py,sha256=8wV5GJwQVFesnW60Qe9XCiOwEZI3Wvk-8WNM0ANsT_M,4442
|
|
7
|
-
sqliter_py-0.1.0.dist-info/METADATA,sha256=R_IogiSpgnGC54Mftva2w5EIa-JnGH-R8NuE5_LDhRo,1267
|
|
8
|
-
sqliter_py-0.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
9
|
-
sqliter_py-0.1.0.dist-info/RECORD,,
|
|
File without changes
|