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 ADDED
@@ -0,0 +1,5 @@
1
+ """The 'sqliter' package provides an minimal ORM for the SQLite library."""
2
+
3
+ from .sqliter import SqliterDB
4
+
5
+ __all__ = ["SqliterDB"]
@@ -0,0 +1,8 @@
1
+ """This module defines the BaseDBModel class.
2
+
3
+ This should be subclassed by the user to interact with the database.
4
+ """
5
+
6
+ from .model import BaseDBModel
7
+
8
+ __all__ = ["BaseDBModel"]
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)
@@ -0,0 +1,5 @@
1
+ """Define the 'QueryBuilder' class for building SQL queries."""
2
+
3
+ from .query import QueryBuilder
4
+
5
+ __all__ = ["QueryBuilder"]
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any