sqliter-py 0.7.0__tar.gz → 0.8.0__tar.gz

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.

@@ -220,3 +220,5 @@ pyrightconfig.json
220
220
  repopack-output.xml
221
221
  .envrc
222
222
  demo-db
223
+ .planning
224
+ .aider*
@@ -1,5 +1,5 @@
1
1
  The MIT License (MIT)
2
- Copyright (c) 2024 Grant Ramsay
2
+ Copyright (c) 2024-2025 Grant Ramsay
3
3
 
4
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  of this software and associated documentation files (the "Software"), to deal
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: sqliter-py
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Interact with SQLite databases using Python and Pydantic
5
+ Project-URL: Homepage, http://sqliter.grantramsay.dev
5
6
  Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
6
7
  Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
7
8
  Project-URL: Changelog, https://github.com/seapagan/sqliter-py/blob/main/CHANGELOG.md
@@ -18,6 +19,8 @@ Classifier: Programming Language :: Python :: 3.9
18
19
  Classifier: Programming Language :: Python :: 3.10
19
20
  Classifier: Programming Language :: Python :: 3.11
20
21
  Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Database :: Front-Ends
21
24
  Classifier: Topic :: Software Development
22
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
26
  Requires-Python: >=3.9
@@ -47,8 +50,7 @@ time).
47
50
  The ideal use case is more for Python CLI tools that need to store data in a
48
51
  database-like format without needing to learn SQL or use a full ORM.
49
52
 
