planar 0.9.3__py3-none-any.whl → 0.11.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.
Files changed (76) hide show
  1. planar/ai/agent.py +2 -1
  2. planar/ai/agent_base.py +24 -5
  3. planar/ai/state.py +17 -0
  4. planar/app.py +18 -1
  5. planar/data/connection.py +108 -0
  6. planar/data/dataset.py +11 -104
  7. planar/data/utils.py +89 -0
  8. planar/db/alembic/env.py +25 -1
  9. planar/files/storage/azure_blob.py +1 -1
  10. planar/registry_items.py +2 -0
  11. planar/routers/dataset_router.py +213 -0
  12. planar/routers/info.py +79 -36
  13. planar/routers/models.py +1 -0
  14. planar/routers/workflow.py +2 -0
  15. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  16. planar/security/authorization.py +31 -3
  17. planar/security/default_policies.cedar +25 -0
  18. planar/testing/fixtures.py +34 -1
  19. planar/testing/planar_test_client.py +1 -1
  20. planar/workflows/decorators.py +2 -1
  21. planar/workflows/wrappers.py +1 -0
  22. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/METADATA +9 -1
  23. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/RECORD +25 -72
  24. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/WHEEL +1 -1
  25. planar/ai/test_agent_serialization.py +0 -229
  26. planar/ai/test_agent_tool_step_display.py +0 -78
  27. planar/data/test_dataset.py +0 -354
  28. planar/files/storage/test_azure_blob.py +0 -435
  29. planar/files/storage/test_local_directory.py +0 -162
  30. planar/files/storage/test_s3.py +0 -299
  31. planar/files/test_files.py +0 -282
  32. planar/human/test_human.py +0 -385
  33. planar/logging/test_formatter.py +0 -327
  34. planar/modeling/mixins/test_auditable.py +0 -97
  35. planar/modeling/mixins/test_timestamp.py +0 -134
  36. planar/modeling/mixins/test_uuid_primary_key.py +0 -52
  37. planar/routers/test_agents_router.py +0 -174
  38. planar/routers/test_files_router.py +0 -49
  39. planar/routers/test_object_config_router.py +0 -367
  40. planar/routers/test_routes_security.py +0 -168
  41. planar/routers/test_rule_router.py +0 -470
  42. planar/routers/test_workflow_router.py +0 -539
  43. planar/rules/test_data/account_dormancy_management.json +0 -223
  44. planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
  45. planar/rules/test_data/applicant_risk_assessment.json +0 -435
  46. planar/rules/test_data/booking_fraud_detection.json +0 -407
  47. planar/rules/test_data/cellular_data_rollover_system.json +0 -258
  48. planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
  49. planar/rules/test_data/customer_lifetime_value.json +0 -143
  50. planar/rules/test_data/import_duties_calculator.json +0 -289
  51. planar/rules/test_data/insurance_prior_authorization.json +0 -443
  52. planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
  53. planar/rules/test_data/order_consolidation_system.json +0 -375
  54. planar/rules/test_data/portfolio_risk_monitor.json +0 -471
  55. planar/rules/test_data/supply_chain_risk.json +0 -253
  56. planar/rules/test_data/warehouse_cross_docking.json +0 -237
  57. planar/rules/test_rules.py +0 -1494
  58. planar/security/tests/test_auth_middleware.py +0 -162
  59. planar/security/tests/test_authorization_context.py +0 -78
  60. planar/security/tests/test_cedar_basics.py +0 -41
  61. planar/security/tests/test_cedar_policies.py +0 -158
  62. planar/security/tests/test_jwt_principal_context.py +0 -179
  63. planar/test_app.py +0 -142
  64. planar/test_cli.py +0 -394
  65. planar/test_config.py +0 -515
  66. planar/test_object_config.py +0 -527
  67. planar/test_object_registry.py +0 -14
  68. planar/test_sqlalchemy.py +0 -193
  69. planar/test_utils.py +0 -105
  70. planar/testing/test_memory_storage.py +0 -143
  71. planar/workflows/test_concurrency_detection.py +0 -120
  72. planar/workflows/test_lock_timeout.py +0 -140
  73. planar/workflows/test_serialization.py +0 -1203
  74. planar/workflows/test_suspend_deserialization.py +0 -231
  75. planar/workflows/test_workflow.py +0 -2005
  76. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/entry_points.txt +0 -0
@@ -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"