sqliter-py 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqliter-py might be problematic. Click here for more details.
- sqliter/__init__.py +5 -0
- sqliter/model/__init__.py +8 -0
- sqliter/model/model.py +38 -0
- sqliter/query/__init__.py +5 -0
- sqliter/query/query.py +139 -0
- sqliter/sqliter.py +179 -0
- sqliter_py-0.1.0.dist-info/METADATA +30 -0
- sqliter_py-0.1.0.dist-info/RECORD +9 -0
- sqliter_py-0.1.0.dist-info/WHEEL +4 -0
sqliter/__init__.py
ADDED
sqliter/model/model.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Define the Base model class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseDBModel(BaseModel):
|
|
11
|
+
"""Custom base model for database models."""
|
|
12
|
+
|
|
13
|
+
class Meta:
|
|
14
|
+
"""Configure the base model with default options."""
|
|
15
|
+
|
|
16
|
+
create_id: bool = True # Whether to create an auto-increment ID
|
|
17
|
+
primary_key: str = "id" # Default primary key field
|
|
18
|
+
table_name: Optional[str] = (
|
|
19
|
+
None # Table name, defaults to class name if not set
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_table_name(cls) -> str:
|
|
24
|
+
"""Get the table name from the Meta, or default to the classname."""
|
|
25
|
+
table_name: str | None = getattr(cls.Meta, "table_name", None)
|
|
26
|
+
if table_name is not None:
|
|
27
|
+
return table_name
|
|
28
|
+
return cls.__name__.lower() # Default to class name in lowercase
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get_primary_key(cls) -> str:
|
|
32
|
+
"""Get the primary key from the Meta class or default to 'id'."""
|
|
33
|
+
return getattr(cls.Meta, "primary_key", "id")
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def should_create_id(cls) -> bool:
|
|
37
|
+
"""Check whether the model should create an auto-increment ID."""
|
|
38
|
+
return getattr(cls.Meta, "create_id", True)
|
sqliter/query/query.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Define the 'QueryBuilder' class for building SQL queries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
6
|
+
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sqliter import SqliterDB
|
|
11
|
+
from sqliter.model import BaseDBModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QueryBuilder:
|
|
15
|
+
"""Functions to build and execute queries for a given model."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, db: SqliterDB, model_class: type[BaseDBModel]) -> None:
|
|
18
|
+
"""Initialize the query builder with the database, model class, etc."""
|
|
19
|
+
self.db = db
|
|
20
|
+
self.model_class = model_class
|
|
21
|
+
self.table_name = model_class.get_table_name() # Use model_class method
|
|
22
|
+
self.filters: list[tuple[str, Any]] = []
|
|
23
|
+
|
|
24
|
+
def filter(self, **conditions: str | float | None) -> Self:
|
|
25
|
+
"""Add filter conditions to the query."""
|
|
26
|
+
for field, value in conditions.items():
|
|
27
|
+
self.filters.append((field, value))
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def _execute_query(
|
|
31
|
+
self,
|
|
32
|
+
limit: Optional[int] = None,
|
|
33
|
+
offset: Optional[int] = None,
|
|
34
|
+
order_by: Optional[str] = None,
|
|
35
|
+
*,
|
|
36
|
+
fetch_one: bool = False,
|
|
37
|
+
) -> list[tuple[Any, ...]] | Optional[tuple[Any, ...]]:
|
|
38
|
+
"""Helper function to execute the query with filters."""
|
|
39
|
+
fields = ", ".join(self.model_class.model_fields)
|
|
40
|
+
where_clause = " AND ".join(
|
|
41
|
+
[f"{field} = ?" for field, _ in self.filters]
|
|
42
|
+
)
|
|
43
|
+
sql = f"SELECT {fields} FROM {self.table_name}" # noqa: S608
|
|
44
|
+
|
|
45
|
+
if self.filters:
|
|
46
|
+
sql += f" WHERE {where_clause}"
|
|
47
|
+
|
|
48
|
+
if order_by:
|
|
49
|
+
sql += f" ORDER BY {order_by}"
|
|
50
|
+
|
|
51
|
+
if limit is not None:
|
|
52
|
+
sql += f" LIMIT {limit}"
|
|
53
|
+
|
|
54
|
+
if offset is not None:
|
|
55
|
+
sql += f" OFFSET {offset}"
|
|
56
|
+
|
|
57
|
+
values = [value for _, value in self.filters]
|
|
58
|
+
|
|
59
|
+
with self.db.connect() as conn:
|
|
60
|
+
cursor = conn.cursor()
|
|
61
|
+
cursor.execute(sql, values)
|
|
62
|
+
return cursor.fetchall() if not fetch_one else cursor.fetchone()
|
|
63
|
+
|
|
64
|
+
def fetch_all(self) -> list[BaseDBModel]:
|
|
65
|
+
"""Fetch all results matching the filters."""
|
|
66
|
+
results = self._execute_query()
|
|
67
|
+
|
|
68
|
+
if results is None:
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
return [
|
|
72
|
+
self.model_class(
|
|
73
|
+
**{
|
|
74
|
+
field: row[idx]
|
|
75
|
+
for idx, field in enumerate(self.model_class.model_fields)
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
for row in results
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
def fetch_one(self) -> BaseDBModel | None:
|
|
82
|
+
"""Fetch exactly one result."""
|
|
83
|
+
result = self._execute_query(fetch_one=True)
|
|
84
|
+
if not result:
|
|
85
|
+
return None
|
|
86
|
+
return self.model_class(
|
|
87
|
+
**{
|
|
88
|
+
field: result[idx]
|
|
89
|
+
for idx, field in enumerate(self.model_class.model_fields)
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def fetch_first(self) -> BaseDBModel | None:
|
|
94
|
+
"""Fetch the first result of the query."""
|
|
95
|
+
result = self._execute_query(limit=1)
|
|
96
|
+
if not result:
|
|
97
|
+
return None
|
|
98
|
+
return self.model_class(
|
|
99
|
+
**{
|
|
100
|
+
field: result[0][idx]
|
|
101
|
+
for idx, field in enumerate(self.model_class.model_fields)
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def fetch_last(self) -> BaseDBModel | None:
|
|
106
|
+
"""Fetch the last result of the query (based on the primary key)."""
|
|
107
|
+
primary_key = self.model_class.get_primary_key()
|
|
108
|
+
result = self._execute_query(limit=1, order_by=f"{primary_key} DESC")
|
|
109
|
+
if not result:
|
|
110
|
+
return None
|
|
111
|
+
return self.model_class(
|
|
112
|
+
**{
|
|
113
|
+
field: result[0][idx]
|
|
114
|
+
for idx, field in enumerate(self.model_class.model_fields)
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def count(self) -> int:
|
|
119
|
+
"""Return the count of records matching the filters."""
|
|
120
|
+
where_clause = " AND ".join(
|
|
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()
|
|
134
|
+
|
|
135
|
+
return int(result[0]) if result else 0
|
|
136
|
+
|
|
137
|
+
def exists(self) -> bool:
|
|
138
|
+
"""Return True if any record matches the filters."""
|
|
139
|
+
return self.count() > 0
|
sqliter/sqliter.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""This is the main module for the sqliter package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from typing import TYPE_CHECKING, Optional
|
|
7
|
+
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from sqliter.query.query import QueryBuilder
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from types import TracebackType
|
|
14
|
+
|
|
15
|
+
from sqliter.model.model import BaseDBModel
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SqliterDB:
|
|
19
|
+
"""Class to manage SQLite database interactions."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, db_filename: str, *, auto_commit: bool = False) -> None:
|
|
22
|
+
"""Initialize the class and options."""
|
|
23
|
+
self.db_filename = db_filename
|
|
24
|
+
self.auto_commit = auto_commit
|
|
25
|
+
self.conn: Optional[sqlite3.Connection] = None
|
|
26
|
+
|
|
27
|
+
def connect(self) -> sqlite3.Connection:
|
|
28
|
+
"""Create or return a connection to the SQLite database."""
|
|
29
|
+
if not self.conn:
|
|
30
|
+
self.conn = sqlite3.connect(self.db_filename)
|
|
31
|
+
return self.conn
|
|
32
|
+
|
|
33
|
+
def create_table(self, model_class: type[BaseDBModel]) -> None:
|
|
34
|
+
"""Create a table based on the Pydantic model."""
|
|
35
|
+
table_name = model_class.get_table_name()
|
|
36
|
+
primary_key = model_class.get_primary_key()
|
|
37
|
+
create_id = model_class.should_create_id()
|
|
38
|
+
|
|
39
|
+
fields = ", ".join(
|
|
40
|
+
f"{field_name} TEXT" for field_name in model_class.model_fields
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if create_id:
|
|
44
|
+
create_table_sql = f"""
|
|
45
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
{fields}
|
|
48
|
+
)
|
|
49
|
+
"""
|
|
50
|
+
else:
|
|
51
|
+
create_table_sql = f"""
|
|
52
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
53
|
+
{fields},
|
|
54
|
+
PRIMARY KEY ({primary_key})
|
|
55
|
+
)
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
with self.connect() as conn:
|
|
59
|
+
cursor = conn.cursor()
|
|
60
|
+
cursor.execute(create_table_sql)
|
|
61
|
+
conn.commit()
|
|
62
|
+
|
|
63
|
+
def _maybe_commit(self, conn: sqlite3.Connection) -> None:
|
|
64
|
+
"""Commit changes if auto_commit is True."""
|
|
65
|
+
if self.auto_commit:
|
|
66
|
+
conn.commit()
|
|
67
|
+
|
|
68
|
+
def insert(self, model_instance: BaseDBModel) -> None:
|
|
69
|
+
"""Insert a new record into the table defined by the Pydantic model."""
|
|
70
|
+
model_class = type(model_instance)
|
|
71
|
+
table_name = model_class.get_table_name()
|
|
72
|
+
|
|
73
|
+
fields = ", ".join(model_class.model_fields)
|
|
74
|
+
placeholders = ", ".join(["?"] * len(model_class.model_fields))
|
|
75
|
+
values = tuple(
|
|
76
|
+
getattr(model_instance, field) for field in model_class.model_fields
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
insert_sql = f"""
|
|
80
|
+
INSERT OR REPLACE INTO {table_name} ({fields})
|
|
81
|
+
VALUES ({placeholders})
|
|
82
|
+
""" # noqa: S608
|
|
83
|
+
|
|
84
|
+
with self.connect() as conn:
|
|
85
|
+
cursor = conn.cursor()
|
|
86
|
+
cursor.execute(insert_sql, values)
|
|
87
|
+
self._maybe_commit(conn)
|
|
88
|
+
|
|
89
|
+
def get(
|
|
90
|
+
self, model_class: type[BaseDBModel], primary_key_value: str
|
|
91
|
+
) -> BaseDBModel | None:
|
|
92
|
+
"""Retrieve a record by its PK and return a Pydantic instance."""
|
|
93
|
+
table_name = model_class.get_table_name()
|
|
94
|
+
primary_key = model_class.get_primary_key()
|
|
95
|
+
|
|
96
|
+
fields = ", ".join(model_class.model_fields)
|
|
97
|
+
|
|
98
|
+
select_sql = f"""
|
|
99
|
+
SELECT {fields} FROM {table_name} WHERE {primary_key} = ?
|
|
100
|
+
""" # noqa: S608
|
|
101
|
+
|
|
102
|
+
with self.connect() as conn:
|
|
103
|
+
cursor = conn.cursor()
|
|
104
|
+
cursor.execute(select_sql, (primary_key_value,))
|
|
105
|
+
result = cursor.fetchone()
|
|
106
|
+
|
|
107
|
+
if result:
|
|
108
|
+
result_dict = {
|
|
109
|
+
field: result[idx]
|
|
110
|
+
for idx, field in enumerate(model_class.model_fields)
|
|
111
|
+
}
|
|
112
|
+
return model_class(**result_dict)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def update(self, model_instance: BaseDBModel) -> None:
|
|
116
|
+
"""Update an existing record using the Pydantic model."""
|
|
117
|
+
model_class = type(model_instance)
|
|
118
|
+
table_name = model_class.get_table_name()
|
|
119
|
+
primary_key = model_class.get_primary_key()
|
|
120
|
+
|
|
121
|
+
fields = ", ".join(
|
|
122
|
+
f"{field} = ?"
|
|
123
|
+
for field in model_class.model_fields
|
|
124
|
+
if field != primary_key
|
|
125
|
+
)
|
|
126
|
+
values = tuple(
|
|
127
|
+
getattr(model_instance, field)
|
|
128
|
+
for field in model_class.model_fields
|
|
129
|
+
if field != primary_key
|
|
130
|
+
)
|
|
131
|
+
primary_key_value = getattr(model_instance, primary_key)
|
|
132
|
+
|
|
133
|
+
update_sql = f"""
|
|
134
|
+
UPDATE {table_name} SET {fields} WHERE {primary_key} = ?
|
|
135
|
+
""" # noqa: S608
|
|
136
|
+
|
|
137
|
+
with self.connect() as conn:
|
|
138
|
+
cursor = conn.cursor()
|
|
139
|
+
cursor.execute(update_sql, (*values, primary_key_value))
|
|
140
|
+
self._maybe_commit(conn)
|
|
141
|
+
|
|
142
|
+
def delete(
|
|
143
|
+
self, model_class: type[BaseDBModel], primary_key_value: str
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Delete a record by its primary key."""
|
|
146
|
+
table_name = model_class.get_table_name()
|
|
147
|
+
primary_key = model_class.get_primary_key()
|
|
148
|
+
|
|
149
|
+
delete_sql = f"""
|
|
150
|
+
DELETE FROM {table_name} WHERE {primary_key} = ?
|
|
151
|
+
""" # noqa: S608
|
|
152
|
+
|
|
153
|
+
with self.connect() as conn:
|
|
154
|
+
cursor = conn.cursor()
|
|
155
|
+
cursor.execute(delete_sql, (primary_key_value,))
|
|
156
|
+
self._maybe_commit(conn)
|
|
157
|
+
|
|
158
|
+
def select(self, model_class: type[BaseDBModel]) -> QueryBuilder:
|
|
159
|
+
"""Start a query for the given model."""
|
|
160
|
+
return QueryBuilder(self, model_class)
|
|
161
|
+
|
|
162
|
+
# --- Context manager methods ---
|
|
163
|
+
def __enter__(self) -> Self:
|
|
164
|
+
"""Enter the runtime context for the 'with' statement."""
|
|
165
|
+
self.connect()
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def __exit__(
|
|
169
|
+
self,
|
|
170
|
+
exc_type: Optional[type[BaseException]],
|
|
171
|
+
exc_value: Optional[BaseException],
|
|
172
|
+
traceback: Optional[TracebackType],
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Exit the runtime context and close the connection."""
|
|
175
|
+
if self.conn:
|
|
176
|
+
if not self.auto_commit:
|
|
177
|
+
self.conn.commit()
|
|
178
|
+
self.conn.close()
|
|
179
|
+
self.conn = None
|
|
@@ -0,0 +1,30 @@
|
|
|
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.
|
|
@@ -0,0 +1,9 @@
|
|
|
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,,
|