sqliter-py 0.1.0__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 +120 -0
- sqliter/query/query.py +241 -44
- sqliter/sqliter.py +98 -41
- sqliter_py-0.2.0.dist-info/METADATA +351 -0
- sqliter_py-0.2.0.dist-info/RECORD +11 -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.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
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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 InvalidOrderError(SqliterError):
|
|
76
|
+
"""Raised when an invalid order value is used."""
|
|
77
|
+
|
|
78
|
+
message_template = "Invalid order value - {}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TableCreationError(SqliterError):
|
|
82
|
+
"""Raised when a table cannot be created in the database."""
|
|
83
|
+
|
|
84
|
+
message_template = "Failed to create the table: '{}'"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RecordInsertionError(SqliterError):
|
|
88
|
+
"""Raised when an error occurs during record insertion."""
|
|
89
|
+
|
|
90
|
+
message_template = "Failed to insert record into table: '{}'"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RecordUpdateError(SqliterError):
|
|
94
|
+
"""Raised when an error occurs during record update."""
|
|
95
|
+
|
|
96
|
+
message_template = "Failed to update record in table: '{}'"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RecordNotFoundError(SqliterError):
|
|
100
|
+
"""Raised when a record with the specified primary key is not found."""
|
|
101
|
+
|
|
102
|
+
message_template = "Failed to find a record for key '{}' "
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class RecordFetchError(SqliterError):
|
|
106
|
+
"""Raised when an error occurs during record fetching."""
|
|
107
|
+
|
|
108
|
+
message_template = "Failed to fetch record from table: '{}'"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RecordDeletionError(SqliterError):
|
|
112
|
+
"""Raised when an error occurs during record deletion."""
|
|
113
|
+
|
|
114
|
+
message_template = "Failed to delete record from table: '{}'"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class InvalidFilterError(SqliterError):
|
|
118
|
+
"""Raised when an invalid filter field is used in a query."""
|
|
119
|
+
|
|
120
|
+
message_template = "Failed to apply filter: invalid field '{}'"
|
sqliter/query/query.py
CHANGED
|
@@ -2,14 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import sqlite3
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
|
6
7
|
|
|
7
|
-
from typing_extensions import Self
|
|
8
|
+
from typing_extensions import LiteralString, Self
|
|
9
|
+
|
|
10
|
+
from sqliter.constants import OPERATOR_MAPPING
|
|
11
|
+
from sqliter.exceptions import (
|
|
12
|
+
InvalidFilterError,
|
|
13
|
+
InvalidOffsetError,
|
|
14
|
+
InvalidOrderError,
|
|
15
|
+
RecordFetchError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
19
|
+
from pydantic.fields import FieldInfo
|
|
8
20
|
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
21
|
from sqliter import SqliterDB
|
|
11
22
|
from sqliter.model import BaseDBModel
|
|
12
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
|
+
|
|
13
29
|
|
|
14
30
|
class QueryBuilder:
|
|
15
31
|
"""Functions to build and execute queries for a given model."""
|
|
@@ -19,53 +35,245 @@ class QueryBuilder:
|
|
|
19
35
|
self.db = db
|
|
20
36
|
self.model_class = model_class
|
|
21
37
|
self.table_name = model_class.get_table_name() # Use model_class method
|
|
22
|
-
self.filters: list[tuple[str, Any]] = []
|
|
38
|
+
self.filters: list[tuple[str, Any, str]] = []
|
|
39
|
+
self._limit: Optional[int] = None
|
|
40
|
+
self._offset: Optional[int] = None
|
|
41
|
+
self._order_by: Optional[str] = None
|
|
23
42
|
|
|
24
|
-
def filter(self, **conditions: str | float | None) ->
|
|
43
|
+
def filter(self, **conditions: str | float | None) -> QueryBuilder:
|
|
25
44
|
"""Add filter conditions to the query."""
|
|
45
|
+
valid_fields = self.model_class.model_fields
|
|
46
|
+
|
|
26
47
|
for field, value in conditions.items():
|
|
27
|
-
self.
|
|
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)
|
|
53
|
+
|
|
54
|
+
return self
|
|
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
|
+
|
|
170
|
+
def limit(self, limit_value: int) -> Self:
|
|
171
|
+
"""Limit the number of results returned by the query."""
|
|
172
|
+
self._limit = limit_value
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def offset(self, offset_value: int) -> Self:
|
|
176
|
+
"""Set an offset value for the query."""
|
|
177
|
+
if offset_value < 0:
|
|
178
|
+
raise InvalidOffsetError(offset_value)
|
|
179
|
+
self._offset = offset_value
|
|
180
|
+
|
|
181
|
+
if self._limit is None:
|
|
182
|
+
self._limit = -1
|
|
183
|
+
return self
|
|
184
|
+
|
|
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()}'
|
|
28
213
|
return self
|
|
29
214
|
|
|
30
215
|
def _execute_query(
|
|
31
216
|
self,
|
|
32
|
-
limit: Optional[int] = None,
|
|
33
|
-
offset: Optional[int] = None,
|
|
34
|
-
order_by: Optional[str] = None,
|
|
35
217
|
*,
|
|
36
218
|
fetch_one: bool = False,
|
|
219
|
+
count_only: bool = False,
|
|
37
220
|
) -> list[tuple[Any, ...]] | Optional[tuple[Any, ...]]:
|
|
38
221
|
"""Helper function to execute the query with filters."""
|
|
39
222
|
fields = ", ".join(self.model_class.model_fields)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
|
|
223
|
+
|
|
224
|
+
# Build the WHERE clause with special handling for None (NULL in SQL)
|
|
225
|
+
values, where_clause = self._parse_filter()
|
|
226
|
+
|
|
227
|
+
select_fields = fields if not count_only else "COUNT(*)"
|
|
228
|
+
|
|
229
|
+
sql = f'SELECT {select_fields} FROM "{self.table_name}"' # noqa: S608 # nosec
|
|
44
230
|
|
|
45
231
|
if self.filters:
|
|
46
232
|
sql += f" WHERE {where_clause}"
|
|
47
233
|
|
|
48
|
-
if
|
|
49
|
-
sql += f" ORDER BY {
|
|
234
|
+
if self._order_by:
|
|
235
|
+
sql += f" ORDER BY {self._order_by}"
|
|
236
|
+
|
|
237
|
+
if self._limit is not None:
|
|
238
|
+
sql += " LIMIT ?"
|
|
239
|
+
values.append(self._limit)
|
|
50
240
|
|
|
51
|
-
if
|
|
52
|
-
sql +=
|
|
241
|
+
if self._offset is not None:
|
|
242
|
+
sql += " OFFSET ?"
|
|
243
|
+
values.append(self._offset)
|
|
53
244
|
|
|
54
|
-
|
|
55
|
-
|
|
245
|
+
try:
|
|
246
|
+
with self.db.connect() as conn:
|
|
247
|
+
cursor = conn.cursor()
|
|
248
|
+
cursor.execute(sql, values)
|
|
249
|
+
return cursor.fetchall() if not fetch_one else cursor.fetchone()
|
|
250
|
+
except sqlite3.Error as exc:
|
|
251
|
+
raise RecordFetchError(self.table_name) from exc
|
|
56
252
|
|
|
57
|
-
|
|
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)
|
|
58
268
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
cursor.execute(sql, values)
|
|
62
|
-
return cursor.fetchall() if not fetch_one else cursor.fetchone()
|
|
269
|
+
where_clause = " AND ".join(where_clauses)
|
|
270
|
+
return values, where_clause
|
|
63
271
|
|
|
64
272
|
def fetch_all(self) -> list[BaseDBModel]:
|
|
65
273
|
"""Fetch all results matching the filters."""
|
|
66
274
|
results = self._execute_query()
|
|
67
275
|
|
|
68
|
-
if results
|
|
276
|
+
if not results:
|
|
69
277
|
return []
|
|
70
278
|
|
|
71
279
|
return [
|
|
@@ -92,7 +300,8 @@ class QueryBuilder:
|
|
|
92
300
|
|
|
93
301
|
def fetch_first(self) -> BaseDBModel | None:
|
|
94
302
|
"""Fetch the first result of the query."""
|
|
95
|
-
|
|
303
|
+
self._limit = 1
|
|
304
|
+
result = self._execute_query()
|
|
96
305
|
if not result:
|
|
97
306
|
return None
|
|
98
307
|
return self.model_class(
|
|
@@ -103,9 +312,10 @@ class QueryBuilder:
|
|
|
103
312
|
)
|
|
104
313
|
|
|
105
314
|
def fetch_last(self) -> BaseDBModel | None:
|
|
106
|
-
"""Fetch the last result of the query (based on the
|
|
107
|
-
|
|
108
|
-
|
|
315
|
+
"""Fetch the last result of the query (based on the insertion order)."""
|
|
316
|
+
self._limit = 1
|
|
317
|
+
self._order_by = "rowid DESC"
|
|
318
|
+
result = self._execute_query()
|
|
109
319
|
if not result:
|
|
110
320
|
return None
|
|
111
321
|
return self.model_class(
|
|
@@ -117,22 +327,9 @@ class QueryBuilder:
|
|
|
117
327
|
|
|
118
328
|
def count(self) -> int:
|
|
119
329
|
"""Return the count of records matching the filters."""
|
|
120
|
-
|
|
121
|
-
[f"{field} = ?" for field, _ in self.filters]
|
|
122
|
-
)
|
|
123
|
-
sql = f"SELECT COUNT(*) FROM {self.table_name}" # noqa: S608
|
|
124
|
-
|
|
125
|
-
if self.filters:
|
|
126
|
-
sql += f" WHERE {where_clause}"
|
|
127
|
-
|
|
128
|
-
values = [value for _, value in self.filters]
|
|
129
|
-
|
|
130
|
-
with self.db.connect() as conn:
|
|
131
|
-
cursor = conn.cursor()
|
|
132
|
-
cursor.execute(sql, values)
|
|
133
|
-
result = cursor.fetchone()
|
|
330
|
+
result = self._execute_query(count_only=True)
|
|
134
331
|
|
|
135
|
-
return int(result[0]) if result else 0
|
|
332
|
+
return int(result[0][0]) if result else 0
|
|
136
333
|
|
|
137
334
|
def exists(self) -> bool:
|
|
138
335
|
"""Return True if any record matches the filters."""
|
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
|
|
@@ -18,7 +27,7 @@ if TYPE_CHECKING:
|
|
|
18
27
|
class SqliterDB:
|
|
19
28
|
"""Class to manage SQLite database interactions."""
|
|
20
29
|
|
|
21
|
-
def __init__(self, db_filename: str, *, auto_commit: bool =
|
|
30
|
+
def __init__(self, db_filename: str, *, auto_commit: bool = True) -> None:
|
|
22
31
|
"""Initialize the class and options."""
|
|
23
32
|
self.db_filename = db_filename
|
|
24
33
|
self.auto_commit = auto_commit
|
|
@@ -27,9 +36,24 @@ 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
|
|
|
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
|
+
|
|
33
57
|
def create_table(self, model_class: type[BaseDBModel]) -> None:
|
|
34
58
|
"""Create a table based on the Pydantic model."""
|
|
35
59
|
table_name = model_class.get_table_name()
|
|
@@ -43,7 +67,7 @@ class SqliterDB:
|
|
|
43
67
|
if create_id:
|
|
44
68
|
create_table_sql = f"""
|
|
45
69
|
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
46
|
-
|
|
70
|
+
{primary_key} INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
71
|
{fields}
|
|
48
72
|
)
|
|
49
73
|
"""
|
|
@@ -55,15 +79,18 @@ class SqliterDB:
|
|
|
55
79
|
)
|
|
56
80
|
"""
|
|
57
81
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
try:
|
|
83
|
+
with self.connect() as conn:
|
|
84
|
+
cursor = conn.cursor()
|
|
85
|
+
cursor.execute(create_table_sql)
|
|
86
|
+
conn.commit()
|
|
87
|
+
except sqlite3.Error as exc:
|
|
88
|
+
raise TableCreationError(table_name) from exc
|
|
62
89
|
|
|
63
|
-
def _maybe_commit(self
|
|
90
|
+
def _maybe_commit(self) -> None:
|
|
64
91
|
"""Commit changes if auto_commit is True."""
|
|
65
|
-
if self.auto_commit:
|
|
66
|
-
conn.commit()
|
|
92
|
+
if self.auto_commit and self.conn:
|
|
93
|
+
self.conn.commit()
|
|
67
94
|
|
|
68
95
|
def insert(self, model_instance: BaseDBModel) -> None:
|
|
69
96
|
"""Insert a new record into the table defined by the Pydantic model."""
|
|
@@ -77,14 +104,17 @@ class SqliterDB:
|
|
|
77
104
|
)
|
|
78
105
|
|
|
79
106
|
insert_sql = f"""
|
|
80
|
-
INSERT
|
|
107
|
+
INSERT INTO {table_name} ({fields})
|
|
81
108
|
VALUES ({placeholders})
|
|
82
109
|
""" # noqa: S608
|
|
83
110
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
111
|
+
try:
|
|
112
|
+
with self.connect() as conn:
|
|
113
|
+
cursor = conn.cursor()
|
|
114
|
+
cursor.execute(insert_sql, values)
|
|
115
|
+
self._maybe_commit()
|
|
116
|
+
except sqlite3.Error as exc:
|
|
117
|
+
raise RecordInsertionError(table_name) from exc
|
|
88
118
|
|
|
89
119
|
def get(
|
|
90
120
|
self, model_class: type[BaseDBModel], primary_key_value: str
|
|
@@ -99,18 +129,22 @@ class SqliterDB:
|
|
|
99
129
|
SELECT {fields} FROM {table_name} WHERE {primary_key} = ?
|
|
100
130
|
""" # noqa: S608
|
|
101
131
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
try:
|
|
133
|
+
with self.connect() as conn:
|
|
134
|
+
cursor = conn.cursor()
|
|
135
|
+
cursor.execute(select_sql, (primary_key_value,))
|
|
136
|
+
result = cursor.fetchone()
|
|
137
|
+
|
|
138
|
+
if result:
|
|
139
|
+
result_dict = {
|
|
140
|
+
field: result[idx]
|
|
141
|
+
for idx, field in enumerate(model_class.model_fields)
|
|
142
|
+
}
|
|
143
|
+
return model_class(**result_dict)
|
|
144
|
+
except sqlite3.Error as exc:
|
|
145
|
+
raise RecordFetchError(table_name) from exc
|
|
146
|
+
else:
|
|
147
|
+
return None
|
|
114
148
|
|
|
115
149
|
def update(self, model_instance: BaseDBModel) -> None:
|
|
116
150
|
"""Update an existing record using the Pydantic model."""
|
|
@@ -131,13 +165,24 @@ class SqliterDB:
|
|
|
131
165
|
primary_key_value = getattr(model_instance, primary_key)
|
|
132
166
|
|
|
133
167
|
update_sql = f"""
|
|
134
|
-
UPDATE {table_name}
|
|
168
|
+
UPDATE {table_name}
|
|
169
|
+
SET {fields}
|
|
170
|
+
WHERE {primary_key} = ?
|
|
135
171
|
""" # noqa: S608
|
|
136
172
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
173
|
+
try:
|
|
174
|
+
with self.connect() as conn:
|
|
175
|
+
cursor = conn.cursor()
|
|
176
|
+
cursor.execute(update_sql, (*values, primary_key_value))
|
|
177
|
+
|
|
178
|
+
# Check if any rows were updated
|
|
179
|
+
if cursor.rowcount == 0:
|
|
180
|
+
raise RecordNotFoundError(primary_key_value)
|
|
181
|
+
|
|
182
|
+
self._maybe_commit()
|
|
183
|
+
|
|
184
|
+
except sqlite3.Error as exc:
|
|
185
|
+
raise RecordUpdateError(table_name) from exc
|
|
141
186
|
|
|
142
187
|
def delete(
|
|
143
188
|
self, model_class: type[BaseDBModel], primary_key_value: str
|
|
@@ -150,10 +195,16 @@ class SqliterDB:
|
|
|
150
195
|
DELETE FROM {table_name} WHERE {primary_key} = ?
|
|
151
196
|
""" # noqa: S608
|
|
152
197
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
198
|
+
try:
|
|
199
|
+
with self.connect() as conn:
|
|
200
|
+
cursor = conn.cursor()
|
|
201
|
+
cursor.execute(delete_sql, (primary_key_value,))
|
|
202
|
+
|
|
203
|
+
if cursor.rowcount == 0:
|
|
204
|
+
raise RecordNotFoundError(primary_key_value)
|
|
205
|
+
self._maybe_commit()
|
|
206
|
+
except sqlite3.Error as exc:
|
|
207
|
+
raise RecordDeletionError(table_name) from exc
|
|
157
208
|
|
|
158
209
|
def select(self, model_class: type[BaseDBModel]) -> QueryBuilder:
|
|
159
210
|
"""Start a query for the given model."""
|
|
@@ -173,7 +224,13 @@ class SqliterDB:
|
|
|
173
224
|
) -> None:
|
|
174
225
|
"""Exit the runtime context and close the connection."""
|
|
175
226
|
if self.conn:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
227
|
+
try:
|
|
228
|
+
if exc_type:
|
|
229
|
+
# Roll back the transaction if there was an exception
|
|
230
|
+
self.conn.rollback()
|
|
231
|
+
else:
|
|
232
|
+
self.conn.commit()
|
|
233
|
+
finally:
|
|
234
|
+
# Close the connection and reset the instance variable
|
|
235
|
+
self.conn.close()
|
|
236
|
+
self.conn = None
|
|
@@ -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,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
|