sqliter-py 0.5.0__py3-none-any.whl → 0.7.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/constants.py CHANGED
@@ -6,8 +6,11 @@ operators and data types, which are crucial for translating between
6
6
  Pydantic models and SQLite database operations.
7
7
  """
8
8
 
9
+ import datetime
10
+
9
11
  # A dictionary mapping SQLiter filter operators to their corresponding SQL
10
12
  # operators.
13
+
11
14
  OPERATOR_MAPPING = {
12
15
  "__lt": "<",
13
16
  "__lte": "<=",
@@ -34,4 +37,6 @@ SQLITE_TYPE_MAPPING = {
34
37
  str: "TEXT",
35
38
  bool: "INTEGER", # SQLite stores booleans as integers (0 or 1)
36
39
  bytes: "BLOB",
40
+ datetime.datetime: "INTEGER", # Store as Unix timestamp
41
+ datetime.date: "INTEGER", # Store as Unix timestamp
37
42
  }
sqliter/exceptions.py CHANGED
@@ -145,3 +145,24 @@ class SqlExecutionError(SqliterError):
145
145
  """Raised when an SQL execution fails."""
146
146
 
147
147
  message_template = "Failed to execute SQL: '{}'"
148
+
149
+
150
+ class InvalidIndexError(SqliterError):
151
+ """Exception raised when an invalid index field is specified.
152
+
153
+ This error is triggered if one or more fields specified for an index
154
+ do not exist in the model's fields.
155
+
156
+ Attributes:
157
+ invalid_fields (list[str]): The list of fields that were invalid.
158
+ model_class (str): The name of the model where the error occurred.
159
+ """
160
+
161
+ message_template = "Invalid fields for indexing in model '{}': {}"
162
+
163
+ def __init__(self, invalid_fields: list[str], model_class: str) -> None:
164
+ """Tidy up the error message by joining the invalid fields."""
165
+ # Join invalid fields into a comma-separated string
166
+ invalid_fields_str = ", ".join(invalid_fields)
167
+ # Pass the formatted message to the parent class
168
+ super().__init__(model_class, invalid_fields_str)
sqliter/helpers.py CHANGED
@@ -6,6 +6,9 @@ data types. These utilities support the core functionality of model
6
6
  to database schema translation.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
11
+ import datetime
9
12
  from typing import Union
10
13
 
11
14
  from sqliter.constants import SQLITE_TYPE_MAPPING
@@ -33,3 +36,65 @@ def infer_sqlite_type(field_type: Union[type, None]) -> str:
33
36
 
34
37
  # Map the simplified type to an SQLite type
35
38
  return SQLITE_TYPE_MAPPING.get(field_type, "TEXT")
39
+
40
+
41
+ def to_unix_timestamp(value: datetime.date | datetime.datetime) -> int:
42
+ """Convert datetime or date to a Unix timestamp in UTC.
43
+
44
+ Args:
45
+ value: The datetime or date object to convert.
46
+
47
+ Returns:
48
+ An integer Unix timestamp.
49
+
50
+ Raises:
51
+ TypeError: If the value is not a datetime or date object.
52
+ """
53
+ if isinstance(value, datetime.datetime):
54
+ # If no timezone is provided, assume local time and convert to UTC
55
+ if value.tzinfo is None:
56
+ value = value.astimezone() # Convert to user's local timezone
57
+ # Convert to UTC before storing
58
+ value = value.astimezone(datetime.timezone.utc)
59
+ return int(value.timestamp())
60
+ if isinstance(value, datetime.date):
61
+ # Convert date to datetime at midnight in UTC
62
+ dt = datetime.datetime.combine(
63
+ value, datetime.time(0, 0), tzinfo=datetime.timezone.utc
64
+ )
65
+ return int(dt.timestamp())
66
+
67
+ err_msg = "Expected datetime or date object."
68
+ raise TypeError(err_msg)
69
+
70
+
71
+ def from_unix_timestamp(
72
+ value: int, to_type: type, *, localize: bool = True
73
+ ) -> datetime.date | datetime.datetime:
74
+ """Convert a Unix timestamp to datetime or date, optionally to local time.
75
+
76
+ Args:
77
+ value: The Unix timestamp as an integer.
78
+ to_type: The expected output type, either datetime or date.
79
+ localize: If True, convert the datetime to the user's local timezone.
80
+
81
+ Returns:
82
+ The corresponding datetime or date object.
83
+
84
+ Raises:
85
+ TypeError: If to_type is not datetime or date.
86
+ """
87
+ if to_type is datetime.datetime:
88
+ # Convert the Unix timestamp to UTC datetime
89
+ dt = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
90
+ # Convert to local time if requested
91
+ return dt.astimezone() if localize else dt
92
+ if to_type is datetime.date:
93
+ # Convert to UTC datetime first
94
+ dt = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
95
+ # Convert to local time if requested, then return the date part
96
+ dt_local = dt.astimezone() if localize else dt
97
+ return dt_local.date() # Extract the date part
98
+
99
+ err_msg = "Expected datetime or date type."
100
+ raise TypeError(err_msg)
sqliter/model/__init__.py CHANGED
@@ -1,9 +1,11 @@
1
1
  """This module provides the base model class for SQLiter database models.
