planar 0.10.0__py3-none-any.whl → 0.12.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.
- planar/app.py +26 -6
- planar/cli.py +26 -0
- planar/data/__init__.py +1 -0
- planar/data/config.py +12 -1
- planar/data/connection.py +89 -4
- planar/data/dataset.py +13 -7
- planar/data/utils.py +145 -25
- planar/db/alembic/env.py +68 -57
- planar/db/alembic.ini +1 -1
- planar/files/storage/config.py +7 -1
- planar/routers/dataset_router.py +5 -1
- planar/routers/info.py +79 -36
- planar/scaffold_templates/pyproject.toml.j2 +1 -1
- planar/testing/fixtures.py +7 -4
- planar/testing/planar_test_client.py +8 -0
- planar/version.py +27 -0
- planar-0.12.0.dist-info/METADATA +202 -0
- {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/RECORD +20 -71
- planar/ai/test_agent_serialization.py +0 -229
- planar/ai/test_agent_tool_step_display.py +0 -78
- planar/data/test_dataset.py +0 -358
- planar/files/storage/test_azure_blob.py +0 -435
- planar/files/storage/test_local_directory.py +0 -162
- planar/files/storage/test_s3.py +0 -299
- planar/files/test_files.py +0 -282
- planar/human/test_human.py +0 -385
- planar/logging/test_formatter.py +0 -327
- planar/modeling/mixins/test_auditable.py +0 -97
- planar/modeling/mixins/test_timestamp.py +0 -134
- planar/modeling/mixins/test_uuid_primary_key.py +0 -52
- planar/routers/test_agents_router.py +0 -174
- planar/routers/test_dataset_router.py +0 -429
- planar/routers/test_files_router.py +0 -49
- planar/routers/test_object_config_router.py +0 -367
- planar/routers/test_routes_security.py +0 -168
- planar/routers/test_rule_router.py +0 -470
- planar/routers/test_workflow_router.py +0 -564
- planar/rules/test_data/account_dormancy_management.json +0 -223
- planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
- planar/rules/test_data/applicant_risk_assessment.json +0 -435
- planar/rules/test_data/booking_fraud_detection.json +0 -407
- planar/rules/test_data/cellular_data_rollover_system.json +0 -258
- planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
- planar/rules/test_data/customer_lifetime_value.json +0 -143
- planar/rules/test_data/import_duties_calculator.json +0 -289
- planar/rules/test_data/insurance_prior_authorization.json +0 -443
- planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
- planar/rules/test_data/order_consolidation_system.json +0 -375
- planar/rules/test_data/portfolio_risk_monitor.json +0 -471
- planar/rules/test_data/supply_chain_risk.json +0 -253
- planar/rules/test_data/warehouse_cross_docking.json +0 -237
- planar/rules/test_rules.py +0 -1494
- planar/security/tests/test_auth_middleware.py +0 -162
- planar/security/tests/test_authorization_context.py +0 -78
- planar/security/tests/test_cedar_basics.py +0 -41
- planar/security/tests/test_cedar_policies.py +0 -158
- planar/security/tests/test_jwt_principal_context.py +0 -179
- planar/test_app.py +0 -142
- planar/test_cli.py +0 -394
- planar/test_config.py +0 -515
- planar/test_object_config.py +0 -527
- planar/test_object_registry.py +0 -14
- planar/test_sqlalchemy.py +0 -193
- planar/test_utils.py +0 -105
- planar/testing/test_memory_storage.py +0 -143
- planar/workflows/test_concurrency_detection.py +0 -120
- planar/workflows/test_lock_timeout.py +0 -140
- planar/workflows/test_serialization.py +0 -1203
- planar/workflows/test_suspend_deserialization.py +0 -231
- planar/workflows/test_workflow.py +0 -2005
- planar-0.10.0.dist-info/METADATA +0 -323
- {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/WHEEL +0 -0
- {planar-0.10.0.dist-info → planar-0.12.0.dist-info}/entry_points.txt +0 -0
planar/logging/test_formatter.py
DELETED
@@ -1,327 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import logging
|
3
|
-
import re
|
4
|
-
from datetime import datetime
|
5
|
-
from decimal import Decimal
|
6
|
-
from uuid import UUID, uuid4
|
7
|
-
|
8
|
-
from pydantic import BaseModel
|
9
|
-
|
10
|
-
from planar.logging.formatter import StructuredFormatter, dictionary_print, json_print
|
11
|
-
|
12
|
-
|
13
|
-
class SampleModel(BaseModel):
|
14
|
-
name: str
|
15
|
-
value: int
|
16
|
-
|
17
|
-
|
18
|
-
class TestJsonPrint:
|
19
|
-
def test_json_print_simple_values(self):
|
20
|
-
"""Test json_print with simple values"""
|
21
|
-
assert json_print("test") == '"test"'
|
22
|
-
assert json_print(42) == "42"
|
23
|
-
assert json_print(True) == "true"
|
24
|
-
assert json_print(None) == "null"
|
25
|
-
|
26
|
-
def test_json_print_dict(self):
|
27
|
-
"""Test json_print with dictionary"""
|
28
|
-
data = {"key": "value", "number": 42}
|
29
|
-
result = json_print(data)
|
30
|
-
assert json.loads(result) == data
|
31
|
-
|
32
|
-
def test_json_print_list_without_colors(self):
|
33
|
-
"""Test json_print with list without colors - should produce valid JSON"""
|
34
|
-
data = ["item1", "item2", {"nested": "value"}]
|
35
|
-
result = json_print(data, use_colors=False)
|
36
|
-
# Should be valid JSON
|
37
|
-
parsed = json.loads(result)
|
38
|
-
assert parsed == data
|
39
|
-
|
40
|
-
def test_json_print_list_with_colors(self):
|
41
|
-
"""Test json_print with list with colors - should produce valid JSON with ANSI codes"""
|
42
|
-
data = ["item1", "item2", {"nested": "value"}]
|
43
|
-
result = json_print(data, use_colors=True)
|
44
|
-
# Should contain ANSI escape codes but when stripped should be valid JSON
|
45
|
-
# Remove ANSI codes to check JSON validity
|
46
|
-
import re
|
47
|
-
|
48
|
-
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
49
|
-
clean_result = ansi_escape.sub("", result)
|
50
|
-
parsed = json.loads(clean_result)
|
51
|
-
assert parsed == data
|
52
|
-
# Should contain color codes
|
53
|
-
assert "\x1b[" in result
|
54
|
-
|
55
|
-
def test_json_print_nested_structures(self):
|
56
|
-
"""Test json_print with deeply nested structures"""
|
57
|
-
data = {
|
58
|
-
"messages": [
|
59
|
-
{"content": "hello", "role": "user"},
|
60
|
-
{"content": "world", "role": "assistant"},
|
61
|
-
],
|
62
|
-
"tools": [
|
63
|
-
{"name": "tool1", "params": {"key": "value"}},
|
64
|
-
{"name": "tool2", "params": {"num": 42}},
|
65
|
-
],
|
66
|
-
}
|
67
|
-
result = json_print(data, use_colors=False)
|
68
|
-
parsed = json.loads(result)
|
69
|
-
assert parsed == data
|
70
|
-
|
71
|
-
def test_json_print_pydantic_model(self):
|
72
|
-
"""Test json_print with Pydantic models"""
|
73
|
-
model = SampleModel(name="test", value=42)
|
74
|
-
result = json_print(model, use_colors=False)
|
75
|
-
parsed = json.loads(result)
|
76
|
-
assert parsed == {"name": "test", "value": 42}
|
77
|
-
|
78
|
-
def test_json_print_custom_objects(self):
|
79
|
-
"""Test json_print with custom objects that need string conversion"""
|
80
|
-
|
81
|
-
class CustomObject:
|
82
|
-
def __str__(self):
|
83
|
-
return "custom_object"
|
84
|
-
|
85
|
-
data = {"obj": CustomObject()}
|
86
|
-
result = json_print(data, use_colors=False)
|
87
|
-
parsed = json.loads(result)
|
88
|
-
assert parsed == {"obj": "custom_object"}
|
89
|
-
|
90
|
-
def test_json_print_no_ansi_in_escaped_strings(self):
|
91
|
-
"""Test that ANSI codes don't get escaped in JSON strings"""
|
92
|
-
data = ["message1", "message2", {"key": "value"}]
|
93
|
-
result = json_print(data, use_colors=False)
|
94
|
-
# Should not contain escaped ANSI codes like \u001b
|
95
|
-
assert "\\u001b" not in result
|
96
|
-
# Should be valid JSON
|
97
|
-
parsed = json.loads(result)
|
98
|
-
assert parsed == data
|
99
|
-
|
100
|
-
def test_json_print_complex_types(self):
|
101
|
-
"""Test json_print with datetime, uuid, and decimal types"""
|
102
|
-
test_datetime = datetime(2023, 12, 25, 10, 30, 45)
|
103
|
-
test_uuid = uuid4()
|
104
|
-
test_decimal = Decimal("123.45")
|
105
|
-
|
106
|
-
# Test complex data structure with these types
|
107
|
-
data = {
|
108
|
-
"timestamp": test_datetime,
|
109
|
-
"id": test_uuid,
|
110
|
-
"amount": test_decimal,
|
111
|
-
"nested": {
|
112
|
-
"dates": [test_datetime, datetime(2024, 1, 1)],
|
113
|
-
"ids": [test_uuid, uuid4()],
|
114
|
-
"values": [test_decimal, Decimal("67.89")],
|
115
|
-
},
|
116
|
-
}
|
117
|
-
|
118
|
-
# Test without colors
|
119
|
-
result = json_print(data, use_colors=False)
|
120
|
-
|
121
|
-
# Should be valid JSON
|
122
|
-
parsed = json.loads(result)
|
123
|
-
|
124
|
-
# All complex types should be converted to strings
|
125
|
-
assert isinstance(parsed["timestamp"], str)
|
126
|
-
assert isinstance(parsed["id"], str)
|
127
|
-
assert isinstance(parsed["amount"], str)
|
128
|
-
assert isinstance(parsed["nested"]["dates"][0], str)
|
129
|
-
assert isinstance(parsed["nested"]["ids"][0], str)
|
130
|
-
assert isinstance(parsed["nested"]["values"][0], str)
|
131
|
-
|
132
|
-
# Verify string representations contain expected content
|
133
|
-
assert "2023-12-25" in parsed["timestamp"]
|
134
|
-
assert str(test_uuid) == parsed["id"]
|
135
|
-
assert "123.45" in parsed["amount"]
|
136
|
-
|
137
|
-
# Test with colors - should also work and produce valid JSON when stripped
|
138
|
-
result_colored = json_print(data, use_colors=True)
|
139
|
-
assert "\x1b[" in result_colored # Should contain ANSI codes
|
140
|
-
|
141
|
-
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
142
|
-
clean_result = ansi_escape.sub("", result_colored)
|
143
|
-
parsed_colored = json.loads(clean_result)
|
144
|
-
assert parsed_colored == parsed # Should be same as non-colored version
|
145
|
-
|
146
|
-
def test_json_print_with_base_model(self):
|
147
|
-
"""Test json_print with BaseModel using complex data"""
|
148
|
-
|
149
|
-
class TestModel(BaseModel):
|
150
|
-
name: str
|
151
|
-
date: datetime
|
152
|
-
uuid_val: UUID
|
153
|
-
decimal_val: Decimal
|
154
|
-
|
155
|
-
model = TestModel(
|
156
|
-
name="test",
|
157
|
-
date=datetime(2023, 12, 25, 10, 30, 45),
|
158
|
-
uuid_val=uuid4(),
|
159
|
-
decimal_val=Decimal("123.45"),
|
160
|
-
)
|
161
|
-
result = json_print(model, use_colors=False)
|
162
|
-
parsed = json.loads(result)
|
163
|
-
assert parsed == {
|
164
|
-
"name": "test",
|
165
|
-
"date": "2023-12-25T10:30:45",
|
166
|
-
"uuid_val": str(model.uuid_val),
|
167
|
-
"decimal_val": "123.45",
|
168
|
-
}
|
169
|
-
|
170
|
-
|
171
|
-
class TestDictionaryPrint:
|
172
|
-
def test_dictionary_print_simple(self):
|
173
|
-
"""Test dictionary_print with simple values"""
|
174
|
-
data = {"key": "value", "number": 42}
|
175
|
-
result = dictionary_print(data, use_colors=False)
|
176
|
-
assert 'key="value"' in result
|
177
|
-
assert "number=42" in result
|
178
|
-
|
179
|
-
def test_dictionary_print_with_lists(self):
|
180
|
-
"""Test dictionary_print with lists"""
|
181
|
-
data = {"items": ["a", "b", "c"], "count": 3}
|
182
|
-
result = dictionary_print(data, use_colors=False)
|
183
|
-
assert 'items=["a","b","c"]' in result or 'items=["a", "b", "c"]' in result
|
184
|
-
assert "count=3" in result
|
185
|
-
|
186
|
-
def test_dictionary_print_with_colors(self):
|
187
|
-
"""Test dictionary_print with colors enabled"""
|
188
|
-
data = {"key": "value"}
|
189
|
-
result = dictionary_print(data, use_colors=True)
|
190
|
-
# Should contain ANSI codes
|
191
|
-
assert "\x1b[" in result
|
192
|
-
# Should still contain the key-value pair
|
193
|
-
assert "key=" in result
|
194
|
-
|
195
|
-
|
196
|
-
class TestStructuredFormatter:
|
197
|
-
def test_structured_formatter_with_extra_attrs(self):
|
198
|
-
"""Test StructuredFormatter with extra attributes"""
|
199
|
-
formatter = StructuredFormatter(use_colors=False)
|
200
|
-
|
201
|
-
record = logging.LogRecord(
|
202
|
-
name="test.logger",
|
203
|
-
level=logging.INFO,
|
204
|
-
pathname="test.py",
|
205
|
-
lineno=1,
|
206
|
-
msg="test message",
|
207
|
-
args=(),
|
208
|
-
exc_info=None,
|
209
|
-
)
|
210
|
-
|
211
|
-
# Add extra attributes (simulating what PlanarLogger does)
|
212
|
-
record.__dict__.update(
|
213
|
-
{
|
214
|
-
"$workflow_id": "test-workflow",
|
215
|
-
"$step_id": 42,
|
216
|
-
"$messages": ["msg1", "msg2"],
|
217
|
-
}
|
218
|
-
)
|
219
|
-
|
220
|
-
result = formatter.format(record)
|
221
|
-
assert "workflow_id=" in result
|
222
|
-
assert "step_id=42" in result
|
223
|
-
assert "messages=" in result
|
224
|
-
# Should not contain escaped ANSI codes
|
225
|
-
assert "\\u001b" not in result
|
226
|
-
|
227
|
-
def test_structured_formatter_with_colors(self):
|
228
|
-
"""Test StructuredFormatter with colors enabled"""
|
229
|
-
formatter = StructuredFormatter(use_colors=True)
|
230
|
-
|
231
|
-
record = logging.LogRecord(
|
232
|
-
name="test.logger",
|
233
|
-
level=logging.INFO,
|
234
|
-
pathname="test.py",
|
235
|
-
lineno=1,
|
236
|
-
msg="test message",
|
237
|
-
args=(),
|
238
|
-
exc_info=None,
|
239
|
-
)
|
240
|
-
|
241
|
-
result = formatter.format(record)
|
242
|
-
# Should contain ANSI color codes
|
243
|
-
assert "\x1b[" in result
|
244
|
-
|
245
|
-
def test_structured_formatter_complex_data(self):
|
246
|
-
"""Test StructuredFormatter with complex nested data like the original issue"""
|
247
|
-
formatter = StructuredFormatter(use_colors=True)
|
248
|
-
|
249
|
-
record = logging.LogRecord(
|
250
|
-
name="planar.ai.test_agent",
|
251
|
-
level=logging.INFO,
|
252
|
-
pathname="test_agent.py",
|
253
|
-
lineno=188,
|
254
|
-
msg="patched_complete",
|
255
|
-
args=(),
|
256
|
-
exc_info=None,
|
257
|
-
)
|
258
|
-
|
259
|
-
# Simulate the complex data from the original issue
|
260
|
-
record.__dict__.update(
|
261
|
-
{
|
262
|
-
"$messages": [
|
263
|
-
{"content": "Use tools to solve the problem"},
|
264
|
-
{"content": "Problem: complex problem", "files": []},
|
265
|
-
{
|
266
|
-
"content": None,
|
267
|
-
"tool_calls": [
|
268
|
-
{
|
269
|
-
"id": "call_1",
|
270
|
-
"name": "tool1",
|
271
|
-
"arguments": {"param": "test_param"},
|
272
|
-
}
|
273
|
-
],
|
274
|
-
},
|
275
|
-
{"content": "Tool 1 result: test_param", "tool_call_id": "call_1"},
|
276
|
-
],
|
277
|
-
"$tools": [
|
278
|
-
{
|
279
|
-
"name": "tool1",
|
280
|
-
"description": "Test tool 1",
|
281
|
-
"parameters": {
|
282
|
-
"type": "object",
|
283
|
-
"properties": {"param": {"type": "string"}},
|
284
|
-
},
|
285
|
-
},
|
286
|
-
{
|
287
|
-
"name": "tool2",
|
288
|
-
"description": "Test tool 2",
|
289
|
-
"parameters": {
|
290
|
-
"type": "object",
|
291
|
-
"properties": {"num": {"type": "integer"}},
|
292
|
-
},
|
293
|
-
},
|
294
|
-
],
|
295
|
-
"$workflow_id": "test-workflow-id",
|
296
|
-
"$step_id": 4,
|
297
|
-
}
|
298
|
-
)
|
299
|
-
|
300
|
-
result = formatter.format(record)
|
301
|
-
|
302
|
-
# Should contain the message
|
303
|
-
assert "patched_complete" in result
|
304
|
-
assert "planar.ai.test_agent" in result
|
305
|
-
|
306
|
-
# Should contain the extra attributes
|
307
|
-
assert "messages=" in result
|
308
|
-
assert "tools=" in result
|
309
|
-
assert "workflow_id=" in result
|
310
|
-
assert "step_id=" in result
|
311
|
-
|
312
|
-
# Most importantly: should NOT contain escaped ANSI codes
|
313
|
-
assert "\\u001b" not in result
|
314
|
-
|
315
|
-
# Should contain actual ANSI codes (for colors)
|
316
|
-
assert "\x1b[" in result
|
317
|
-
|
318
|
-
# Verify the fix - the data should be properly formatted JSON with colors
|
319
|
-
# Extract the JSON parts and verify they're valid when ANSI codes are stripped
|
320
|
-
import re
|
321
|
-
|
322
|
-
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
323
|
-
clean_result = ansi_escape.sub("", result)
|
324
|
-
|
325
|
-
# The messages should be valid JSON when extracted
|
326
|
-
assert '"content": "Use tools to solve the problem"' in clean_result
|
327
|
-
assert '"content": "Problem: complex problem"' in clean_result
|
@@ -1,97 +0,0 @@
|
|
1
|
-
import pytest
|
2
|
-
from sqlmodel import Field, SQLModel
|
3
|
-
|
4
|
-
from planar.db import PlanarSession, new_session
|
5
|
-
from planar.modeling.mixins.auditable import AuditableMixin
|
6
|
-
from planar.security.auth_context import (
|
7
|
-
Principal,
|
8
|
-
as_principal,
|
9
|
-
get_current_principal,
|
10
|
-
)
|
11
|
-
|
12
|
-
TEST_PRINCIPAL = Principal(
|
13
|
-
sub="test_user",
|
14
|
-
iss="test",
|
15
|
-
exp=1000,
|
16
|
-
iat=1000,
|
17
|
-
sid="test",
|
18
|
-
jti="test",
|
19
|
-
org_id="test",
|
20
|
-
org_name="test",
|
21
|
-
user_first_name="test",
|
22
|
-
user_last_name="test",
|
23
|
-
user_email="test@test.com",
|
24
|
-
role="test",
|
25
|
-
permissions=["test"],
|
26
|
-
extra_claims={"test": "test"},
|
27
|
-
)
|
28
|
-
|
29
|
-
|
30
|
-
class TestAuditableModel(AuditableMixin, SQLModel, table=True):
|
31
|
-
"""Test model using AuditableMixin."""
|
32
|
-
|
33
|
-
__test__ = False
|
34
|
-
|
35
|
-
id: int | None = Field(default=None, primary_key=True)
|
36
|
-
name: str = Field()
|
37
|
-
|
38
|
-
|
39
|
-
@pytest.fixture
|
40
|
-
async def session(tmp_db_engine):
|
41
|
-
"""Create a database session."""
|
42
|
-
|
43
|
-
async with new_session(tmp_db_engine) as session:
|
44
|
-
await (await session.connection()).run_sync(SQLModel.metadata.create_all)
|
45
|
-
yield session
|
46
|
-
|
47
|
-
|
48
|
-
def test_auditable_mixin_has_audit_fields():
|
49
|
-
"""Test that AuditableMixin provides default audit fields."""
|
50
|
-
model = TestAuditableModel(name="test")
|
51
|
-
|
52
|
-
assert hasattr(model, "created_by")
|
53
|
-
assert hasattr(model, "updated_by")
|
54
|
-
assert model.created_by == "system"
|
55
|
-
assert model.updated_by == "system"
|
56
|
-
|
57
|
-
|
58
|
-
async def test_auditable_mixin_sets_values_on_insert(session: PlanarSession):
|
59
|
-
"""Test that audit fields are set from SecurityContext on insert."""
|
60
|
-
with as_principal(TEST_PRINCIPAL):
|
61
|
-
model = TestAuditableModel(name="test_insert")
|
62
|
-
session.add(model)
|
63
|
-
await session.commit()
|
64
|
-
|
65
|
-
# Refresh to get the updated values
|
66
|
-
await session.refresh(model)
|
67
|
-
|
68
|
-
assert model.created_by == "test@test.com"
|
69
|
-
assert model.updated_by == "test@test.com"
|
70
|
-
|
71
|
-
|
72
|
-
async def test_auditable_mixin_sets_updated_by_on_update(session: PlanarSession):
|
73
|
-
"""Test that updated_by is set from SecurityContext on update."""
|
74
|
-
# First insert with initial user
|
75
|
-
with as_principal(TEST_PRINCIPAL):
|
76
|
-
model = TestAuditableModel(name="test_update")
|
77
|
-
session.add(model)
|
78
|
-
await session.commit()
|
79
|
-
await session.refresh(model)
|
80
|
-
|
81
|
-
assert model.created_by == "test@test.com"
|
82
|
-
assert model.updated_by == "test@test.com"
|
83
|
-
|
84
|
-
# Now update with different user
|
85
|
-
updating_principal = TEST_PRINCIPAL.model_copy(
|
86
|
-
update={"user_email": "updating@test.com"}
|
87
|
-
)
|
88
|
-
with as_principal(updating_principal):
|
89
|
-
assert get_current_principal() == updating_principal
|
90
|
-
model.name = "updated_name"
|
91
|
-
session.add(model)
|
92
|
-
await session.commit()
|
93
|
-
await session.refresh(model)
|
94
|
-
|
95
|
-
# created_by should remain the same, updated_by should change
|
96
|
-
assert model.created_by == "test@test.com"
|
97
|
-
assert model.updated_by == "updating@test.com"
|
@@ -1,134 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
from datetime import timedelta
|
3
|
-
|
4
|
-
from sqlalchemy.ext.asyncio import AsyncEngine
|
5
|
-
from sqlmodel import select
|
6
|
-
|
7
|
-
from planar.db import new_session
|
8
|
-
from planar.modeling.mixins import TimestampMixin
|
9
|
-
from planar.modeling.orm.planar_base_entity import PlanarBaseEntity
|
10
|
-
from planar.utils import utc_now
|
11
|
-
|
12
|
-
|
13
|
-
class TimestampTestModel(TimestampMixin, PlanarBaseEntity, table=True):
|
14
|
-
"""Test model that uses the TimestampMixin."""
|
15
|
-
|
16
|
-
name: str
|
17
|
-
value: int = 0
|
18
|
-
|
19
|
-
|
20
|
-
async def test_timestamp_fields_set_on_creation(tmp_db_engine: AsyncEngine):
|
21
|
-
"""Test that created_at and updated_at are set when a model is created."""
|
22
|
-
# Record time before the operation
|
23
|
-
before_creation = utc_now()
|
24
|
-
|
25
|
-
# Create and insert model
|
26
|
-
model_id = None
|
27
|
-
async with new_session(tmp_db_engine) as session:
|
28
|
-
model = TimestampTestModel(name="test_item", value=42)
|
29
|
-
session.add(model)
|
30
|
-
await session.commit()
|
31
|
-
model_id = model.id
|
32
|
-
|
33
|
-
# Record time after the operation
|
34
|
-
after_creation = utc_now()
|
35
|
-
|
36
|
-
# Fetch model to verify timestamps
|
37
|
-
async with new_session(tmp_db_engine) as session:
|
38
|
-
created_model = (
|
39
|
-
await session.exec(
|
40
|
-
select(TimestampTestModel).where(TimestampTestModel.id == model_id)
|
41
|
-
)
|
42
|
-
).one()
|
43
|
-
|
44
|
-
# Verify created_at is set and within the expected time range
|
45
|
-
# and that it equals updated_at
|
46
|
-
assert created_model.created_at is not None
|
47
|
-
assert before_creation <= created_model.created_at <= after_creation
|
48
|
-
assert created_model.created_at == created_model.updated_at
|
49
|
-
|
50
|
-
|
51
|
-
async def test_updated_at_reflects_changes(tmp_db_engine: AsyncEngine):
|
52
|
-
"""Test that updated_at is updated when a model is modified."""
|
53
|
-
# Create and insert model
|
54
|
-
model_id = None
|
55
|
-
async with new_session(tmp_db_engine) as session:
|
56
|
-
model = TimestampTestModel(name="test_item", value=42)
|
57
|
-
session.add(model)
|
58
|
-
await session.commit()
|
59
|
-
model_id = model.id
|
60
|
-
|
61
|
-
# Get initial created_at and updated_at values
|
62
|
-
initial_model = (
|
63
|
-
await session.exec(
|
64
|
-
select(TimestampTestModel).where(TimestampTestModel.id == model_id)
|
65
|
-
)
|
66
|
-
).one()
|
67
|
-
await session.commit()
|
68
|
-
initial_created_at = initial_model.created_at
|
69
|
-
initial_updated_at = initial_model.updated_at
|
70
|
-
|
71
|
-
# Wait a moment to ensure timestamp will be different
|
72
|
-
await asyncio.sleep(0.01)
|
73
|
-
|
74
|
-
# Record time before update
|
75
|
-
before_update = utc_now()
|
76
|
-
|
77
|
-
# Update the model
|
78
|
-
async with new_session(tmp_db_engine) as session:
|
79
|
-
model_to_update = (
|
80
|
-
await session.exec(
|
81
|
-
select(TimestampTestModel).where(TimestampTestModel.id == model_id)
|
82
|
-
)
|
83
|
-
).one()
|
84
|
-
model_to_update.value = 99
|
85
|
-
await session.commit()
|
86
|
-
|
87
|
-
# Record time after update
|
88
|
-
after_update = utc_now()
|
89
|
-
|
90
|
-
# Verify the timestamps
|
91
|
-
async with new_session(tmp_db_engine) as session:
|
92
|
-
updated_model = (
|
93
|
-
await session.exec(
|
94
|
-
select(TimestampTestModel).where(TimestampTestModel.id == model_id)
|
95
|
-
)
|
96
|
-
).one()
|
97
|
-
await session.commit()
|
98
|
-
|
99
|
-
# created_at should not change
|
100
|
-
assert updated_model.created_at == initial_created_at
|
101
|
-
|
102
|
-
# updated_at should be newer than before
|
103
|
-
assert updated_model.updated_at is not None
|
104
|
-
assert initial_updated_at is not None
|
105
|
-
assert updated_model.updated_at > initial_updated_at
|
106
|
-
assert before_update <= updated_model.updated_at <= after_update
|
107
|
-
|
108
|
-
|
109
|
-
async def test_timestamp_init_with_explicit_values():
|
110
|
-
"""Test initializing a model with explicit timestamp values."""
|
111
|
-
# Create a specific timestamp
|
112
|
-
now = utc_now()
|
113
|
-
past = now - timedelta(days=1)
|
114
|
-
|
115
|
-
# Initialize model with explicit timestamps
|
116
|
-
model = TimestampTestModel(
|
117
|
-
name="test_explicit_timestamps",
|
118
|
-
value=200,
|
119
|
-
created_at=past,
|
120
|
-
updated_at=now,
|
121
|
-
)
|
122
|
-
|
123
|
-
# Verify timestamps match what we provided
|
124
|
-
assert model.created_at == past
|
125
|
-
assert model.updated_at == now
|
126
|
-
|
127
|
-
# Initialize model with only created_at
|
128
|
-
model2 = TimestampTestModel(
|
129
|
-
name="test_partial_timestamps", value=300, created_at=past
|
130
|
-
)
|
131
|
-
|
132
|
-
# Verify updated_at equals created_at when only created_at is provided
|
133
|
-
assert model2.created_at == past
|
134
|
-
assert model2.updated_at == past
|
@@ -1,52 +0,0 @@
|
|
1
|
-
from uuid import UUID
|
2
|
-
|
3
|
-
from sqlmodel import Field
|
4
|
-
|
5
|
-
from planar.db import PlanarSession
|
6
|
-
from planar.modeling.mixins.uuid_primary_key import UUIDPrimaryKeyMixin
|
7
|
-
from planar.modeling.orm.planar_base_entity import PlanarBaseEntity
|
8
|
-
|
9
|
-
|
10
|
-
class UUIDModelTest(PlanarBaseEntity, UUIDPrimaryKeyMixin, table=True):
|
11
|
-
"""Test model using UUIDPrimaryKeyMixin."""
|
12
|
-
|
13
|
-
name: str = Field()
|
14
|
-
|
15
|
-
|
16
|
-
def test_uuid_primary_key_mixin_creates_uuid_id():
|
17
|
-
"""Test that UUIDPrimaryKeyMixin provides a UUID id field."""
|
18
|
-
model = UUIDModelTest(name="test")
|
19
|
-
|
20
|
-
assert hasattr(model, "id")
|
21
|
-
assert isinstance(model.id, UUID)
|
22
|
-
assert model.id is not None
|
23
|
-
|
24
|
-
|
25
|
-
def test_uuid_primary_key_mixin_allows_custom_id():
|
26
|
-
"""Test that a custom UUID can be provided."""
|
27
|
-
custom_uuid = UUID("12345678-1234-5678-1234-123456789abc")
|
28
|
-
model = UUIDModelTest(id=custom_uuid, name="test")
|
29
|
-
|
30
|
-
assert model.id == custom_uuid
|
31
|
-
|
32
|
-
|
33
|
-
async def test_uuid_primary_key_mixin_is_primary_key(session: PlanarSession):
|
34
|
-
"""Test that the id field works as a primary key."""
|
35
|
-
model1 = UUIDModelTest(name="test1")
|
36
|
-
model2 = UUIDModelTest(name="test2")
|
37
|
-
|
38
|
-
session.add(model1)
|
39
|
-
session.add(model2)
|
40
|
-
await session.commit()
|
41
|
-
|
42
|
-
# Both should have different IDs
|
43
|
-
assert model1.id != model2.id
|
44
|
-
|
45
|
-
# Both should be retrievable by their IDs
|
46
|
-
retrieved1 = await session.get(UUIDModelTest, model1.id)
|
47
|
-
retrieved2 = await session.get(UUIDModelTest, model2.id)
|
48
|
-
|
49
|
-
assert retrieved1 is not None
|
50
|
-
assert retrieved1.name == "test1"
|
51
|
-
assert retrieved2 is not None
|
52
|
-
assert retrieved2.name == "test2"
|