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 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) -> Self:
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
- if field not in valid_fields:
40
- raise InvalidFilterError(field)
41
- self.filters.append((field, value))
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 <= 0:
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
- self._order_by = order_by_field
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 = " AND ".join(
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
- sql = f"SELECT {fields} FROM {self.table_name}" # noqa: S608
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 += f" LIMIT {self._limit}"
238
+ sql += " LIMIT ?"
239
+ values.append(self._limit)
91
240
 
92
241
  if self._offset is not None:
93
- sql += f" OFFSET {self._offset}"
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
- where_clause = " AND ".join(
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 = False) -> None:
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, conn: sqlite3.Connection) -> None:
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(conn)
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(conn)
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(conn)
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
- self._maybe_commit(self.conn)
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
+ ![PyPI - Version](https://img.shields.io/pypi/v/sqliter-py)
29
+ [![Test Suite](https://github.com/seapagan/sqliter-py/actions/workflows/testing.yml/badge.svg)](https://github.com/seapagan/sqliter-py/actions/workflows/testing.yml)
30
+ [![Linting](https://github.com/seapagan/sqliter-py/actions/workflows/linting.yml/badge.svg)](https://github.com/seapagan/sqliter-py/actions/workflows/linting.yml)
31
+ [![Type Checking](https://github.com/seapagan/sqliter-py/actions/workflows/mypy.yml/badge.svg)](https://github.com/seapagan/sqliter-py/actions/workflows/mypy.yml)
32
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sqliter-py)
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
- <!-- ![PyPI - Version](https://img.shields.io/pypi/v/sqliter-py) -->
28
- [![Test Suite](https://github.com/seapagan/sqliter-py/actions/workflows/testing.yml/badge.svg)](https://github.com/seapagan/sqliter-py/actions/workflows/testing.yml)
29
- [![Linting](https://github.com/seapagan/sqliter-py/actions/workflows/linting.yml/badge.svg)](https://github.com/seapagan/sqliter-py/actions/workflows/linting.yml)
30
- [![Type Checking](https://github.com/seapagan/sqliter-py/actions/workflows/mypy.yml/badge.svg)](https://github.com/seapagan/sqliter-py/actions/workflows/mypy.yml)
31
- <!-- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sqliter-py) -->
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,,