2
2
 
3
3
  It exports the BaseDBModel class, which is used to define database
4
- models in SQLiter applications.
4
+ models in SQLiter applications, and the Unique class, which is used to
5
+ define unique constraints on model fields.
5
6
  """
6
7
 
7
- from .model import BaseDBModel
8
+ from .model import BaseDBModel, SerializableField
9
+ from .unique import Unique
8
10
 
9
- __all__ = ["BaseDBModel"]
11
+ __all__ = ["BaseDBModel", "Unique", "SerializableField"]
sqliter/model/model.py CHANGED
@@ -9,14 +9,31 @@ in SQLiter applications.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import datetime
12
13
  import re
13
- from typing import Any, Optional, TypeVar, Union, cast, get_args, get_origin
14
+ from typing import (
15
+ Any,
16
+ ClassVar,
17
+ Optional,
18
+ Protocol,
19
+ TypeVar,
20
+ Union,
21
+ cast,
22
+ get_args,
23
+ get_origin,
24
+ )
14
25
 
15
26
  from pydantic import BaseModel, ConfigDict, Field
16
27
 
28
+ from sqliter.helpers import from_unix_timestamp, to_unix_timestamp
29
+
17
30
  T = TypeVar("T", bound="BaseDBModel")
18
31
 
19
32
 
33
+ class SerializableField(Protocol):
34
+ """Protocol for fields that can be serialized or deserialized."""
35
+
36
+
20
37
  class BaseDBModel(BaseModel):
21
38
  """Base model class for SQLiter database models.
22
39
 
@@ -29,6 +46,14 @@ class BaseDBModel(BaseModel):
29
46
  """
30
47
 
31
48
  pk: int = Field(0, description="The mandatory primary key of the table.")
49
+ created_at: int = Field(
50
+ default=0,
51
+ description="Unix timestamp when the record was created.",
52
+ )
53
+ updated_at: int = Field(
54
+ default=0,
55
+ description="Unix timestamp when the record was last updated.",
56
+ )
32
57
 
33
58
  model_config = ConfigDict(
34
59
  extra="ignore",
@@ -41,14 +66,24 @@ class BaseDBModel(BaseModel):
41
66
  """Metadata class for configuring database-specific attributes.
42
67
 
43
68
  Attributes:
44
- create_pk (bool): Whether to create a primary key field.
45
- primary_key (str): The name of the primary key field.
46
- table_name (Optional[str]): The name of the database table.
69
+ table_name (Optional[str]): The name of the database table. If not
70
+ specified, the table name will be inferred from the model class
71
+ name and converted to snake_case.
72
+ indexes (ClassVar[list[Union[str, tuple[str]]]]): A list of fields
73
+ or tuples of fields for which regular (non-unique) indexes
74
+ should be created. Indexes improve query performance on these
75
+ fields.
76
+ unique_indexes (ClassVar[list[Union[str, tuple[str]]]]): A list of
77
+ fields or tuples of fields for which unique indexes should be
78
+ created. Unique indexes enforce that all values in these fields
79
+ are distinct across the table.
47
80
  """
48
81
 
49
82
  table_name: Optional[str] = (
50
83
  None # Table name, defaults to class name if not set
51
84
  )
85
+ indexes: ClassVar[list[Union[str, tuple[str]]]] = []
86
+ unique_indexes: ClassVar[list[Union[str, tuple[str]]]] = []
52
87
 
53
88
  @classmethod
54
89
  def model_validate_partial(cls: type[T], obj: dict[str, Any]) -> T:
@@ -132,3 +167,50 @@ class BaseDBModel(BaseModel):
132
167
  def should_create_pk(cls) -> bool:
133
168
  """Returns True since the primary key is always created."""
134
169
  return True
170
+
171
+ @classmethod
172
+ def serialize_field(cls, value: SerializableField) -> SerializableField:
173
+ """Serialize datetime or date fields to Unix timestamp.
174
+
175
+ Args:
176
+ field_name: The name of the field.
177
+ value: The value of the field.
178
+
179
+ Returns:
180
+ An integer Unix timestamp if the field is a datetime or date.
181
+ """
182
+ if isinstance(value, (datetime.datetime, datetime.date)):
183
+ return to_unix_timestamp(value)
184
+ return value # Return value as-is for non-datetime fields
185
+
186
+ # Deserialization after fetching from the database
187
+
188
+ @classmethod
189
+ def deserialize_field(
190
+ cls,
191
+ field_name: str,
192
+ value: SerializableField,
193
+ *,
194
+ return_local_time: bool,
195
+ ) -> object:
196
+ """Deserialize fields from Unix timestamp to datetime or date.
197
+
198
+ Args:
199
+ field_name: The name of the field being deserialized.
200
+ value: The Unix timestamp value fetched from the database.
201
+ return_local_time: Flag to control whether the datetime is localized
202
+ to the user's timezone.
203
+
204
+ Returns:
205
+ A datetime or date object if the field type is datetime or date,
206
+ otherwise returns the value as-is.
207
+ """
208
+ field_type = cls.__annotations__.get(field_name)
209
+
210
+ if field_type in (datetime.datetime, datetime.date) and isinstance(
211
+ value, int
212
+ ):
213
+ return from_unix_timestamp(
214
+ value, field_type, localize=return_local_time
215
+ )
216
+ return value # Return value as-is for non-datetime fields
@@ -0,0 +1,19 @@
1
+ """Define a custom field type for unique constraints in SQLiter."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic.fields import FieldInfo
6
+
7
+
8
+ class Unique(FieldInfo):
9
+ """A custom field type for unique constraints in SQLiter."""
10
+
11
+ def __init__(self, default: Any = ..., **kwargs: Any) -> None: # noqa: ANN401
12
+ """Initialize a Unique field.
13
+
14
+ Args:
15
+ default: The default value for the field.
16
+ **kwargs: Additional keyword arguments to pass to FieldInfo.
17
+ """
18
+ super().__init__(default=default, **kwargs)
19
+ self.unique = True
sqliter/query/query.py CHANGED
@@ -35,7 +35,7 @@ if TYPE_CHECKING: # pragma: no cover
35
35
  from pydantic.fields import FieldInfo
36
36
 
37
37
  from sqliter import SqliterDB
38
- from sqliter.model import BaseDBModel
38
+ from sqliter.model import BaseDBModel, SerializableField
39
39
 
40
40
  # Define a type alias for the possible value types
41
41
  FilterValue = Union[
@@ -609,14 +609,32 @@ class QueryBuilder:
609
609
  An instance of the model class populated with the row data.
610
610
  """
611
611
  if self._fields:
612
- return self.model_class.model_validate_partial(
613
- {field: row[idx] for idx, field in enumerate(self._fields)}
614
- )
615
- return self.model_class(
616
- **{
617
- field: row[idx]
618
- for idx, field in enumerate(self.model_class.model_fields)
612
+ data = {
613
+ field: self._deserialize(field, row[idx])
614
+ for idx, field in enumerate(self._fields)
619
615
  }
616
+ return self.model_class.model_validate_partial(data)
617
+
618
+ data = {
619
+ field: self._deserialize(field, row[idx])
620
+ for idx, field in enumerate(self.model_class.model_fields)
621
+ }
622
+ return self.model_class(**data)
623
+
624
+ def _deserialize(
625
+ self, field_name: str, value: SerializableField
626
+ ) -> SerializableField:
627
+ """Deserialize a field value if needed.
628
+
629
+ Args:
630
+ field_name: Name of the field being deserialized.
631
+ value: Value from the database.
632
+
633
+ Returns:
634
+ The deserialized value.
635
+ """
636
+ return self.model_class.deserialize_field(
637
+ field_name, value, return_local_time=self.db.return_local_time
620
638
  )
621
639
 
622
640
  @overload
sqliter/sqliter.py CHANGED
@@ -10,12 +10,14 @@ from __future__ import annotations
10
10
 
11
11
  import logging
12
12
  import sqlite3
13
- from typing import TYPE_CHECKING, Any, Optional, TypeVar
13
+ import time
14
+ from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
14
15
 
15
16
  from typing_extensions import Self
16
17
 
17
18
  from sqliter.exceptions import (
18
19
  DatabaseConnectionError,
20
+ InvalidIndexError,
19
21
  RecordDeletionError,
20
22
  RecordFetchError,
21
23
  RecordInsertionError,
@@ -26,6 +28,7 @@ from sqliter.exceptions import (
26
28
  TableDeletionError,
27
29
  )
28
30
  from sqliter.helpers import infer_sqlite_type
31
+ from sqliter.model.unique import Unique
29
32
  from sqliter.query.query import QueryBuilder
30
33
 
31
34
  if TYPE_CHECKING: # pragma: no cover
@@ -49,7 +52,9 @@ class SqliterDB:
49
52
  logger (Optional[logging.Logger]): Custom logger for debug output.
50
53
  """
51
54
 
52
- def __init__(
55
+ MEMORY_DB = ":memory:"
56
+
57
+ def __init__( # noqa: PLR0913
53
58
  self,
54
59
  db_filename: Optional[str] = None,
55
60
  *,
@@ -58,6 +63,7 @@ class SqliterDB:
58
63
  debug: bool = False,
59
64
  logger: Optional[logging.Logger] = None,
60
65
  reset: bool = False,
66
+ return_local_time: bool = True,
61
67
  ) -> None:
62
68
  """Initialize a new SqliterDB instance.
63
69
 
@@ -69,12 +75,13 @@ class SqliterDB:
69
75
  logger: Custom logger for debug output.
70
76
  reset: Whether to reset the database on initialization. This will
71
77
  basically drop all existing tables.
78
+ return_local_time: Whether to return local time for datetime fields.
72
79
 
73
80
  Raises:
74
81
  ValueError: If no filename is provided for a non-memory database.
75
82
  """
76
83
  if memory:
77
- self.db_filename = ":memory:"
84
+ self.db_filename = self.MEMORY_DB
78
85
  elif db_filename:
79
86
  self.db_filename = db_filename
80
87
  else:
@@ -88,6 +95,9 @@ class SqliterDB:
88
95
  self.logger = logger
89
96
  self.conn: Optional[sqlite3.Connection] = None
90
97
  self.reset = reset
98
+ self.return_local_time = return_local_time
99
+
100
+ self._in_transaction = False
91
101
 
92
102
  if self.debug:
93
103
  self._setup_logger()
@@ -95,6 +105,54 @@ class SqliterDB:
95
105
  if self.reset:
96
106
  self._reset_database()
97
107
 
108
+ @property
109
+ def filename(self) -> Optional[str]:
110
+ """Returns the filename of the current database or None if in-memory."""
111
+ return None if self.db_filename == self.MEMORY_DB else self.db_filename
112
+
113
+ @property
114
+ def is_memory(self) -> bool:
115
+ """Returns True if the database is in-memory."""
116
+ return self.db_filename == self.MEMORY_DB
117
+
118
+ @property
119
+ def is_autocommit(self) -> bool:
120
+ """Returns True if auto-commit is enabled."""
121
+ return self.auto_commit
122
+
123
+ @property
124
+ def is_connected(self) -> bool:
125
+ """Returns True if the database is connected, False otherwise."""
126
+ return self.conn is not None
127
+
128
+ @property
129
+ def table_names(self) -> list[str]:
130
+ """Returns a list of all table names in the database.
131
+
132
+ Temporarily connects to the database if not connected and restores
133
+ the connection state afterward.
134
+ """
135
+ was_connected = self.is_connected
136
+ if not was_connected:
137
+ self.connect()
138
+
139
+ if self.conn is None:
140
+ err_msg = "Failed to establish a database connection."
141
+ raise DatabaseConnectionError(err_msg)
142
+
143
+ cursor = self.conn.cursor()
144
+ cursor.execute(
145
+ "SELECT name FROM sqlite_master WHERE type='table' "
146
+ "AND name NOT LIKE 'sqlite_%';"
147
+ )
148
+ tables = [row[0] for row in cursor.fetchall()]
149
+
150
+ # Restore the connection state
151
+ if not was_connected:
152
+ self.close()
153
+
154
+ return tables
155
+
98
156
  def _reset_database(self) -> None:
99
157
  """Drop all user-created tables in the database."""
100
158
  with self.connect() as conn:
@@ -236,7 +294,12 @@ class SqliterDB:
236
294
  for field_name, field_info in model_class.model_fields.items():
237
295
  if field_name != primary_key:
238
296
  sqlite_type = infer_sqlite_type(field_info.annotation)
239
- fields.append(f"{field_name} {sqlite_type}")
297
+ unique_constraint = (
298
+ "UNIQUE" if isinstance(field_info, Unique) else ""
299
+ )
300
+ fields.append(
301
+ f"{field_name} {sqlite_type} {unique_constraint}".strip()
302
+ )
240
303
 
241
304
  create_str = (
242
305
  "CREATE TABLE IF NOT EXISTS" if exists_ok else "CREATE TABLE"
@@ -259,6 +322,65 @@ class SqliterDB:
259
322
  except sqlite3.Error as exc:
260
323
  raise TableCreationError(table_name) from exc
261
324
 
325
+ # Create regular indexes
326
+ if hasattr(model_class.Meta, "indexes"):
327
+ self._create_indexes(
328
+ model_class, model_class.Meta.indexes, unique=False
329
+ )
330
+
331
+ # Create unique indexes
332
+ if hasattr(model_class.Meta, "unique_indexes"):
333
+ self._create_indexes(
334
+ model_class, model_class.Meta.unique_indexes, unique=True
335
+ )
336
+
337
+ def _create_indexes(
338
+ self,
339
+ model_class: type[BaseDBModel],
340
+ indexes: list[Union[str, tuple[str]]],
341
+ *,
342
+ unique: bool = False,
343
+ ) -> None:
344
+ """Helper method to create regular or unique indexes.
345
+
346
+ Args:
347
+ model_class: The model class defining the table.
348
+ indexes: List of fields or tuples of fields to create indexes for.
349
+ unique: If True, creates UNIQUE indexes; otherwise, creates regular
350
+ indexes.
351
+
352
+ Raises:
353
+ InvalidIndexError: If any fields specified for indexing do not exist
354
+ in the model.
355
+ """
356
+ valid_fields = set(
357
+ model_class.model_fields.keys()
358
+ ) # Get valid fields from the model
359
+
360
+ for index in indexes:
361
+ # Handle multiple fields in tuple form
362
+ fields = list(index) if isinstance(index, tuple) else [index]
363
+
364
+ # Check if all fields exist in the model
365
+ invalid_fields = [
366
+ field for field in fields if field not in valid_fields
367
+ ]
368
+ if invalid_fields:
369
+ raise InvalidIndexError(invalid_fields, model_class.__name__)
370
+
371
+ # Build the SQL string
372
+ index_name = "_".join(fields)
373
+ index_postfix = "_unique" if unique else ""
374
+ index_type = " UNIQUE " if unique else " "
375
+
376
+ create_index_sql = (
377
+ f"CREATE{index_type}INDEX IF NOT EXISTS "
378
+ f"idx_{model_class.get_table_name()}"
379
+ f"_{index_name}{index_postfix} "
380
+ f"ON {model_class.get_table_name()} ({', '.join(fields)})"
381
+ )
382
+ self._execute_sql(create_index_sql)
383
+
262
384
  def _execute_sql(self, sql: str) -> None:
263
385
  """Execute an SQL statement.
264
386
 
@@ -308,14 +430,21 @@ class SqliterDB:
308
430
  This method is called after operations that modify the database,
309
431
  committing changes only if auto_commit is set to True.
310
432
  """
311
- if self.auto_commit and self.conn:
433
+ if not self._in_transaction and self.auto_commit and self.conn:
312
434
  self.conn.commit()
313
435
 
314
- def insert(self, model_instance: T) -> T:
436
+ def insert(
437
+ self, model_instance: T, *, timestamp_override: bool = False
438
+ ) -> T:
315
439
  """Insert a new record into the database.
316
440
 
317
441
  Args:
318
442
  model_instance: The instance of the model class to insert.
443
+ timestamp_override: If True, override the created_at and updated_at
444
+ timestamps with provided values. Default is False. If the values
445
+ are not provided, they will be set to the current time as
446
+ normal. Without this flag, the timestamps will always be set to
447
+ the current time, even if provided.
319
448
 
320
449
  Returns:
321
450
  The updated model instance with the primary key (pk) set.
@@ -326,8 +455,28 @@ class SqliterDB:
326
455
  model_class = type(model_instance)
327
456
  table_name = model_class.get_table_name()
328
457
 
458
+ # Always set created_at and updated_at timestamps
459
+ current_timestamp = int(time.time())
460
+
461
+ # Handle the case where timestamp_override is False
462
+ if not timestamp_override:
463
+ # Always override both timestamps with the current time
464
+ model_instance.created_at = current_timestamp
465
+ model_instance.updated_at = current_timestamp
466
+ else:
467
+ # Respect provided values, but set to current time if they are 0
468
+ if model_instance.created_at == 0:
469
+ model_instance.created_at = current_timestamp
470
+ if model_instance.updated_at == 0:
471
+ model_instance.updated_at = current_timestamp
472
+
329
473
  # Get the data from the model
330
474
  data = model_instance.model_dump()
475
+
476
+ # Serialize the data
477
+ for field_name, value in list(data.items()):
478
+ data[field_name] = model_instance.serialize_field(value)
479
+
331
480
  # remove the primary key field if it exists, otherwise we'll get
332
481
  # TypeErrors as multiple primary keys will exist
333
482
  if data.get("pk", None) == 0:
@@ -405,24 +554,27 @@ class SqliterDB:
405
554
 
406
555
  Raises:
407
556
  RecordUpdateError: If there's an error updating the record or if it
408
- is not found.
557
+ is not found.
409
558
  """
410
559
  model_class = type(model_instance)
411
560
  table_name = model_class.get_table_name()
412
-
413
561
  primary_key = model_class.get_primary_key()
414
562
 
415
- fields = ", ".join(
416
- f"{field} = ?"
417
- for field in model_class.model_fields
418
- if field != primary_key
419
- )
420
- values = tuple(
421
- getattr(model_instance, field)
422
- for field in model_class.model_fields
423
- if field != primary_key
424
- )
425
- primary_key_value = getattr(model_instance, primary_key)
563
+ # Set updated_at timestamp
564
+ current_timestamp = int(time.time())
565
+ model_instance.updated_at = current_timestamp
566
+
567
+ # Get the data and serialize any datetime/date fields
568
+ data = model_instance.model_dump()
569
+ for field_name, value in list(data.items()):
570
+ data[field_name] = model_instance.serialize_field(value)
571
+
572
+ # Remove the primary key from the update data
573
+ primary_key_value = data.pop(primary_key)
574
+
575
+ # Create the SQL using the processed data
576
+ fields = ", ".join(f"{field} = ?" for field in data)
577
+ values = tuple(data.values())
426
578
 
427
579
  update_sql = f"""
428
580
  UPDATE {table_name}
@@ -515,6 +667,7 @@ class SqliterDB:
515
667
 
516
668
  """
517
669
  self.connect()
670
+ self._in_transaction = True
518
671
  return self
519
672
 
520
673
  def __exit__(
@@ -552,3 +705,4 @@ class SqliterDB:
552
705
  # Close the connection and reset the instance variable
553
706
  self.conn.close()
554
707
  self.conn = None
708
+ self._in_transaction = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sqliter-py
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Interact with SQLite databases using Python and Pydantic
5
5
  Project-URL: Pull Requests, https://github.com/seapagan/sqliter-py/pulls
6
6
  Project-URL: Bug Tracker, https://github.com/seapagan/sqliter-py/issues
@@ -60,8 +60,7 @@ Website](https://sqliter.grantramsay.dev)
60
60
  > Also, structures like `list`, `dict`, `set` etc are not supported **at this
61
61
  > time** as field types, since SQLite does not have a native column type for
62
62
  > these. This is the **next planned enhancement**. These will need to be
63
- > `pickled` first then stored as a BLOB in the database . Also support `date`
64
- > which can be stored as a Unix timestamp in an integer field.
63
+ > `pickled` first then stored as a BLOB in the database.
65
64
  >
66
65
  > See the [TODO](TODO.md) for planned features and improvements.
67
66
 
@@ -75,6 +74,10 @@ Website](https://sqliter.grantramsay.dev)
75
74
  ## Features
76
75
 
77
76
  - Table creation based on Pydantic models
77
+ - Supports `date` and `datetime` fields. List/Dict/Set fields are planned.
78
+ - Automatic primary key generation
79
+ - User defined indexes on any field
80
+ - Set any field as UNIQUE
78
81
  - CRUD operations (Create, Read, Update, Delete)
79
82
  - Chained Query building with filtering, ordering, and pagination
80
83
  - Transaction support
@@ -98,16 +101,16 @@ virtual environments (`uv` is used for developing this project and in the CI):
98
101
  uv add sqliter-py
99
102
  ```
100
103
 
101
- With `pip`:
104
+ With `Poetry`:
102
105
 
103
106
  ```bash
104
- pip install sqliter-py
107
+ poetry add sqliter-py
105
108
  ```
106
109
 
107
- Or with `Poetry`:
110
+ Or with `pip`:
108
111
 
109
112
  ```bash
110
- poetry add sqliter-py
113
+ pip install sqliter-py
111
114
  ```
112
115
 
113
116
  ### Optional Dependencies
@@ -116,9 +119,9 @@ Currently by default, the only external dependency is Pydantic. However, there
116
119
  are some optional dependencies that can be installed to enable additional
117
120
  features:
118
121
 
119
- - `inflect`: For pluralizing table names (if not specified). This just offers a
120
- more-advanced pluralization than the default method used. In most cases you
121
- will not need this.
122
+ - `inflect`: For pluralizing the auto-generated table names (if not explicitly
123
+ set in the Model) This just offers a more-advanced pluralization than the
124
+ default method used. In most cases you will not need this.
122
125
 
123
126
  See [Installing Optional
124
127
  Dependencies](https://sqliter.grantramsay.dev/installation#optional-dependencies)
@@ -0,0 +1,14 @@
1
+ sqliter/__init__.py,sha256=ECfn02OPmiMCQvRYbfizKFhVDk00xV-HV1s2cH9037I,244
2
+ sqliter/constants.py,sha256=clai8tiF6g9raTKUvEcBA5srmLePa6GFX7zC4YQ_6Zo,1186
3
+ sqliter/exceptions.py,sha256=g-YZaPazBxlYxQ0lVP_MCWC-mQlX_qgyWip5LJCVLV4,5654
4
+ sqliter/helpers.py,sha256=d6k4oWl43Se_c8rAmX8Zj0_sf2O-bpaaeu33VN2IcsI,3442
5
+ sqliter/sqliter.py,sha256=Cvko-UqmdXbZqkVTglBlVAKUyPcDyiuiOjeNY6-_2EI,24340
6
+ sqliter/model/__init__.py,sha256=MblCLIdoUwvRQmTALxXTadW54vH8SyOShonkzktbH5A,396
7
+ sqliter/model/model.py,sha256=yc-fbiDa2swNd5yl1x-QP0vqeRTCPMpfZbIA6yG8k7M,7393
8
+ sqliter/model/unique.py,sha256=r4CXrW1GXB6OgNoOVDTpTEVZl8_zta3mYbbT5quMc-c,578
9
+ sqliter/query/__init__.py,sha256=MRajhjTPJqjbmmrwndVKj8vqMbK5-XufpwoIswQf5z4,239
10
+ sqliter/query/query.py,sha256=GL3Wh7jQY5n-X1oacVoHIFBTUa-zQJwzx8TM_ZEeuVE,24657
11
+ sqliter_py-0.7.0.dist-info/METADATA,sha256=ApjziCgXFt-N3KC_ogS1KuRV7_YRjoV2Plxv1HBBPzA,7395
12
+ sqliter_py-0.7.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
13
+ sqliter_py-0.7.0.dist-info/licenses/LICENSE.txt,sha256=-r4mvgoEWzkl1hPO5k8I_iMwJate7zDj8p_Fmn7dhVg,1078
14
+ sqliter_py-0.7.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- sqliter/__init__.py,sha256=ECfn02OPmiMCQvRYbfizKFhVDk00xV-HV1s2cH9037I,244
2
- sqliter/constants.py,sha256=j0lE2cB1Uj8cNo40GGtCPdOR5asm-dHRDmGG0oyindA,1050
3
- sqliter/exceptions.py,sha256=g6jjcBnU2wnd_Sz90PCARl8PRayhU-D88W0tIUh548A,4808
4
- sqliter/helpers.py,sha256=75r4zMmGztVPm9_Bz3L1cSvBdx17uPEAnaggVhD70Pg,1138
5
- sqliter/sqliter.py,sha256=XLMIZZfMdRFv7iyGLH6N9uHE_0bAIxgvxgLxADYAjv8,18545
6
- sqliter/model/__init__.py,sha256=GgULmKRn0Dq0Jz6LbHGPndS6GP3vd1uxI1KrEevofLs,237
7
- sqliter/model/model.py,sha256=1aLe0QX5HEovOb9U6F9i_-fs4JqpPpewafd8KWINAkQ,4595
8
- sqliter/query/__init__.py,sha256=MRajhjTPJqjbmmrwndVKj8vqMbK5-XufpwoIswQf5z4,239
9
- sqliter/query/query.py,sha256=g9MbF3AGqNNha7QStxNv1qtIXv0A9FUgerur_0qhu8U,24081
10
- sqliter_py-0.5.0.dist-info/METADATA,sha256=-kRDsXK1GrxUoL3B9uNEMwnsNUwhGsUjYGuNsjSxZ2g,7271
11
- sqliter_py-0.5.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
12
- sqliter_py-0.5.0.dist-info/licenses/LICENSE.txt,sha256=-r4mvgoEWzkl1hPO5k8I_iMwJate7zDj8p_Fmn7dhVg,1078
13
- sqliter_py-0.5.0.dist-info/RECORD,,