sqliter-py 0.16.0__py3-none-any.whl → 0.17.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/exceptions.py CHANGED
@@ -209,3 +209,19 @@ class InvalidRelationshipError(SqliterError):
209
209
  "Invalid relationship path '{}': field '{}' is not a valid "
210
210
  "foreign key relationship on model {}"
211
211
  )
212
+
213
+
214
+ class ManyToManyError(SqliterError):
215
+ """Base exception for many-to-many relationship errors."""
216
+
217
+ message_template = "Many-to-many error: {}"
218
+
219
+
220
+ class ManyToManyIntegrityError(ManyToManyError):
221
+ """Raised when a M2M operation fails due to missing context or pk.
222
+
223
+ This error occurs when attempting to use a M2M relationship without
224
+ a database context or on an unsaved instance (no primary key).
225
+ """
226
+
227
+ message_template = "Many-to-many integrity error: {}"
sqliter/helpers.py CHANGED
@@ -9,11 +9,38 @@ to database schema translation.
9
9
  from __future__ import annotations
10
10
 
11
11
  import datetime
12
+ import re
12
13
  from typing import Union
13
14
 
14
15
  from sqliter.constants import SQLITE_TYPE_MAPPING
15
16
 
16
17
 
18
+ def validate_table_name(table_name: str) -> str:
19
+ """Validate that a table name contains only safe characters.
20
+
21
+ Table names must contain only alphanumeric characters and underscores,
22
+ and must start with a letter or underscore. This prevents SQL injection
23
+ through malicious table names.
24
+
25
+ Args:
26
+ table_name: The table name to validate.
27
+
28
+ Returns:
29
+ The validated table name.
30
+
31
+ Raises:
32
+ ValueError: If the table name contains invalid characters.
33
+ """
34
+ if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", table_name):
35
+ msg = (
36
+ f"Invalid table name '{table_name}'. "
37
+ "Table names must start with a letter or underscore and "
38
+ "contain only letters, numbers, and underscores."
39
+ )
40
+ raise ValueError(msg)
41
+ return table_name
42
+
43
+
17
44
  def infer_sqlite_type(field_type: Union[type, None]) -> str:
18
45
  """Infer the SQLite column type based on the Python type.
19
46
 
sqliter/model/model.py CHANGED
@@ -26,7 +26,11 @@ from typing import (
26
26
  from pydantic import BaseModel, ConfigDict, Field
27
27
  from typing_extensions import Self
28
28
 
29
- from sqliter.helpers import from_unix_timestamp, to_unix_timestamp
29
+ from sqliter.helpers import (
30
+ from_unix_timestamp,
31
+ to_unix_timestamp,
32
+ validate_table_name,
33
+ )
30
34
 
31
35
 
32
36
  class SerializableField(Protocol):
@@ -123,32 +127,6 @@ class BaseDBModel(BaseModel):
123
127
 
124
128
  return cast("Self", cls.model_construct(**converted_obj))
125
129
 
126
- @staticmethod
127
- def _validate_table_name(table_name: str) -> str:
128
- """Validate that a table name contains only safe characters.
129
-
130
- Table names must contain only alphanumeric characters and underscores,
131
- and must start with a letter or underscore. This prevents SQL injection
132
- through malicious table names.
133
-
134
- Args:
135
- table_name: The table name to validate.
136
-
137
- Returns:
138
- The validated table name.
139
-
140
- Raises:
141
- ValueError: If the table name contains invalid characters.
142
- """
143
- if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", table_name):
144
- msg = (
145
- f"Invalid table name '{table_name}'. "
146
- "Table names must start with a letter or underscore and "
147
- "contain only letters, numbers, and underscores."
148
- )
149
- raise ValueError(msg)
150
- return table_name
151
-
152
130
  @classmethod
153
131
  def get_table_name(cls) -> str:
154
132
  """Get the database table name for the model.
@@ -171,7 +149,7 @@ class BaseDBModel(BaseModel):
171
149
  table_name: str | None = getattr(cls.Meta, "table_name", None)
172
150
  if table_name is not None:
173
151
  # Validate custom table names
174
- return cls._validate_table_name(table_name)
152
+ return validate_table_name(table_name)
175
153
 
176
154
  # Get class name and remove 'Model' suffix if present
177
155
  class_name = cls.__name__.removesuffix("Model")
@@ -194,7 +172,7 @@ class BaseDBModel(BaseModel):
194
172
  )
195
173
 
196
174
  # Validate auto-generated table names (should always pass)
197
- return cls._validate_table_name(table_name)
175
+ return validate_table_name(table_name)
198
176
 
199
177
  @classmethod
200
178
  def get_primary_key(cls) -> str:
sqliter/orm/__init__.py CHANGED
@@ -10,7 +10,8 @@ Users can choose between modes via import:
10
10
  """
11
11
 
12
12
  from sqliter.orm.foreign_key import ForeignKey
13
+ from sqliter.orm.m2m import ManyToMany
13
14
  from sqliter.orm.model import BaseDBModel
14
15
  from sqliter.orm.registry import ModelRegistry
15
16
 
16
- __all__ = ["BaseDBModel", "ForeignKey", "ModelRegistry"]
17
+ __all__ = ["BaseDBModel", "ForeignKey", "ManyToMany", "ModelRegistry"]