sqliter-py 0.1.1__py3-none-any.whl → 0.2.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.
- sqliter/constants.py +20 -0
- sqliter/exceptions.py +6 -0
- sqliter/query/query.py +191 -38
- sqliter/sqliter.py +21 -8
- sqliter_py-0.2.0.dist-info/METADATA +351 -0
- sqliter_py-0.2.0.dist-info/RECORD +11 -0
- sqliter_py-0.1.1.dist-info/METADATA +0 -204
- sqliter_py-0.1.1.dist-info/RECORD +0 -10
- {sqliter_py-0.1.1.dist-info → sqliter_py-0.2.0.dist-info}/WHEEL +0 -0
sqliter/constants.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Define constants used in the library."""
|
|
2
|
+
|
|
3
|
+
OPERATOR_MAPPING = {
|
|
4
|
+
"__lt": "<",
|
|
5
|
+
"__lte": "<=",
|
|
6
|
+
"__gt": ">",
|
|
7
|
+
"__gte": ">=",
|
|
8
|
+
"__eq": "=",
|
|
9
|
+
"__ne": "!=",
|
|
10
|
+
"__in": "IN",
|
|
11
|
+
"__not_in": "NOT IN",
|
|
12
|
+
"__isnull": "IS NULL",
|
|
13
|
+
"__notnull": "IS NOT NULL",
|
|
14
|
+
"__startswith": "LIKE",
|
|
15
|
+
"__endswith": "LIKE",
|
|
16
|
+
"__contains": "LIKE",
|
|
17
|
+
"__istartswith": "LIKE",
|
|
18
|
+
"__iendswith": "LIKE",
|
|
19
|
+
"__icontains": "LIKE",
|
|
20
|
+
}
|
sqliter/exceptions.py
CHANGED
|
@@ -72,6 +72,12 @@ class InvalidOffsetError(SqliterError):
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
class InvalidOrderError(SqliterError):
|
|
76
|
+
"""Raised when an invalid order value is used."""
|
|
77
|
+
|
|
78
|
+
message_template = "Invalid order value - {}"
|
|
79
|
+
|
|
80
|
+
|
|
75
81
|
class TableCreationError(SqliterError):
|
|
76
82
|
"""Raised when a table cannot be created in the database."""
|
|
77
83
|
|
sqliter/query/query.py
CHANGED
|
@@ -3,20 +3,29 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import sqlite3
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Optional
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
|
7
7
|
|
|
8
|
-
from typing_extensions import Self
|
|
8
|
+
from typing_extensions import LiteralString, Self
|
|
9
9
|
|
|
10
|
+
from sqliter.constants import OPERATOR_MAPPING
|
|
10
11
|
from sqliter.exceptions import (
|
|
11
12
|
InvalidFilterError,
|
|
12
13
|
InvalidOffsetError,
|
|
14
|
+
InvalidOrderError,
|
|
13
15
|
RecordFetchError,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
if TYPE_CHECKING: # pragma: no cover
|
|
19
|
+
from pydantic.fields import FieldInfo
|
|
20
|
+
|
|
17
21
|
from sqliter import SqliterDB
|
|
18
22
|
from sqliter.model import BaseDBModel
|
|
19
23
|
|
|
24
|
+
# Define a type alias for the possible value types
|
|
25
|
+
FilterValue = Union[
|
|
26
|
+
str, int, float, bool, None, list[Union[str, int, float, bool]]
|
|
27
|
+
]
|
|
28
|
+
|
|
20
29
|
|
|
21
30
|
class QueryBuilder:
|
|
22
31
|
"""Functions to build and execute queries for a given model."""
|
|
@@ -26,22 +35,138 @@ class QueryBuilder:
|
|
|
26
35
|
self.db = db
|
|
27
36
|
self.model_class = model_class
|
|
28
37
|
self.table_name = model_class.get_table_name() # Use model_class method
|
|
29
|
-
self.filters: list[tuple[str, Any]] = []
|
|
38
|
+
self.filters: list[tuple[str, Any, str]] = []
|
|
30
39
|
self._limit: Optional[int] = None
|
|
31
40
|
self._offset: Optional[int] = None
|
|
32
41
|
self._order_by: Optional[str] = None
|
|
33
42
|
|
|
34
|
-
def filter(self, **conditions: str | float | None) ->
|
|
43
|
+
def filter(self, **conditions: str | float | None) -> QueryBuilder:
|
|
35
44
|
"""Add filter conditions to the query."""
|
|
36
45
|
valid_fields = self.model_class.model_fields
|
|
37
46
|
|
|
38
47
|
for field, value in conditions.items():
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
field_name, operator = self._parse_field_operator(field)
|
|
49
|
+
self._validate_field(field_name, valid_fields)
|
|
50
|
+
|
|
51
|
+
handler = self._get_operator_handler(operator)
|
|
52
|
+
handler(field_name, value, operator)
|
|
42
53
|
|
|
43
54
|
return self
|
|
44
55
|
|
|
56
|
+
def _get_operator_handler(
|
|
57
|
+
self, operator: str
|
|
58
|
+
) -> Callable[[str, Any, str], None]:
|
|
59
|
+
handlers = {
|
|
60
|
+
"__isnull": self._handle_null,
|
|
61
|
+
"__notnull": self._handle_null,
|
|
62
|
+
"__in": self._handle_in,
|
|
63
|
+
"__not_in": self._handle_in,
|
|
64
|
+
"__startswith": self._handle_like,
|
|
65
|
+
"__endswith": self._handle_like,
|
|
66
|
+
"__contains": self._handle_like,
|
|
67
|
+
"__istartswith": self._handle_like,
|
|
68
|
+
"__iendswith": self._handle_like,
|
|
69
|
+
"__icontains": self._handle_like,
|
|
70
|
+
"__lt": self._handle_comparison,
|
|
71
|
+
"__lte": self._handle_comparison,
|
|
72
|
+
"__gt": self._handle_comparison,
|
|
73
|
+
"__gte": self._handle_comparison,
|
|
74
|
+
"__ne": self._handle_comparison,
|
|
75
|
+
}
|
|
76
|
+
return handlers.get(operator, self._handle_equality)
|
|
77
|
+
|
|
78
|
+
def _validate_field(
|
|
79
|
+
self, field_name: str, valid_fields: dict[str, FieldInfo]
|
|
80
|
+
) -> None:
|
|
81
|
+
if field_name not in valid_fields:
|
|
82
|
+
raise InvalidFilterError(field_name)
|
|
83
|
+
|
|
84
|
+
def _handle_equality(
|
|
85
|
+
self, field_name: str, value: FilterValue, operator: str
|
|
86
|
+
) -> None:
|
|
87
|
+
if value is None:
|
|
88
|
+
self.filters.append((f"{field_name} IS NULL", None, "__isnull"))
|
|
89
|
+
else:
|
|
90
|
+
self.filters.append((field_name, value, operator))
|
|
91
|
+
|
|
92
|
+
def _handle_null(
|
|
93
|
+
self, field_name: str, _: FilterValue, operator: str
|
|
94
|
+
) -> None:
|
|
95
|
+
condition = (
|
|
96
|
+
f"{field_name} IS NOT NULL"
|
|
97
|
+
if operator == "__notnull"
|
|
98
|
+
else f"{field_name} IS NULL"
|
|
99
|
+
)
|
|
100
|
+
self.filters.append((condition, None, operator))
|
|
101
|
+
|
|
102
|
+
def _handle_in(
|
|
103
|
+
self, field_name: str, value: FilterValue, operator: str
|
|
104
|
+
) -> None:
|
|
105
|
+
if not isinstance(value, list):
|
|
106
|
+
err = f"{field_name} requires a list for '{operator}'"
|
|
107
|
+
raise TypeError(err)
|
|
108
|
+
sql_operator = OPERATOR_MAPPING.get(operator, "IN")
|
|
109
|
+
placeholder_list = ", ".join(["?"] * len(value))
|
|
110
|
+
self.filters.append(
|
|
111
|
+
(
|
|
112
|
+
f"{field_name} {sql_operator} ({placeholder_list})",
|
|
113
|
+
value,
|
|
114
|
+
operator,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _handle_like(
|
|
119
|
+
self, field_name: str, value: FilterValue, operator: str
|
|
120
|
+
) -> None:
|
|
121
|
+
if not isinstance(value, str):
|
|
122
|
+
err = f"{field_name} requires a string value for '{operator}'"
|
|
123
|
+
raise TypeError(err)
|
|
124
|
+
formatted_value = self._format_string_for_operator(operator, value)
|
|
125
|
+
if operator in ["__startswith", "__endswith", "__contains"]:
|
|
126
|
+
self.filters.append(
|
|
127
|
+
(
|
|
128
|
+
f"{field_name} GLOB ?",
|
|
129
|
+
[formatted_value],
|
|
130
|
+
operator,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
elif operator in ["__istartswith", "__iendswith", "__icontains"]:
|
|
134
|
+
self.filters.append(
|
|
135
|
+
(
|
|
136
|
+
f"{field_name} LIKE ?",
|
|
137
|
+
[formatted_value],
|
|
138
|
+
operator,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _handle_comparison(
|
|
143
|
+
self, field_name: str, value: FilterValue, operator: str
|
|
144
|
+
) -> None:
|
|
145
|
+
sql_operator = OPERATOR_MAPPING[operator]
|
|
146
|
+
self.filters.append((f"{field_name} {sql_operator} ?", value, operator))
|
|
147
|
+
|
|
148
|
+
# Helper method for parsing field and operator
|
|
149
|
+
def _parse_field_operator(self, field: str) -> tuple[str, str]:
|
|
150
|
+
for operator in OPERATOR_MAPPING:
|
|
151
|
+
if field.endswith(operator):
|
|
152
|
+
return field[: -len(operator)], operator
|
|
153
|
+
return field, "__eq" # Default to equality if no operator is found
|
|
154
|
+
|
|
155
|
+
# Helper method for formatting string operators (like startswith)
|
|
156
|
+
def _format_string_for_operator(self, operator: str, value: str) -> str:
|
|
157
|
+
# Mapping operators to their corresponding string format
|
|
158
|
+
format_map = {
|
|
159
|
+
"__startswith": f"{value}*",
|
|
160
|
+
"__endswith": f"*{value}",
|
|
161
|
+
"__contains": f"*{value}*",
|
|
162
|
+
"__istartswith": f"{value.lower()}%",
|
|
163
|
+
"__iendswith": f"%{value.lower()}",
|
|
164
|
+
"__icontains": f"%{value.lower()}%",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Return the formatted string or the original value if no match
|
|
168
|
+
return format_map.get(operator, value)
|
|
169
|
+
|
|
45
170
|
def limit(self, limit_value: int) -> Self:
|
|
46
171
|
"""Limit the number of results returned by the query."""
|
|
47
172
|
self._limit = limit_value
|
|
@@ -49,7 +174,7 @@ class QueryBuilder:
|
|
|
49
174
|
|
|
50
175
|
def offset(self, offset_value: int) -> Self:
|
|
51
176
|
"""Set an offset value for the query."""
|
|
52
|
-
if offset_value
|
|
177
|
+
if offset_value < 0:
|
|
53
178
|
raise InvalidOffsetError(offset_value)
|
|
54
179
|
self._offset = offset_value
|
|
55
180
|
|
|
@@ -57,28 +182,51 @@ class QueryBuilder:
|
|
|
57
182
|
self._limit = -1
|
|
58
183
|
return self
|
|
59
184
|
|
|
60
|
-
def order(self, order_by_field: str) -> Self:
|
|
61
|
-
"""Order the results by a specific field and optionally direction.
|
|
62
|
-
|
|
185
|
+
def order(self, order_by_field: str, direction: str = "ASC") -> Self:
|
|
186
|
+
"""Order the results by a specific field and optionally direction.
|
|
187
|
+
|
|
188
|
+
Currently only supports ordering by a single field, though this will be
|
|
189
|
+
expanded in the future. You can chain this method to order by multiple
|
|
190
|
+
fields.
|
|
191
|
+
|
|
192
|
+
Parameters:
|
|
193
|
+
order_by_field (str): The field to order by.
|
|
194
|
+
direction (str, optional): The sorting direction, either 'ASC' or
|
|
195
|
+
'DESC'. Defaults to 'ASC'.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Self: Returns the query object for chaining.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
InvalidOrderError: If the field or direction is invalid.
|
|
202
|
+
"""
|
|
203
|
+
if order_by_field not in self.model_class.model_fields:
|
|
204
|
+
err = f"'{order_by_field}' does not exist in the model fields."
|
|
205
|
+
raise InvalidOrderError(err)
|
|
206
|
+
|
|
207
|
+
valid_directions = {"ASC", "DESC"}
|
|
208
|
+
if direction.upper() not in valid_directions:
|
|
209
|
+
err = f"'{direction}' is not a valid sorting direction."
|
|
210
|
+
raise InvalidOrderError(err)
|
|
211
|
+
|
|
212
|
+
self._order_by = f'"{order_by_field}" {direction.upper()}'
|
|
63
213
|
return self
|
|
64
214
|
|
|
65
215
|
def _execute_query(
|
|
66
216
|
self,
|
|
67
217
|
*,
|
|
68
218
|
fetch_one: bool = False,
|
|
219
|
+
count_only: bool = False,
|
|
69
220
|
) -> list[tuple[Any, ...]] | Optional[tuple[Any, ...]]:
|
|
70
221
|
"""Helper function to execute the query with filters."""
|
|
71
222
|
fields = ", ".join(self.model_class.model_fields)
|
|
72
223
|
|
|
73
224
|
# Build the WHERE clause with special handling for None (NULL in SQL)
|
|
74
|
-
where_clause =
|
|
75
|
-
[
|
|
76
|
-
f"{field} IS NULL" if value is None else f"{field} = ?"
|
|
77
|
-
for field, value in self.filters
|
|
78
|
-
]
|
|
79
|
-
)
|
|
225
|
+
values, where_clause = self._parse_filter()
|
|
80
226
|
|
|
81
|
-
|
|
227
|
+
select_fields = fields if not count_only else "COUNT(*)"
|
|
228
|
+
|
|
229
|
+
sql = f'SELECT {select_fields} FROM "{self.table_name}"' # noqa: S608 # nosec
|
|
82
230
|
|
|
83
231
|
if self.filters:
|
|
84
232
|
sql += f" WHERE {where_clause}"
|
|
@@ -87,13 +235,12 @@ class QueryBuilder:
|
|
|
87
235
|
sql += f" ORDER BY {self._order_by}"
|
|
88
236
|
|
|
89
237
|
if self._limit is not None:
|
|
90
|
-
sql +=
|
|
238
|
+
sql += " LIMIT ?"
|
|
239
|
+
values.append(self._limit)
|
|
91
240
|
|
|
92
241
|
if self._offset is not None:
|
|
93
|
-
sql +=
|
|
94
|
-
|
|
95
|
-
# Only include non-None values in the values list
|
|
96
|
-
values = [value for _, value in self.filters if value is not None]
|
|
242
|
+
sql += " OFFSET ?"
|
|
243
|
+
values.append(self._offset)
|
|
97
244
|
|
|
98
245
|
try:
|
|
99
246
|
with self.db.connect() as conn:
|
|
@@ -103,6 +250,25 @@ class QueryBuilder:
|
|
|
103
250
|
except sqlite3.Error as exc:
|
|
104
251
|
raise RecordFetchError(self.table_name) from exc
|
|
105
252
|
|
|
253
|
+
def _parse_filter(self) -> tuple[list[Any], LiteralString]:
|
|
254
|
+
"""Actually parse the filters."""
|
|
255
|
+
where_clauses = []
|
|
256
|
+
values = []
|
|
257
|
+
for field, value, operator in self.filters:
|
|
258
|
+
if operator == "__eq":
|
|
259
|
+
where_clauses.append(f"{field} = ?")
|
|
260
|
+
values.append(value)
|
|
261
|
+
else:
|
|
262
|
+
where_clauses.append(field)
|
|
263
|
+
if operator not in ["__isnull", "__notnull"]:
|
|
264
|
+
if isinstance(value, list):
|
|
265
|
+
values.extend(value)
|
|
266
|
+
else:
|
|
267
|
+
values.append(value)
|
|
268
|
+
|
|
269
|
+
where_clause = " AND ".join(where_clauses)
|
|
270
|
+
return values, where_clause
|
|
271
|
+
|
|
106
272
|
def fetch_all(self) -> list[BaseDBModel]:
|
|
107
273
|
"""Fetch all results matching the filters."""
|
|
108
274
|
results = self._execute_query()
|
|
@@ -161,22 +327,9 @@ class QueryBuilder:
|
|
|
161
327
|
|
|
162
328
|
def count(self) -> int:
|
|
163
329
|
"""Return the count of records matching the filters."""
|
|
164
|
-
|
|
165
|
-
[f"{field} = ?" for field, _ in self.filters]
|
|
166
|
-
)
|
|
167
|
-
sql = f"SELECT COUNT(*) FROM {self.table_name}" # noqa: S608
|
|
168
|
-
|
|
169
|
-
if self.filters:
|
|
170
|
-
sql += f" WHERE {where_clause}"
|
|
171
|
-
|
|
172
|
-
values = [value for _, value in self.filters]
|
|
173
|
-
|
|
174
|
-
with self.db.connect() as conn:
|
|
175
|
-
cursor = conn.cursor()
|
|
176
|
-
cursor.execute(sql, values)
|
|
177
|
-
result = cursor.fetchone()
|
|
330
|
+
result = self._execute_query(count_only=True)
|
|
178
331
|
|
|
179
|
-
return int(result[0]) if result else 0
|
|
332
|
+
return int(result[0][0]) if result else 0
|
|
180
333
|
|
|
181
334
|
def exists(self) -> bool:
|
|
182
335
|
"""Return True if any record matches the filters."""
|
sqliter/sqliter.py
CHANGED
|
@@ -27,7 +27,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
27
27
|
class SqliterDB:
|
|
28
28
|
"""Class to manage SQLite database interactions."""
|
|
29
29
|
|
|
30
|
-
def __init__(self, db_filename: str, *, auto_commit: bool =
|
|
30
|
+
def __init__(self, db_filename: str, *, auto_commit: bool = True) -> None:
|
|
31
31
|
"""Initialize the class and options."""
|
|
32
32
|
self.db_filename = db_filename
|
|
33
33
|
self.auto_commit = auto_commit
|
|
@@ -42,6 +42,18 @@ class SqliterDB:
|
|
|
42
42
|
raise DatabaseConnectionError(self.db_filename) from exc
|
|
43
43
|
return self.conn
|
|
44
44
|
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
"""Close the connection to the SQLite database."""
|
|
47
|
+
if self.conn:
|
|
48
|
+
self._maybe_commit()
|
|
49
|
+
self.conn.close()
|
|
50
|
+
self.conn = None
|
|
51
|
+
|
|
52
|
+
def commit(self) -> None:
|
|
53
|
+
"""Commit any pending transactions."""
|
|
54
|
+
if self.conn:
|
|
55
|
+
self.conn.commit()
|
|
56
|
+
|
|
45
57
|
def create_table(self, model_class: type[BaseDBModel]) -> None:
|
|
46
58
|
"""Create a table based on the Pydantic model."""
|
|
47
59
|
table_name = model_class.get_table_name()
|
|
@@ -75,10 +87,10 @@ class SqliterDB:
|
|
|
75
87
|
except sqlite3.Error as exc:
|
|
76
88
|
raise TableCreationError(table_name) from exc
|
|
77
89
|
|
|
78
|
-
def _maybe_commit(self
|
|
90
|
+
def _maybe_commit(self) -> None:
|
|
79
91
|
"""Commit changes if auto_commit is True."""
|
|
80
|
-
if self.auto_commit:
|
|
81
|
-
conn.commit()
|
|
92
|
+
if self.auto_commit and self.conn:
|
|
93
|
+
self.conn.commit()
|
|
82
94
|
|
|
83
95
|
def insert(self, model_instance: BaseDBModel) -> None:
|
|
84
96
|
"""Insert a new record into the table defined by the Pydantic model."""
|
|
@@ -100,7 +112,7 @@ class SqliterDB:
|
|
|
100
112
|
with self.connect() as conn:
|
|
101
113
|
cursor = conn.cursor()
|
|
102
114
|
cursor.execute(insert_sql, values)
|
|
103
|
-
self._maybe_commit(
|
|
115
|
+
self._maybe_commit()
|
|
104
116
|
except sqlite3.Error as exc:
|
|
105
117
|
raise RecordInsertionError(table_name) from exc
|
|
106
118
|
|
|
@@ -167,7 +179,7 @@ class SqliterDB:
|
|
|
167
179
|
if cursor.rowcount == 0:
|
|
168
180
|
raise RecordNotFoundError(primary_key_value)
|
|
169
181
|
|
|
170
|
-
self._maybe_commit(
|
|
182
|
+
self._maybe_commit()
|
|
171
183
|
|
|
172
184
|
except sqlite3.Error as exc:
|
|
173
185
|
raise RecordUpdateError(table_name) from exc
|
|
@@ -190,7 +202,7 @@ class SqliterDB:
|
|
|
190
202
|
|
|
191
203
|
if cursor.rowcount == 0:
|
|
192
204
|
raise RecordNotFoundError(primary_key_value)
|
|
193
|
-
self._maybe_commit(
|
|
205
|
+
self._maybe_commit()
|
|
194
206
|
except sqlite3.Error as exc:
|
|
195
207
|
raise RecordDeletionError(table_name) from exc
|
|
196
208
|
|
|
@@ -216,7 +228,8 @@ class SqliterDB:
|
|
|
216
228
|
if exc_type:
|
|
217
229
|
# Roll back the transaction if there was an exception
|
|
218
230
|
self.conn.rollback()
|
|
219
|
-
|
|
231
|
+
else:
|
|
232
|
+
self.conn.commit()
|
|
220
233
|
finally:
|
|
221
234
|
# Close the connection and reset the instance variable
|
|
222
235
|
self.conn.close()
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sqliter-py
|
|
3
|
+
Version: 0.2.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: Changelog, https://github.com/seapagan/sqliter-py/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Repository, https://github.com/seapagan/sqliter-py
|
|
9
|
+
Author-email: Grant Ramsay <grant@gnramsay.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: pydantic>=2.9.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# SQLiter <!-- omit in toc -->
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/testing.yml)
|
|
30
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/linting.yml)
|
|
31
|
+
[](https://github.com/seapagan/sqliter-py/actions/workflows/mypy.yml)
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
SQLiter is a lightweight Object-Relational Mapping (ORM) library for SQLite
|
|
35
|
+
databases in Python. It provides a simplified interface for interacting with
|
|
36
|
+
SQLite databases using Pydantic models. The only external run-time dependency
|
|
37
|
+
is Pydantic itself.
|
|
38
|
+
|
|
39
|
+
It does not aim to be a full-fledged ORM like SQLAlchemy, but rather a simple
|
|
40
|
+
and easy-to-use library for basic database operations, especially for small
|
|
41
|
+
projects. It is NOT asynchronous and does not support complex queries (at this
|
|
42
|
+
time).
|
|
43
|
+
|
|
44
|
+
The ideal use case is more for Python CLI tools that need to store data in a
|
|
45
|
+
database-like format without needing to learn SQL or use a full ORM.
|
|
46
|
+
|
|
47
|
+
> [!NOTE]
|
|
48
|
+
> This project is still in the early stages of development and is lacking some
|
|
49
|
+
> planned functionality. Please use with caution.
|
|
50
|
+
>
|
|
51
|
+
> See the [TODO](TODO.md) for planned features and improvements.
|
|
52
|
+
|
|
53
|
+
- [Features](#features)
|
|
54
|
+
- [Installation](#installation)
|
|
55
|
+
- [Quick Start](#quick-start)
|
|
56
|
+
- [Detailed Usage](#detailed-usage)
|
|
57
|
+
- [Defining Models](#defining-models)
|
|
58
|
+
- [Database Operations](#database-operations)
|
|
59
|
+
- [Creating a Connection](#creating-a-connection)
|
|
60
|
+
- [Creating Tables](#creating-tables)
|
|
61
|
+
- [Inserting Records](#inserting-records)
|
|
62
|
+
- [Querying Records](#querying-records)
|
|
63
|
+
- [Updating Records](#updating-records)
|
|
64
|
+
- [Deleting Records](#deleting-records)
|
|
65
|
+
- [Commit your changes](#commit-your-changes)
|
|
66
|
+
- [Close the Connection](#close-the-connection)
|
|
67
|
+
- [Transactions](#transactions)
|
|
68
|
+
- [Filter Options](#filter-options)
|
|
69
|
+
- [Basic Filters](#basic-filters)
|
|
70
|
+
- [Null Checks](#null-checks)
|
|
71
|
+
- [Comparison Operators](#comparison-operators)
|
|
72
|
+
- [List Operations](#list-operations)
|
|
73
|
+
- [String Operations (Case-Sensitive)](#string-operations-case-sensitive)
|
|
74
|
+
- [String Operations (Case-Insensitive)](#string-operations-case-insensitive)
|
|
75
|
+
- [Contributing](#contributing)
|
|
76
|
+
- [License](#license)
|
|
77
|
+
- [Acknowledgements](#acknowledgements)
|
|
78
|
+
|
|
79
|
+
## Features
|
|
80
|
+
|
|
81
|
+
- Table creation based on Pydantic models
|
|
82
|
+
- CRUD operations (Create, Read, Update, Delete)
|
|
83
|
+
- Basic query building with filtering, ordering, and pagination
|
|
84
|
+
- Transaction support
|
|
85
|
+
- Custom exceptions for better error handling
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
You can install SQLiter using whichever method you prefer or is compatible with
|
|
90
|
+
your project setup.
|
|
91
|
+
|
|
92
|
+
With `pip`:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install sqliter-py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or `Poetry`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
poetry add sqliter-py
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or `uv` which is rapidly becoming my favorite tool for managing projects and
|
|
105
|
+
virtual environments (`uv` is used for developing this project and in the CI):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uv add sqliter-py
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Quick Start
|
|
112
|
+
|
|
113
|
+
Here's a quick example of how to use SQLiter:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from sqliter import SqliterDB
|
|
117
|
+
from sqliter.model import BaseDBModel
|
|
118
|
+
|
|
119
|
+
# Define your model
|
|
120
|
+
class User(BaseDBModel):
|
|
121
|
+
name: str
|
|
122
|
+
age: int
|
|
123
|
+
|
|
124
|
+
class Meta:
|
|
125
|
+
table_name = "users"
|
|
126
|
+
|
|
127
|
+
# Create a database connection
|
|
128
|
+
db = SqliterDB("example.db")
|
|
129
|
+
|
|
130
|
+
# Create the table
|
|
131
|
+
db.create_table(User)
|
|
132
|
+
|
|
133
|
+
# Insert a record
|
|
134
|
+
user = User(name="John Doe", age=30)
|
|
135
|
+
db.insert(user)
|
|
136
|
+
|
|
137
|
+
# Query records
|
|
138
|
+
results = db.select(User).filter(name="John Doe").fetch_all()
|
|
139
|
+
for user in results:
|
|
140
|
+
print(f"User: {user.name}, Age: {user.age}")
|
|
141
|
+
|
|
142
|
+
# Update a record
|
|
143
|
+
user.age = 31
|
|
144
|
+
db.update(user)
|
|
145
|
+
|
|
146
|
+
# Delete a record
|
|
147
|
+
db.delete(User, "John Doe")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Detailed Usage
|
|
151
|
+
|
|
152
|
+
### Defining Models
|
|
153
|
+
|
|
154
|
+
Models in SQLiter are based on Pydantic's `BaseModel`. You can define your
|
|
155
|
+
models like this:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from sqliter.model import BaseDBModel
|
|
159
|
+
|
|
160
|
+
class User(BaseDBModel):
|
|
161
|
+
name: str
|
|
162
|
+
age: int
|
|
163
|
+
email: str
|
|
164
|
+
|
|
165
|
+
class Meta:
|
|
166
|
+
table_name = "users"
|
|
167
|
+
primary_key = "name" # Default is "id"
|
|
168
|
+
create_id = False # Set to True to auto-create an ID field
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Database Operations
|
|
172
|
+
|
|
173
|
+
#### Creating a Connection
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from sqliter import SqliterDB
|
|
177
|
+
|
|
178
|
+
db = SqliterDB("your_database.db")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The default behavior is to automatically commit changes to the database after
|
|
182
|
+
each operation. If you want to disable this behavior, you can set `auto_commit=False`
|
|
183
|
+
when creating the database connection:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
db = SqliterDB("your_database.db", auto_commit=False)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
It is then up to you to manually commit changes using the `commit()` method.
|
|
190
|
+
This can be useful when you want to perform multiple operations in a single
|
|
191
|
+
transaction without the overhead of committing after each operation.
|
|
192
|
+
|
|
193
|
+
#### Creating Tables
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
db.create_table(User)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Inserting Records
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
user = User(name="Jane Doe", age=25, email="jane@example.com")
|
|
203
|
+
db.insert(user)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### Querying Records
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# Fetch all users
|
|
210
|
+
all_users = db.select(User).fetch_all()
|
|
211
|
+
|
|
212
|
+
# Filter users
|
|
213
|
+
young_users = db.select(User).filter(age=25).fetch_all()
|
|
214
|
+
|
|
215
|
+
# Order users
|
|
216
|
+
ordered_users = db.select(User).order("age", direction="DESC").fetch_all()
|
|
217
|
+
|
|
218
|
+
# Limit and offset
|
|
219
|
+
paginated_users = db.select(User).limit(10).offset(20).fetch_all()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
See below for more advanced filtering options.
|
|
223
|
+
|
|
224
|
+
#### Updating Records
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
user.age = 26
|
|
228
|
+
db.update(user)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### Deleting Records
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
db.delete(User, "Jane Doe")
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### Commit your changes
|
|
238
|
+
|
|
239
|
+
By default, SQLiter will automatically commit changes to the database after each
|
|
240
|
+
operation. If you want to disable this behavior, you can set `auto_commit=False`
|
|
241
|
+
when creating the database connection:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
db = SqliterDB("your_database.db", auto_commit=False)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
You can then manually commit changes using the `commit()` method:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
db.commit()
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Close the Connection
|
|
254
|
+
|
|
255
|
+
When you're done with the database connection, you should close it to release
|
|
256
|
+
resources:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
db.close()
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Note that closing the connection will also commit any pending changes, unless
|
|
263
|
+
`auto_commit` is set to `False`.
|
|
264
|
+
|
|
265
|
+
### Transactions
|
|
266
|
+
|
|
267
|
+
SQLiter supports transactions using Python's context manager:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
with db:
|
|
271
|
+
db.insert(User(name="Alice", age=30, email="alice@example.com"))
|
|
272
|
+
db.insert(User(name="Bob", age=35, email="bob@example.com"))
|
|
273
|
+
# If an exception occurs, the transaction will be rolled back
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
> [!WARNING]
|
|
277
|
+
> Using the context manager will automatically commit the transaction
|
|
278
|
+
> at the end (unless an exception occurs), regardless of the `auto_commit`
|
|
279
|
+
> setting.
|
|
280
|
+
>
|
|
281
|
+
> the 'close()' method will also be called when the context manager exits, so you
|
|
282
|
+
> do not need to call it manually.
|
|
283
|
+
|
|
284
|
+
### Filter Options
|
|
285
|
+
|
|
286
|
+
The `filter()` method in SQLiter supports various filter options to query records.
|
|
287
|
+
|
|
288
|
+
#### Basic Filters
|
|
289
|
+
|
|
290
|
+
- `__eq`: Equal to (default if no operator is specified)
|
|
291
|
+
- Example: `name="John"` or `name__eq="John"`
|
|
292
|
+
|
|
293
|
+
#### Null Checks
|
|
294
|
+
|
|
295
|
+
- `__isnull`: Is NULL
|
|
296
|
+
- Example: `email__isnull=True`
|
|
297
|
+
- `__notnull`: Is NOT NULL
|
|
298
|
+
- Example: `email__notnull=True`
|
|
299
|
+
|
|
300
|
+
#### Comparison Operators
|
|
301
|
+
|
|
302
|
+
- `__lt`: Less than
|
|
303
|
+
- Example: `age__lt=30`
|
|
304
|
+
- `__lte`: Less than or equal to
|
|
305
|
+
- Example: `age__lte=30`
|
|
306
|
+
- `__gt`: Greater than
|
|
307
|
+
- Example: `age__gt=30`
|
|
308
|
+
- `__gte`: Greater than or equal to
|
|
309
|
+
- Example: `age__gte=30`
|
|
310
|
+
- `__ne`: Not equal to
|
|
311
|
+
- Example: `status__ne="inactive"`
|
|
312
|
+
|
|
313
|
+
#### List Operations
|
|
314
|
+
|
|
315
|
+
- `__in`: In a list of values
|
|
316
|
+
- Example: `status__in=["active", "pending"]`
|
|
317
|
+
- `__not_in`: Not in a list of values
|
|
318
|
+
- Example: `category__not_in=["archived", "deleted"]`
|
|
319
|
+
|
|
320
|
+
#### String Operations (Case-Sensitive)
|
|
321
|
+
|
|
322
|
+
- `__startswith`: Starts with
|
|
323
|
+
- Example: `name__startswith="A"`
|
|
324
|
+
- `__endswith`: Ends with
|
|
325
|
+
- Example: `email__endswith=".com"`
|
|
326
|
+
- `__contains`: Contains
|
|
327
|
+
- Example: `description__contains="important"`
|
|
328
|
+
|
|
329
|
+
#### String Operations (Case-Insensitive)
|
|
330
|
+
|
|
331
|
+
- `__istartswith`: Starts with (case-insensitive)
|
|
332
|
+
- Example: `name__istartswith="a"`
|
|
333
|
+
- `__iendswith`: Ends with (case-insensitive)
|
|
334
|
+
- Example: `email__iendswith=".COM"`
|
|
335
|
+
- `__icontains`: Contains (case-insensitive)
|
|
336
|
+
- Example: `description__icontains="IMPORTANT"`
|
|
337
|
+
|
|
338
|
+
## Contributing
|
|
339
|
+
|
|
340
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
341
|
+
|
|
342
|
+
## License
|
|
343
|
+
|
|
344
|
+
This project is licensed under the MIT License.
|
|
345
|
+
|
|
346
|
+
## Acknowledgements
|
|
347
|
+
|
|
348
|
+
SQLiter was initially developed as an experiment to see how helpful ChatGPT and
|
|
349
|
+
Claud AI can be to speed up the development process. The initial version of the
|
|
350
|
+
code was generated by ChatGPT, with subsequent manual/AI refinements and
|
|
351
|
+
improvements.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
sqliter/__init__.py,sha256=L8R0uvCbbbACwaI5xtd3khtvpNhlPRgHJAaYZvqjzig,134
|
|
2
|
+
sqliter/constants.py,sha256=QEUC6kPkwYItgFRUmV6qfK9YV1PcUyUoBwj34yhAyik,441
|
|
3
|
+
sqliter/exceptions.py,sha256=RP1T67PkJMOgkT7yIjES1xil832_UmuAeABtNiv-RKE,3756
|
|
4
|
+
sqliter/sqliter.py,sha256=1MOa763OQdwkiAHHAitEKox5O9EV0FVMbMWC7pqyk9U,7823
|
|
5
|
+
sqliter/model/__init__.py,sha256=Ovpkbyx2-T6Oee0qFNgUBBc2M0uwK-cdG0pigG3mkd8,179
|
|
6
|
+
sqliter/model/model.py,sha256=t1w38om37gma1gRk01Z_9II0h4g-l734ijN_8M1SYoY,1247
|
|
7
|
+
sqliter/query/__init__.py,sha256=BluNMJpuoo2PsYN-bL7fXlEc02O_8LgOMsvCmyv04ao,125
|
|
8
|
+
sqliter/query/query.py,sha256=dRW-Y7X3qH4HrTw-oFbomnl4Kz7WOsImIfoKXZqkMKQ,11648
|
|
9
|
+
sqliter_py-0.2.0.dist-info/METADATA,sha256=OBYavQg-H3HipYh_mVkaJ4cL_EUpzqPk1MtypWUpwlg,9880
|
|
10
|
+
sqliter_py-0.2.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
11
|
+
sqliter_py-0.2.0.dist-info/RECORD,,
|
|
@@ -1,204 +0,0 @@
|
|
|
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.
|
|
@@ -1,10 +0,0 @@
|
|
|
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,,
|
|
File without changes
|