50
- Full documentation is available on the [Documentation
51
- Website](https://sqliter.grantramsay.dev)
53
+ Full documentation is available on the [Website](https://sqliter.grantramsay.dev)
52
54
 
53
55
  > [!CAUTION]
54
56
  > This project is still in the early stages of development and is lacking some
@@ -57,11 +59,6 @@ Website](https://sqliter.grantramsay.dev)
57
59
  > minimum and the releases and documentation will be very clear about any
58
60
  > breaking changes.
59
61
  >
60
- > Also, structures like `list`, `dict`, `set` etc are not supported **at this
61
- > time** as field types, since SQLite does not have a native column type for
62
- > these. This is the **next planned enhancement**. These will need to be
63
- > `pickled` first then stored as a BLOB in the database.
64
- >
65
62
  > See the [TODO](TODO.md) for planned features and improvements.
66
63
 
67
64
  - [Features](#features)
@@ -74,7 +71,8 @@ Website](https://sqliter.grantramsay.dev)
74
71
  ## Features
75
72
 
76
73
  - Table creation based on Pydantic models
77
- - Supports `date` and `datetime` fields. List/Dict/Set fields are planned.
74
+ - Supports `date` and `datetime` fields
75
+ - Support for complex data types (`list`, `dict`, `set`, `tuple`) stored as BLOBs
78
76
  - Automatic primary key generation
79
77
  - User defined indexes on any field
80
78
  - Set any field as UNIQUE
@@ -159,8 +157,11 @@ for user in results:
159
157
  new_user.age = 31
160
158
  db.update(new_user)
161
159
 
162
- # Delete a record
160
+ # Delete a record by primary key
163
161
  db.delete(User, new_user.pk)
162
+
163
+ # Delete all records returned from a query:
164
+ delete_count = db.select(User).filter(age__gt=30).delete()
164
165
  ```
165
166
 
166
167
  See the [Usage](https://sqliter.grantramsay.dev/usage) section of the documentation
@@ -180,7 +181,7 @@ which you can read in the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file.
180
181
  This project is licensed under the MIT License.
181
182
 
182
183
  ```pre
183
- Copyright (c) 2024 Grant Ramsay
184
+ Copyright (c) 2024-2025 Grant Ramsay
184
185
 
185
186
  Permission is hereby granted, free of charge, to any person obtaining a copy
186
187
  of this software and associated documentation files (the "Software"), to deal
@@ -19,8 +19,7 @@ time).
19
19
  The ideal use case is more for Python CLI tools that need to store data in a
20
20
  database-like format without needing to learn SQL or use a full ORM.
21
21
 
22
- Full documentation is available on the [Documentation
23
- Website](https://sqliter.grantramsay.dev)
22
+ Full documentation is available on the [Website](https://sqliter.grantramsay.dev)
24
23
 
25
24
  > [!CAUTION]
26
25
  > This project is still in the early stages of development and is lacking some
@@ -29,11 +28,6 @@ Website](https://sqliter.grantramsay.dev)
29
28
  > minimum and the releases and documentation will be very clear about any
30
29
  > breaking changes.
31
30
  >
32
- > Also, structures like `list`, `dict`, `set` etc are not supported **at this
33
- > time** as field types, since SQLite does not have a native column type for
34
- > these. This is the **next planned enhancement**. These will need to be
35
- > `pickled` first then stored as a BLOB in the database.
36
- >
37
31
  > See the [TODO](TODO.md) for planned features and improvements.
38
32
 
39
33
  - [Features](#features)
@@ -46,7 +40,8 @@ Website](https://sqliter.grantramsay.dev)
46
40
  ## Features
47
41
 
48
42
  - Table creation based on Pydantic models
49
- - Supports `date` and `datetime` fields. List/Dict/Set fields are planned.
43
+ - Supports `date` and `datetime` fields
44
+ - Support for complex data types (`list`, `dict`, `set`, `tuple`) stored as BLOBs
50
45
  - Automatic primary key generation
51
46
  - User defined indexes on any field
52
47
  - Set any field as UNIQUE
@@ -131,8 +126,11 @@ for user in results:
131
126
  new_user.age = 31
132
127
  db.update(new_user)
133
128
 
134
- # Delete a record
129
+ # Delete a record by primary key
135
130
  db.delete(User, new_user.pk)
131
+
132
+ # Delete all records returned from a query:
133
+ delete_count = db.select(User).filter(age__gt=30).delete()
136
134
  ```
137
135
 
138
136
  See the [Usage](https://sqliter.grantramsay.dev/usage) section of the documentation
@@ -152,7 +150,7 @@ which you can read in the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file.
152
150
  This project is licensed under the MIT License.
153
151
 
154
152
  ```pre
155
- Copyright (c) 2024 Grant Ramsay
153
+ Copyright (c) 2024-2025 Grant Ramsay
156
154
 
157
155
  Permission is hereby granted, free of charge, to any person obtaining a copy
158
156
  of this software and associated documentation files (the "Software"), to deal
@@ -3,7 +3,7 @@
3
3
 
4
4
  [project]
5
5
  name = "sqliter-py"
6
- version = "0.7.0"
6
+ version = "0.8.0"
7
7
  description = "Interact with SQLite databases using Python and Pydantic"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.9"
@@ -21,6 +21,8 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.10",
22
22
  "Programming Language :: Python :: 3.11",
23
23
  "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Database :: Front-Ends",
24
26
  "Topic :: Software Development",
25
27
  "Topic :: Software Development :: Libraries :: Python Modules",
26
28
  ]
@@ -29,7 +31,7 @@ classifiers = [
29
31
  extras = ["inflect==7.0.0"]
30
32
 
31
33
  [project.urls]
32
- # "HomeHage" = "https://xxxxxx"
34
+ "Homepage" = "http://sqliter.grantramsay.dev"
33
35
  "Pull Requests" = "https://github.com/seapagan/sqliter-py/pulls"
34
36
  "Bug Tracker" = "https://github.com/seapagan/sqliter-py/issues"
35
37
  "Changelog" = "https://github.com/seapagan/sqliter-py/blob/main/CHANGELOG.md"
@@ -100,12 +102,11 @@ changelog.help = "Generate a changelog"
100
102
  line-length = 80
101
103
  lint.select = ["ALL"] # we are being very strict!
102
104
  lint.ignore = [
103
- "ANN101",
104
- "ANN102",
105
105
  "PGH003",
106
106
  "FBT002",
107
107
  "FBT003",
108
108
  "B006",
109
+ "S301", # in this library we use 'pickle' for saving and loading list etc
109
110
  ] # These rules are too strict even for us 😝
110
111
  lint.extend-ignore = [
111
112
  "COM812",
@@ -10,7 +10,6 @@ import datetime
10
10
 
11
11
  # A dictionary mapping SQLiter filter operators to their corresponding SQL
12
12
  # operators.
13
-
14
13
  OPERATOR_MAPPING = {
15
14
  "__lt": "<",
16
15
  "__lte": "<=",
@@ -39,4 +38,8 @@ SQLITE_TYPE_MAPPING = {
39
38
  bytes: "BLOB",
40
39
  datetime.datetime: "INTEGER", # Store as Unix timestamp
41
40
  datetime.date: "INTEGER", # Store as Unix timestamp
41
+ list: "BLOB",
42
+ dict: "BLOB",
43
+ set: "BLOB",
44
+ tuple: "BLOB",
42
45
  }
@@ -8,4 +8,4 @@ define unique constraints on model fields.
8
8
  from .model import BaseDBModel, SerializableField
9
9
  from .unique import Unique
10
10
 
11
- __all__ = ["BaseDBModel", "Unique", "SerializableField"]
11
+ __all__ = ["BaseDBModel", "SerializableField", "Unique"]
@@ -10,6 +10,7 @@ in SQLiter applications.
10
10
  from __future__ import annotations
11
11
 
12
12
  import datetime
13
+ import pickle
13
14
  import re
14
15
  from typing import (
15
16
  Any,
@@ -58,7 +59,7 @@ class BaseDBModel(BaseModel):
58
59
  model_config = ConfigDict(
59
60
  extra="ignore",
60
61
  populate_by_name=True,
61
- validate_assignment=False,
62
+ validate_assignment=True,
62
63
  from_attributes=True,
63
64
  )
64
65
 
@@ -181,7 +182,9 @@ class BaseDBModel(BaseModel):
181
182
  """
182
183
  if isinstance(value, (datetime.datetime, datetime.date)):
183
184
  return to_unix_timestamp(value)
184
- return value # Return value as-is for non-datetime fields
185
+ if isinstance(value, (list, dict, set, tuple)):
186
+ return pickle.dumps(value)
187
+ return value # Return value as-is for other fields
185
188
 
186
189
  # Deserialization after fetching from the database
187
190
 
@@ -205,12 +208,31 @@ class BaseDBModel(BaseModel):
205
208
  A datetime or date object if the field type is datetime or date,
206
209
  otherwise returns the value as-is.
207
210
  """
208
- field_type = cls.__annotations__.get(field_name)
211
+ if value is None:
212
+ return None
209
213
 
210
- if field_type in (datetime.datetime, datetime.date) and isinstance(
211
- value, int
214
+ # Get field type if it exists in model_fields
215
+ field_info = cls.model_fields.get(field_name)
216
+ if field_info is None:
217
+ # If field doesn't exist in model, return value as-is
218
+ return value
219
+
220
+ field_type = field_info.annotation
221
+
222
+ if (
223
+ isinstance(field_type, type)
224
+ and issubclass(field_type, (datetime.datetime, datetime.date))
225
+ and isinstance(value, int)
212
226
  ):
213
227
  return from_unix_timestamp(
214
228
  value, field_type, localize=return_local_time
215
229
  )
216
- return value # Return value as-is for non-datetime fields
230
+
231
+ origin_type = get_origin(field_type) or field_type
232
+ if origin_type in (list, dict, set, tuple) and isinstance(value, bytes):
233
+ try:
234
+ return pickle.loads(value)
235
+ except pickle.UnpicklingError:
236
+ return value
237
+
238
+ return value
@@ -28,6 +28,7 @@ from sqliter.exceptions import (
28
28
  InvalidFilterError,
29
29
  InvalidOffsetError,
30
30
  InvalidOrderError,
31
+ RecordDeletionError,
31
32
  RecordFetchError,
32
33
  )
33
34
 
@@ -728,3 +729,34 @@ class QueryBuilder:
728
729
  True if at least one result exists, False otherwise.
729
730
  """
730
731
  return self.count() > 0
732
+
733
+ def delete(self) -> int:
734
+ """Delete records that match the current query conditions.
735
+
736
+ Returns:
737
+ The number of records deleted.
738
+
739
+ Raises:
740
+ RecordDeletionError: If there's an error deleting the records.
741
+ """
742
+ sql = f'DELETE FROM "{self.table_name}"' # noqa: S608 # nosec
743
+
744
+ # Build the WHERE clause with special handling for None (NULL in SQL)
745
+ values, where_clause = self._parse_filter()
746
+
747
+ if self.filters:
748
+ sql += f" WHERE {where_clause}"
749
+
750
+ # Print the raw SQL and values if debug is enabled
751
+ if self.db.debug:
752
+ self.db._log_sql(sql, values) # noqa: SLF001
753
+
754
+ try:
755
+ with self.db.connect() as conn:
756
+ cursor = conn.cursor()
757
+ cursor.execute(sql, values)
758
+ deleted_count = cursor.rowcount
759
+ self.db._maybe_commit() # noqa: SLF001
760
+ return deleted_count
761
+ except sqlite3.Error as exc:
762
+ raise RecordDeletionError(self.table_name) from exc
@@ -503,7 +503,13 @@ class SqliterDB:
503
503
  raise RecordInsertionError(table_name) from exc
504
504
  else:
505
505
  data.pop("pk", None)
506
- return model_class(pk=cursor.lastrowid, **data)
506
+ # Deserialize each field before creating the model instance
507
+ deserialized_data = {}
508
+ for field_name, value in data.items():
509
+ deserialized_data[field_name] = model_class.deserialize_field(
510
+ field_name, value, return_local_time=self.return_local_time
511
+ )
512
+ return model_class(pk=cursor.lastrowid, **deserialized_data)
507
513
 
508
514
  def get(
509
515
  self, model_class: type[BaseDBModel], primary_key_value: int
@@ -540,7 +546,17 @@ class SqliterDB:
540
546
  field: result[idx]
541
547
  for idx, field in enumerate(model_class.model_fields)
542
548
  }
543
- return model_class(**result_dict)
549
+ # Deserialize each field before creating the model instance
550
+ deserialized_data = {}
551
+ for field_name, value in result_dict.items():
552
+ deserialized_data[field_name] = (
553
+ model_class.deserialize_field(
554
+ field_name,
555
+ value,
556
+ return_local_time=self.return_local_time,
557
+ )
558
+ )
559
+ return model_class(**deserialized_data)
544
560
  except sqlite3.Error as exc:
545
561
  raise RecordFetchError(table_name) from exc
546
562
  else: