sqlmodel-object-helpers 0.0.6__tar.gz → 0.0.7__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.
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/PKG-INFO +1 -1
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/__init__.py +1 -1
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/query.py +17 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/types/filters.py +32 -3
- sqlmodel_object_helpers-0.0.7/tests/test_data_error_handling.py +153 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_datetime_range.py +28 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_types.py +17 -4
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/.github/workflows/publish.yml +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/.gitignore +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/LICENSE +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/README.md +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/pyproject.toml +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/constants.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/dynamic_meta.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/exceptions.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/filters.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/loaders.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/operators.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/session.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/standalone.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/types/__init__.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/types/columns.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/types/datetime.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/types/pagination.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/types/projections.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/conftest.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_bulk_mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_column_meta.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_computed_columns.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_count_exists.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_dynamic_meta.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_exceptions.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_filters.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_for_update.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_generated_columns_pg.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_loaders.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_operators.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_query.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_settings.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_standalone.py +0 -0
- {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_time_filter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-object-helpers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: Generic async query helpers for SQLModel: filtering, eager loading, pagination
|
|
5
5
|
Project-URL: Homepage, https://github.com/itstandart/sqlmodel-object-helpers
|
|
6
6
|
Project-URL: Repository, https://github.com/itstandart/sqlmodel-object-helpers
|
{sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/src/sqlmodel_object_helpers/query.py
RENAMED
|
@@ -4,6 +4,7 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
from sqlalchemy import exists as sa_exists
|
|
7
|
+
from sqlalchemy.exc import DataError as SADataError
|
|
7
8
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
9
|
from sqlalchemy.sql import func
|
|
9
10
|
from sqlmodel import SQLModel, and_, or_, select
|
|
@@ -398,6 +399,10 @@ async def get_objects[T: SQLModel](
|
|
|
398
399
|
if not suspend_error:
|
|
399
400
|
raise
|
|
400
401
|
return []
|
|
402
|
+
except SADataError as e:
|
|
403
|
+
if not suspend_error:
|
|
404
|
+
raise InvalidFilterError(str(e)) from e
|
|
405
|
+
return []
|
|
401
406
|
except Exception as e:
|
|
402
407
|
if not suspend_error:
|
|
403
408
|
raise DatabaseError(str(e)) from e
|
|
@@ -467,6 +472,10 @@ async def count_objects[T: SQLModel](
|
|
|
467
472
|
if not suspend_error:
|
|
468
473
|
raise
|
|
469
474
|
return 0
|
|
475
|
+
except SADataError as e:
|
|
476
|
+
if not suspend_error:
|
|
477
|
+
raise InvalidFilterError(str(e)) from e
|
|
478
|
+
return 0
|
|
470
479
|
except Exception as e:
|
|
471
480
|
if not suspend_error:
|
|
472
481
|
raise DatabaseError(str(e)) from e
|
|
@@ -529,6 +538,10 @@ async def exists_object[T: SQLModel](
|
|
|
529
538
|
if not suspend_error:
|
|
530
539
|
raise
|
|
531
540
|
return False
|
|
541
|
+
except SADataError as e:
|
|
542
|
+
if not suspend_error:
|
|
543
|
+
raise InvalidFilterError(str(e)) from e
|
|
544
|
+
return False
|
|
532
545
|
except Exception as e:
|
|
533
546
|
if not suspend_error:
|
|
534
547
|
raise DatabaseError(str(e)) from e
|
|
@@ -721,6 +734,10 @@ async def get_projection[T: SQLModel](
|
|
|
721
734
|
if suspend_error:
|
|
722
735
|
return []
|
|
723
736
|
raise
|
|
737
|
+
except SADataError as e:
|
|
738
|
+
if suspend_error:
|
|
739
|
+
return []
|
|
740
|
+
raise InvalidFilterError(str(e)) from e
|
|
724
741
|
except Exception as e:
|
|
725
742
|
if suspend_error:
|
|
726
743
|
return []
|
|
@@ -103,6 +103,12 @@ class FilterDate(BaseModel):
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
class FilterNaiveDatetime(BaseModel):
|
|
106
|
+
"""Filter for TIMESTAMP WITHOUT TIME ZONE columns.
|
|
107
|
+
|
|
108
|
+
Timezone-aware input is **rejected** (raises ``ValueError`` → HTTP 422)
|
|
109
|
+
because silently stripping timezone info hides conversion bugs on the caller side.
|
|
110
|
+
"""
|
|
111
|
+
|
|
106
112
|
eq: datetime | None = None
|
|
107
113
|
ne: datetime | None = None
|
|
108
114
|
gt: datetime | None = None
|
|
@@ -110,6 +116,17 @@ class FilterNaiveDatetime(BaseModel):
|
|
|
110
116
|
ge: datetime | None = None
|
|
111
117
|
le: datetime | None = None
|
|
112
118
|
|
|
119
|
+
@field_validator("eq", "ne", "gt", "lt", "ge", "le", mode="after")
|
|
120
|
+
@classmethod
|
|
121
|
+
def reject_timezone(cls, v: datetime | None) -> datetime | None:
|
|
122
|
+
if v is not None and v.tzinfo is not None:
|
|
123
|
+
msg = (
|
|
124
|
+
"Timezone-aware datetime is not allowed for naive datetime filters. "
|
|
125
|
+
"Send a naive datetime (without timezone offset), e.g. '2026-03-19T00:00:00'"
|
|
126
|
+
)
|
|
127
|
+
raise ValueError(msg)
|
|
128
|
+
return v
|
|
129
|
+
|
|
113
130
|
|
|
114
131
|
class FilterDatetime(BaseModel):
|
|
115
132
|
eq: UTCDatetime | None = None
|
|
@@ -207,7 +224,8 @@ class FilterNaiveDatetimeRange(BaseModel):
|
|
|
207
224
|
"""
|
|
208
225
|
Same as :class:`FilterDatetimeRange` but produces **naive** (timezone-unaware) datetimes.
|
|
209
226
|
|
|
210
|
-
|
|
227
|
+
Timezone-aware input is **rejected** (raises ``ValueError`` → HTTP 422)
|
|
228
|
+
because silently stripping timezone info hides conversion bugs on the caller side.
|
|
211
229
|
"""
|
|
212
230
|
|
|
213
231
|
gt: datetime | None = None
|
|
@@ -220,6 +238,17 @@ class FilterNaiveDatetimeRange(BaseModel):
|
|
|
220
238
|
return cls._parse_range(data)
|
|
221
239
|
return data
|
|
222
240
|
|
|
241
|
+
@field_validator("gt", "lt", mode="after")
|
|
242
|
+
@classmethod
|
|
243
|
+
def reject_timezone(cls, v: datetime | None) -> datetime | None:
|
|
244
|
+
if v is not None and v.tzinfo is not None:
|
|
245
|
+
msg = (
|
|
246
|
+
"Timezone-aware datetime is not allowed for naive datetime range filters. "
|
|
247
|
+
"Send a naive datetime (without timezone offset), e.g. '2026-03-19T00:00:00'"
|
|
248
|
+
)
|
|
249
|
+
raise ValueError(msg)
|
|
250
|
+
return v
|
|
251
|
+
|
|
223
252
|
@staticmethod
|
|
224
253
|
def _parse_range(range_str: str) -> dict[str, datetime]:
|
|
225
254
|
range_str = range_str.strip()
|
|
@@ -231,9 +260,9 @@ class FilterNaiveDatetimeRange(BaseModel):
|
|
|
231
260
|
result: dict[str, datetime] = {}
|
|
232
261
|
|
|
233
262
|
if left:
|
|
234
|
-
result["gt"] = _parse_range_part(left)
|
|
263
|
+
result["gt"] = _parse_range_part(left)
|
|
235
264
|
if right:
|
|
236
|
-
result["lt"] = _parse_range_part(right)
|
|
265
|
+
result["lt"] = _parse_range_part(right)
|
|
237
266
|
|
|
238
267
|
return result
|
|
239
268
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for SADataError → InvalidFilterError handling in query.py.
|
|
3
|
+
|
|
4
|
+
SQLite does not raise sqlalchemy.exc.DataError, so we mock session methods
|
|
5
|
+
to simulate the asyncpg DataError that occurs when e.g. a timezone-aware
|
|
6
|
+
datetime is sent to a TIMESTAMP WITHOUT TIME ZONE column.
|
|
7
|
+
|
|
8
|
+
Covers all query entry points: get_objects, count_objects, exists_object, get_projection.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from unittest.mock import AsyncMock, patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from sqlalchemy.exc import DataError as SADataError
|
|
15
|
+
|
|
16
|
+
import sqlmodel_object_helpers as soh
|
|
17
|
+
|
|
18
|
+
from conftest import Applicant
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
pytestmark = pytest.mark.asyncio
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _make_sa_data_error() -> SADataError:
|
|
25
|
+
"""Build a realistic SADataError matching the asyncpg timezone mismatch."""
|
|
26
|
+
return SADataError(
|
|
27
|
+
statement="SELECT ... WHERE gp_payment_date = $1::TIMESTAMP WITHOUT TIME ZONE",
|
|
28
|
+
params=(None,),
|
|
29
|
+
orig=Exception(
|
|
30
|
+
"can't subtract offset-naive and offset-aware datetimes"
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# get_objects — SADataError → InvalidFilterError(400)
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def test_get_objects_data_error_raises_invalid_filter(session):
|
|
41
|
+
"""SADataError from DB must be re-raised as InvalidFilterError, not DatabaseError."""
|
|
42
|
+
with patch.object(session, "execute", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
43
|
+
with pytest.raises(soh.InvalidFilterError) as exc_info:
|
|
44
|
+
await soh.get_objects(session, Applicant)
|
|
45
|
+
|
|
46
|
+
assert exc_info.value.status_code == 400
|
|
47
|
+
assert "offset-naive and offset-aware" in exc_info.value.message
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def test_get_objects_data_error_suspend_returns_empty(session):
|
|
51
|
+
"""SADataError with suspend_error=True returns empty list, not raises."""
|
|
52
|
+
with patch.object(session, "execute", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
53
|
+
result = await soh.get_objects(session, Applicant, suspend_error=True)
|
|
54
|
+
|
|
55
|
+
assert result == []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def test_get_objects_data_error_not_database_error(session):
|
|
59
|
+
"""SADataError must NOT become DatabaseError(500)."""
|
|
60
|
+
with patch.object(session, "execute", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
61
|
+
with pytest.raises(soh.InvalidFilterError):
|
|
62
|
+
await soh.get_objects(session, Applicant)
|
|
63
|
+
|
|
64
|
+
# Verify it's NOT DatabaseError
|
|
65
|
+
try:
|
|
66
|
+
await soh.get_objects(session, Applicant)
|
|
67
|
+
except soh.DatabaseError:
|
|
68
|
+
pytest.fail("SADataError was incorrectly wrapped as DatabaseError(500)")
|
|
69
|
+
except soh.InvalidFilterError:
|
|
70
|
+
pass # correct behavior
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# count_objects — SADataError → InvalidFilterError(400)
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def test_count_objects_data_error_raises_invalid_filter(session):
|
|
79
|
+
"""SADataError in count_objects must raise InvalidFilterError."""
|
|
80
|
+
with patch.object(session, "scalar", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
81
|
+
with pytest.raises(soh.InvalidFilterError) as exc_info:
|
|
82
|
+
await soh.count_objects(session, Applicant)
|
|
83
|
+
|
|
84
|
+
assert exc_info.value.status_code == 400
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def test_count_objects_data_error_suspend_returns_zero(session):
|
|
88
|
+
"""SADataError in count_objects with suspend_error=True returns 0."""
|
|
89
|
+
with patch.object(session, "scalar", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
90
|
+
result = await soh.count_objects(session, Applicant, suspend_error=True)
|
|
91
|
+
|
|
92
|
+
assert result == 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# =============================================================================
|
|
96
|
+
# exists_object — SADataError → InvalidFilterError(400)
|
|
97
|
+
# =============================================================================
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def test_exists_object_data_error_raises_invalid_filter(session):
|
|
101
|
+
"""SADataError in exists_object must raise InvalidFilterError."""
|
|
102
|
+
with patch.object(session, "scalar", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
103
|
+
with pytest.raises(soh.InvalidFilterError) as exc_info:
|
|
104
|
+
await soh.exists_object(session, Applicant)
|
|
105
|
+
|
|
106
|
+
assert exc_info.value.status_code == 400
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def test_exists_object_data_error_suspend_returns_false(session):
|
|
110
|
+
"""SADataError in exists_object with suspend_error=True returns False."""
|
|
111
|
+
with patch.object(session, "scalar", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
112
|
+
result = await soh.exists_object(session, Applicant, suspend_error=True)
|
|
113
|
+
|
|
114
|
+
assert result is False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# =============================================================================
|
|
118
|
+
# get_projection — SADataError → InvalidFilterError(400)
|
|
119
|
+
# =============================================================================
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def test_get_projection_data_error_raises_invalid_filter(session):
|
|
123
|
+
"""SADataError in get_projection must raise InvalidFilterError."""
|
|
124
|
+
with patch.object(session, "execute", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
125
|
+
with pytest.raises(soh.InvalidFilterError) as exc_info:
|
|
126
|
+
await soh.get_projection(session, Applicant, columns=["id", "last_name"])
|
|
127
|
+
|
|
128
|
+
assert exc_info.value.status_code == 400
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def test_get_projection_data_error_suspend_returns_empty(session):
|
|
132
|
+
"""SADataError in get_projection with suspend_error=True returns []."""
|
|
133
|
+
with patch.object(session, "execute", new_callable=AsyncMock, side_effect=_make_sa_data_error()):
|
|
134
|
+
result = await soh.get_projection(
|
|
135
|
+
session, Applicant, columns=["id", "last_name"], suspend_error=True,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
assert result == []
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# =============================================================================
|
|
142
|
+
# Contrast: non-DataError exceptions still become DatabaseError(500)
|
|
143
|
+
# =============================================================================
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def test_generic_exception_still_raises_database_error(session):
|
|
147
|
+
"""Non-DataError exceptions must still become DatabaseError(500)."""
|
|
148
|
+
with patch.object(session, "execute", new_callable=AsyncMock, side_effect=RuntimeError("connection lost")):
|
|
149
|
+
with pytest.raises(soh.DatabaseError) as exc_info:
|
|
150
|
+
await soh.get_objects(session, Applicant)
|
|
151
|
+
|
|
152
|
+
assert exc_info.value.status_code == 500
|
|
153
|
+
assert "connection lost" in exc_info.value.message
|
{sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_datetime_range.py
RENAMED
|
@@ -293,3 +293,31 @@ class TestFilterNaiveDatetimeRange:
|
|
|
293
293
|
assert f.lt == datetime(2026, 2, 1)
|
|
294
294
|
assert f.gt.tzinfo is None
|
|
295
295
|
assert f.lt.tzinfo is None
|
|
296
|
+
|
|
297
|
+
def test_rejects_tz_in_dict_input(self):
|
|
298
|
+
"""Dict with timezone-aware ISO strings is rejected (was the 500 bug)."""
|
|
299
|
+
with pytest.raises(ValidationError, match="Timezone-aware datetime is not allowed"):
|
|
300
|
+
soh.FilterNaiveDatetimeRange.model_validate(
|
|
301
|
+
{"gt": "2026-03-20T12:24:00.000+03:00", "lt": "2026-03-20T23:59:59.999+03:00"},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def test_rejects_tz_in_dict_gt_only(self):
|
|
305
|
+
"""Single gt with timezone in dict is rejected."""
|
|
306
|
+
with pytest.raises(ValidationError, match="Timezone-aware datetime is not allowed"):
|
|
307
|
+
soh.FilterNaiveDatetimeRange.model_validate(
|
|
308
|
+
{"gt": "2026-01-01T00:00:00+00:00"},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def test_rejects_tz_in_string_input(self):
|
|
312
|
+
"""String range with timezone offset is rejected."""
|
|
313
|
+
with pytest.raises(ValidationError, match="Timezone-aware datetime is not allowed"):
|
|
314
|
+
soh.FilterNaiveDatetimeRange.model_validate(
|
|
315
|
+
"2026-04-01T03:00:00+03:00,2026-04-02T03:00:00+03:00"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def test_rejects_z_suffix_in_string_input(self):
|
|
319
|
+
"""Z suffix means UTC (timezone-aware) — must be rejected for naive range."""
|
|
320
|
+
with pytest.raises(ValidationError, match="Timezone-aware datetime is not allowed"):
|
|
321
|
+
soh.FilterNaiveDatetimeRange.model_validate(
|
|
322
|
+
"2026-04-01T00:00:00Z,2026-04-02T00:00:00Z"
|
|
323
|
+
)
|
|
@@ -375,11 +375,24 @@ def test_filter_naive_datetime_accepts_iso_string():
|
|
|
375
375
|
assert f.eq == datetime(2026, 6, 15, 10, 30, 0)
|
|
376
376
|
|
|
377
377
|
|
|
378
|
-
def
|
|
379
|
-
"""FilterNaiveDatetime
|
|
378
|
+
def test_filter_naive_datetime_rejects_aware():
|
|
379
|
+
"""FilterNaiveDatetime rejects timezone-aware datetimes with ValidationError."""
|
|
380
380
|
dt = datetime(2026, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
|
|
381
|
-
|
|
382
|
-
|
|
381
|
+
with pytest.raises(ValidationError, match="Timezone-aware datetime is not allowed"):
|
|
382
|
+
soh.FilterNaiveDatetime(eq=dt)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_filter_naive_datetime_rejects_aware_iso_string():
|
|
386
|
+
"""FilterNaiveDatetime rejects ISO string with timezone offset."""
|
|
387
|
+
with pytest.raises(ValidationError, match="Timezone-aware datetime is not allowed"):
|
|
388
|
+
soh.FilterNaiveDatetime(gt="2026-03-20T12:24:00.000+03:00")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_filter_naive_datetime_rejects_aware_all_operators():
|
|
392
|
+
"""FilterNaiveDatetime rejects timezone-aware input on every operator."""
|
|
393
|
+
for field in ("eq", "ne", "gt", "lt", "ge", "le"):
|
|
394
|
+
with pytest.raises(ValidationError, match="Timezone-aware datetime is not allowed"):
|
|
395
|
+
soh.FilterNaiveDatetime(**{field: "2026-01-01T00:00:00+00:00"})
|
|
383
396
|
|
|
384
397
|
|
|
385
398
|
def test_filter_naive_datetime_all_none():
|
{sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_bulk_mutations.py
RENAMED
|
File without changes
|
|
File without changes
|
{sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_computed_columns.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.7}/tests/test_generated_columns_pg.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|