julee 0.1.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.
- julee/__init__.py +3 -0
- julee/api/__init__.py +20 -0
- julee/api/app.py +180 -0
- julee/api/dependencies.py +257 -0
- julee/api/requests.py +175 -0
- julee/api/responses.py +43 -0
- julee/api/routers/__init__.py +43 -0
- julee/api/routers/assembly_specifications.py +212 -0
- julee/api/routers/documents.py +182 -0
- julee/api/routers/knowledge_service_configs.py +79 -0
- julee/api/routers/knowledge_service_queries.py +293 -0
- julee/api/routers/system.py +137 -0
- julee/api/routers/workflows.py +234 -0
- julee/api/services/__init__.py +20 -0
- julee/api/services/system_initialization.py +214 -0
- julee/api/tests/__init__.py +14 -0
- julee/api/tests/routers/__init__.py +17 -0
- julee/api/tests/routers/test_assembly_specifications.py +749 -0
- julee/api/tests/routers/test_documents.py +301 -0
- julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
- julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
- julee/api/tests/routers/test_system.py +179 -0
- julee/api/tests/routers/test_workflows.py +393 -0
- julee/api/tests/test_app.py +285 -0
- julee/api/tests/test_dependencies.py +245 -0
- julee/api/tests/test_requests.py +250 -0
- julee/domain/__init__.py +22 -0
- julee/domain/models/__init__.py +49 -0
- julee/domain/models/assembly/__init__.py +17 -0
- julee/domain/models/assembly/assembly.py +103 -0
- julee/domain/models/assembly/tests/__init__.py +0 -0
- julee/domain/models/assembly/tests/factories.py +37 -0
- julee/domain/models/assembly/tests/test_assembly.py +430 -0
- julee/domain/models/assembly_specification/__init__.py +24 -0
- julee/domain/models/assembly_specification/assembly_specification.py +172 -0
- julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
- julee/domain/models/assembly_specification/tests/__init__.py +0 -0
- julee/domain/models/assembly_specification/tests/factories.py +78 -0
- julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
- julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
- julee/domain/models/custom_fields/__init__.py +0 -0
- julee/domain/models/custom_fields/content_stream.py +68 -0
- julee/domain/models/custom_fields/tests/__init__.py +0 -0
- julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
- julee/domain/models/document/__init__.py +17 -0
- julee/domain/models/document/document.py +150 -0
- julee/domain/models/document/tests/__init__.py +0 -0
- julee/domain/models/document/tests/factories.py +76 -0
- julee/domain/models/document/tests/test_document.py +297 -0
- julee/domain/models/knowledge_service_config/__init__.py +17 -0
- julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
- julee/domain/models/policy/__init__.py +15 -0
- julee/domain/models/policy/document_policy_validation.py +220 -0
- julee/domain/models/policy/policy.py +203 -0
- julee/domain/models/policy/tests/__init__.py +0 -0
- julee/domain/models/policy/tests/factories.py +47 -0
- julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
- julee/domain/models/policy/tests/test_policy.py +546 -0
- julee/domain/repositories/__init__.py +27 -0
- julee/domain/repositories/assembly.py +45 -0
- julee/domain/repositories/assembly_specification.py +52 -0
- julee/domain/repositories/base.py +146 -0
- julee/domain/repositories/document.py +49 -0
- julee/domain/repositories/document_policy_validation.py +52 -0
- julee/domain/repositories/knowledge_service_config.py +54 -0
- julee/domain/repositories/knowledge_service_query.py +44 -0
- julee/domain/repositories/policy.py +49 -0
- julee/domain/use_cases/__init__.py +17 -0
- julee/domain/use_cases/decorators.py +107 -0
- julee/domain/use_cases/extract_assemble_data.py +649 -0
- julee/domain/use_cases/initialize_system_data.py +842 -0
- julee/domain/use_cases/tests/__init__.py +7 -0
- julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
- julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
- julee/domain/use_cases/tests/test_validate_document.py +1228 -0
- julee/domain/use_cases/validate_document.py +736 -0
- julee/fixtures/assembly_specifications.yaml +70 -0
- julee/fixtures/documents.yaml +178 -0
- julee/fixtures/knowledge_service_configs.yaml +37 -0
- julee/fixtures/knowledge_service_queries.yaml +27 -0
- julee/repositories/__init__.py +17 -0
- julee/repositories/memory/__init__.py +31 -0
- julee/repositories/memory/assembly.py +84 -0
- julee/repositories/memory/assembly_specification.py +125 -0
- julee/repositories/memory/base.py +227 -0
- julee/repositories/memory/document.py +149 -0
- julee/repositories/memory/document_policy_validation.py +104 -0
- julee/repositories/memory/knowledge_service_config.py +123 -0
- julee/repositories/memory/knowledge_service_query.py +120 -0
- julee/repositories/memory/policy.py +87 -0
- julee/repositories/memory/tests/__init__.py +0 -0
- julee/repositories/memory/tests/test_document.py +212 -0
- julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
- julee/repositories/memory/tests/test_policy.py +443 -0
- julee/repositories/minio/__init__.py +31 -0
- julee/repositories/minio/assembly.py +103 -0
- julee/repositories/minio/assembly_specification.py +170 -0
- julee/repositories/minio/client.py +570 -0
- julee/repositories/minio/document.py +530 -0
- julee/repositories/minio/document_policy_validation.py +120 -0
- julee/repositories/minio/knowledge_service_config.py +187 -0
- julee/repositories/minio/knowledge_service_query.py +211 -0
- julee/repositories/minio/policy.py +106 -0
- julee/repositories/minio/tests/__init__.py +0 -0
- julee/repositories/minio/tests/fake_client.py +213 -0
- julee/repositories/minio/tests/test_assembly.py +374 -0
- julee/repositories/minio/tests/test_assembly_specification.py +391 -0
- julee/repositories/minio/tests/test_client_protocol.py +57 -0
- julee/repositories/minio/tests/test_document.py +591 -0
- julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
- julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
- julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
- julee/repositories/minio/tests/test_policy.py +559 -0
- julee/repositories/temporal/__init__.py +38 -0
- julee/repositories/temporal/activities.py +114 -0
- julee/repositories/temporal/activity_names.py +34 -0
- julee/repositories/temporal/proxies.py +159 -0
- julee/services/__init__.py +18 -0
- julee/services/knowledge_service/__init__.py +48 -0
- julee/services/knowledge_service/anthropic/__init__.py +12 -0
- julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
- julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
- julee/services/knowledge_service/factory.py +138 -0
- julee/services/knowledge_service/knowledge_service.py +160 -0
- julee/services/knowledge_service/memory/__init__.py +13 -0
- julee/services/knowledge_service/memory/knowledge_service.py +278 -0
- julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
- julee/services/knowledge_service/test_factory.py +112 -0
- julee/services/temporal/__init__.py +38 -0
- julee/services/temporal/activities.py +86 -0
- julee/services/temporal/activity_names.py +22 -0
- julee/services/temporal/proxies.py +41 -0
- julee/util/__init__.py +0 -0
- julee/util/domain.py +119 -0
- julee/util/repos/__init__.py +0 -0
- julee/util/repos/minio/__init__.py +0 -0
- julee/util/repos/minio/file_storage.py +213 -0
- julee/util/repos/temporal/__init__.py +11 -0
- julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
- julee/util/repos/temporal/data_converter.py +123 -0
- julee/util/repos/temporal/minio_file_storage.py +12 -0
- julee/util/repos/temporal/proxies/__init__.py +0 -0
- julee/util/repos/temporal/proxies/file_storage.py +58 -0
- julee/util/repositories.py +55 -0
- julee/util/temporal/__init__.py +22 -0
- julee/util/temporal/activities.py +123 -0
- julee/util/temporal/decorators.py +473 -0
- julee/util/tests/__init__.py +1 -0
- julee/util/tests/test_decorators.py +770 -0
- julee/util/validation/__init__.py +29 -0
- julee/util/validation/repository.py +100 -0
- julee/util/validation/type_guards.py +369 -0
- julee/worker.py +211 -0
- julee/workflows/__init__.py +26 -0
- julee/workflows/extract_assemble.py +215 -0
- julee/workflows/validate_document.py +228 -0
- julee-0.1.0.dist-info/METADATA +195 -0
- julee-0.1.0.dist-info/RECORD +161 -0
- julee-0.1.0.dist-info/WHEEL +5 -0
- julee-0.1.0.dist-info/licenses/LICENSE +674 -0
- julee-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for temporal decorators.
|
|
3
|
+
|
|
4
|
+
This module tests the decorators in isolation to ensure they properly wrap
|
|
5
|
+
async methods as Temporal activities and handle type substitution correctly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Standard library imports
|
|
9
|
+
import asyncio
|
|
10
|
+
import inspect
|
|
11
|
+
from typing import (
|
|
12
|
+
Any,
|
|
13
|
+
List,
|
|
14
|
+
Optional,
|
|
15
|
+
Protocol,
|
|
16
|
+
TypeVar,
|
|
17
|
+
get_args,
|
|
18
|
+
get_origin,
|
|
19
|
+
runtime_checkable,
|
|
20
|
+
)
|
|
21
|
+
from unittest.mock import patch
|
|
22
|
+
|
|
23
|
+
# Third-party imports
|
|
24
|
+
import pytest
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
from temporalio import activity
|
|
27
|
+
|
|
28
|
+
# Project imports
|
|
29
|
+
import julee.util.temporal.decorators as decorators_module
|
|
30
|
+
from julee.domain.repositories.base import BaseRepository
|
|
31
|
+
from julee.util.temporal.decorators import (
|
|
32
|
+
_extract_concrete_type_from_base,
|
|
33
|
+
_needs_pydantic_validation,
|
|
34
|
+
_substitute_typevar_with_concrete,
|
|
35
|
+
temporal_activity_registration,
|
|
36
|
+
temporal_workflow_proxy,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@runtime_checkable
|
|
41
|
+
class MockBaseRepositoryProtocol(Protocol):
|
|
42
|
+
"""Mock base repository protocol for testing inheritance."""
|
|
43
|
+
|
|
44
|
+
async def base_async_method(self, arg1: str) -> str:
|
|
45
|
+
"""Base async method that should be wrapped."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def base_sync_method(self, arg1: str) -> str:
|
|
49
|
+
"""Base sync method that should NOT be wrapped."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
async def _private_async_method(self, arg1: str) -> str:
|
|
53
|
+
"""Private async method that should NOT be wrapped."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@runtime_checkable
|
|
58
|
+
class MockRepositoryProtocol(MockBaseRepositoryProtocol, Protocol):
|
|
59
|
+
"""Mock repository protocol for testing the decorator."""
|
|
60
|
+
|
|
61
|
+
async def process_payment(self, order_id: str, amount: float) -> dict:
|
|
62
|
+
"""Mock payment processing method."""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
async def get_payment(self, payment_id: str) -> Optional[dict]:
|
|
66
|
+
"""Mock get payment method."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
async def refund_payment(self, payment_id: str) -> dict:
|
|
70
|
+
"""Mock refund payment method."""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def sync_method(self, value: str) -> str:
|
|
74
|
+
"""Sync method that should NOT be wrapped."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
async def _private_method(self, value: str) -> str:
|
|
78
|
+
"""Private async method that should NOT be wrapped."""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MockRepository(MockRepositoryProtocol):
|
|
83
|
+
"""Concrete mock repository implementation for testing."""
|
|
84
|
+
|
|
85
|
+
async def base_async_method(self, arg1: str) -> str:
|
|
86
|
+
"""Base async method that should be wrapped."""
|
|
87
|
+
return f"base_result_{arg1}"
|
|
88
|
+
|
|
89
|
+
def base_sync_method(self, arg1: str) -> str:
|
|
90
|
+
"""Base sync method that should NOT be wrapped."""
|
|
91
|
+
return f"base_sync_{arg1}"
|
|
92
|
+
|
|
93
|
+
async def _private_async_method(self, arg1: str) -> str:
|
|
94
|
+
"""Private async method that should NOT be wrapped."""
|
|
95
|
+
return f"private_{arg1}"
|
|
96
|
+
|
|
97
|
+
async def process_payment(self, order_id: str, amount: float) -> dict:
|
|
98
|
+
"""Mock payment processing method."""
|
|
99
|
+
return {"status": "success", "order_id": order_id, "amount": amount}
|
|
100
|
+
|
|
101
|
+
async def get_payment(self, payment_id: str) -> Optional[dict]:
|
|
102
|
+
"""Mock get payment method."""
|
|
103
|
+
if payment_id == "not_found":
|
|
104
|
+
return None
|
|
105
|
+
return {"payment_id": payment_id, "status": "completed"}
|
|
106
|
+
|
|
107
|
+
async def refund_payment(self, payment_id: str) -> dict:
|
|
108
|
+
"""Mock refund payment method."""
|
|
109
|
+
return {"status": "refunded", "payment_id": payment_id}
|
|
110
|
+
|
|
111
|
+
def sync_method(self, value: str) -> str:
|
|
112
|
+
"""Sync method that should NOT be wrapped."""
|
|
113
|
+
return f"sync_{value}"
|
|
114
|
+
|
|
115
|
+
async def _private_method(self, value: str) -> str:
|
|
116
|
+
"""Private async method that should NOT be wrapped."""
|
|
117
|
+
return f"private_{value}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_decorator_wraps_public_async_methods() -> None:
|
|
121
|
+
"""Test decorator wraps all public async methods as activities."""
|
|
122
|
+
|
|
123
|
+
@temporal_activity_registration("test.repo")
|
|
124
|
+
class DecoratedRepository(MockRepository):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
# Check that async methods are wrapped with activity decorator
|
|
128
|
+
# Use dir() check since hasattr() doesn't work with Temporal activities
|
|
129
|
+
assert "__temporal_activity_definition" in dir(DecoratedRepository.process_payment)
|
|
130
|
+
assert "__temporal_activity_definition" in dir(DecoratedRepository.get_payment)
|
|
131
|
+
assert "__temporal_activity_definition" in dir(DecoratedRepository.refund_payment)
|
|
132
|
+
assert "__temporal_activity_definition" in dir(
|
|
133
|
+
DecoratedRepository.base_async_method
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Check activity names by accessing the attribute directly
|
|
137
|
+
process_payment_attrs = {
|
|
138
|
+
attr: getattr(DecoratedRepository.process_payment, attr, None)
|
|
139
|
+
for attr in dir(DecoratedRepository.process_payment)
|
|
140
|
+
if attr == "__temporal_activity_definition"
|
|
141
|
+
}
|
|
142
|
+
get_payment_attrs = {
|
|
143
|
+
attr: getattr(DecoratedRepository.get_payment, attr, None)
|
|
144
|
+
for attr in dir(DecoratedRepository.get_payment)
|
|
145
|
+
if attr == "__temporal_activity_definition"
|
|
146
|
+
}
|
|
147
|
+
refund_payment_attrs = {
|
|
148
|
+
attr: getattr(DecoratedRepository.refund_payment, attr, None)
|
|
149
|
+
for attr in dir(DecoratedRepository.refund_payment)
|
|
150
|
+
if attr == "__temporal_activity_definition"
|
|
151
|
+
}
|
|
152
|
+
base_async_attrs = {
|
|
153
|
+
attr: getattr(DecoratedRepository.base_async_method, attr, None)
|
|
154
|
+
for attr in dir(DecoratedRepository.base_async_method)
|
|
155
|
+
if attr == "__temporal_activity_definition"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Verify the attributes exist and have the expected names
|
|
159
|
+
assert process_payment_attrs
|
|
160
|
+
assert get_payment_attrs
|
|
161
|
+
assert refund_payment_attrs
|
|
162
|
+
assert base_async_attrs
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_decorator_does_not_wrap_sync_methods() -> None:
|
|
166
|
+
"""Test that sync methods are not wrapped as activities."""
|
|
167
|
+
|
|
168
|
+
@temporal_activity_registration("test.repo")
|
|
169
|
+
class DecoratedRepository(MockRepository):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
# Check that sync methods are NOT wrapped
|
|
173
|
+
assert "__temporal_activity_definition" not in dir(DecoratedRepository.sync_method)
|
|
174
|
+
assert "__temporal_activity_definition" not in dir(
|
|
175
|
+
DecoratedRepository.base_sync_method
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_decorator_does_not_wrap_private_methods() -> None:
|
|
180
|
+
"""Test that private async methods are not wrapped as activities."""
|
|
181
|
+
|
|
182
|
+
@temporal_activity_registration("test.repo")
|
|
183
|
+
class DecoratedRepository(MockRepository):
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
# Check that private async methods are NOT wrapped
|
|
187
|
+
assert "__temporal_activity_definition" not in dir(
|
|
188
|
+
DecoratedRepository._private_method
|
|
189
|
+
)
|
|
190
|
+
assert "__temporal_activity_definition" not in dir(
|
|
191
|
+
DecoratedRepository._private_async_method
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_decorated_methods_preserve_functionality() -> None:
|
|
196
|
+
"""Test that decorated methods still work as expected."""
|
|
197
|
+
|
|
198
|
+
@temporal_activity_registration("test.repo")
|
|
199
|
+
class DecoratedRepository(MockRepository):
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
repo = DecoratedRepository()
|
|
203
|
+
|
|
204
|
+
# Test sync method works normally
|
|
205
|
+
result = repo.sync_method("test")
|
|
206
|
+
assert result == "sync_test"
|
|
207
|
+
|
|
208
|
+
# Test private method works normally
|
|
209
|
+
async def test_private() -> None:
|
|
210
|
+
result = await repo._private_method("test")
|
|
211
|
+
assert result == "private_test"
|
|
212
|
+
|
|
213
|
+
asyncio.run(test_private())
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_decorated_methods_preserve_metadata() -> None:
|
|
217
|
+
"""Test that decorated methods preserve original method metadata."""
|
|
218
|
+
|
|
219
|
+
@temporal_activity_registration("test.repo")
|
|
220
|
+
class DecoratedRepository(MockRepository):
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
repo = DecoratedRepository()
|
|
224
|
+
|
|
225
|
+
# Check that method names are preserved
|
|
226
|
+
assert repo.process_payment.__name__ == "process_payment"
|
|
227
|
+
assert repo.get_payment.__name__ == "get_payment"
|
|
228
|
+
assert repo.refund_payment.__name__ == "refund_payment"
|
|
229
|
+
|
|
230
|
+
# Check that docstrings are preserved
|
|
231
|
+
assert "Mock payment processing method" in (repo.process_payment.__doc__ or "")
|
|
232
|
+
assert "Mock get payment method" in (repo.get_payment.__doc__ or "")
|
|
233
|
+
assert "Mock refund payment method" in (repo.refund_payment.__doc__ or "")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_activity_names_with_different_prefixes() -> None:
|
|
237
|
+
"""Test different prefixes generate different activity names."""
|
|
238
|
+
|
|
239
|
+
captured_activity_names = []
|
|
240
|
+
original_activity_defn = activity.defn
|
|
241
|
+
|
|
242
|
+
def mock_activity_defn(name: Optional[str] = None, **kwargs: Any) -> Any:
|
|
243
|
+
"""Mock activity.defn to capture the activity names being created."""
|
|
244
|
+
if name:
|
|
245
|
+
captured_activity_names.append(name)
|
|
246
|
+
return original_activity_defn(name=name, **kwargs)
|
|
247
|
+
|
|
248
|
+
with patch(
|
|
249
|
+
"julee.util.temporal.decorators.activity.defn",
|
|
250
|
+
side_effect=mock_activity_defn,
|
|
251
|
+
):
|
|
252
|
+
|
|
253
|
+
@temporal_activity_registration("test.payment_service")
|
|
254
|
+
class PaymentServiceRepo(MockRepository):
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
@temporal_activity_registration("test.inventory_service")
|
|
258
|
+
class InventoryServiceRepo(MockRepository):
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
# Verify that activity names were captured with the correct prefixes
|
|
262
|
+
payment_activities = [
|
|
263
|
+
name
|
|
264
|
+
for name in captured_activity_names
|
|
265
|
+
if name.startswith("test.payment_service")
|
|
266
|
+
]
|
|
267
|
+
inventory_activities = [
|
|
268
|
+
name
|
|
269
|
+
for name in captured_activity_names
|
|
270
|
+
if name.startswith("test.inventory_service")
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
# Should have created activities for each async method
|
|
274
|
+
expected_payment_activities = {
|
|
275
|
+
"test.payment_service.process_payment",
|
|
276
|
+
"test.payment_service.get_payment",
|
|
277
|
+
"test.payment_service.refund_payment",
|
|
278
|
+
"test.payment_service.base_async_method",
|
|
279
|
+
}
|
|
280
|
+
expected_inventory_activities = {
|
|
281
|
+
"test.inventory_service.process_payment",
|
|
282
|
+
"test.inventory_service.get_payment",
|
|
283
|
+
"test.inventory_service.refund_payment",
|
|
284
|
+
"test.inventory_service.base_async_method",
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
assert set(payment_activities) == expected_payment_activities
|
|
288
|
+
assert set(inventory_activities) == expected_inventory_activities
|
|
289
|
+
|
|
290
|
+
# Verify no activity names overlap between the two services
|
|
291
|
+
assert not set(payment_activities).intersection(set(inventory_activities))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_decorator_handles_inheritance_correctly() -> None:
|
|
295
|
+
"""Test that the decorator properly handles method resolution order."""
|
|
296
|
+
|
|
297
|
+
@runtime_checkable
|
|
298
|
+
class ChildRepositoryProtocol(MockRepositoryProtocol, Protocol):
|
|
299
|
+
async def child_method(self, value: str) -> str:
|
|
300
|
+
"""Child-specific method."""
|
|
301
|
+
...
|
|
302
|
+
|
|
303
|
+
class ChildRepository(ChildRepositoryProtocol):
|
|
304
|
+
async def base_async_method(self, arg1: str) -> str:
|
|
305
|
+
return f"base_result_{arg1}"
|
|
306
|
+
|
|
307
|
+
def base_sync_method(self, arg1: str) -> str:
|
|
308
|
+
return f"base_sync_{arg1}"
|
|
309
|
+
|
|
310
|
+
async def _private_async_method(self, arg1: str) -> str:
|
|
311
|
+
return f"private_{arg1}"
|
|
312
|
+
|
|
313
|
+
async def process_payment(self, order_id: str, amount: float) -> dict:
|
|
314
|
+
return {
|
|
315
|
+
"status": "success",
|
|
316
|
+
"order_id": order_id,
|
|
317
|
+
"amount": amount,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async def get_payment(self, payment_id: str) -> Optional[dict]:
|
|
321
|
+
if payment_id == "not_found":
|
|
322
|
+
return None
|
|
323
|
+
return {"payment_id": payment_id, "status": "completed"}
|
|
324
|
+
|
|
325
|
+
async def refund_payment(self, payment_id: str) -> dict:
|
|
326
|
+
return {"status": "refunded", "payment_id": payment_id}
|
|
327
|
+
|
|
328
|
+
def sync_method(self, value: str) -> str:
|
|
329
|
+
return f"sync_{value}"
|
|
330
|
+
|
|
331
|
+
async def _private_method(self, value: str) -> str:
|
|
332
|
+
return f"private_{value}"
|
|
333
|
+
|
|
334
|
+
async def child_method(self, value: str) -> str:
|
|
335
|
+
"""Child-specific method."""
|
|
336
|
+
return f"child_{value}"
|
|
337
|
+
|
|
338
|
+
@temporal_activity_registration("test.child")
|
|
339
|
+
class DecoratedChildRepository(ChildRepository):
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
# Check that all async methods are wrapped, including inherited ones
|
|
343
|
+
assert "__temporal_activity_definition" in dir(
|
|
344
|
+
DecoratedChildRepository.child_method
|
|
345
|
+
)
|
|
346
|
+
assert "__temporal_activity_definition" in dir(
|
|
347
|
+
DecoratedChildRepository.process_payment
|
|
348
|
+
)
|
|
349
|
+
assert "__temporal_activity_definition" in dir(
|
|
350
|
+
DecoratedChildRepository.base_async_method
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_decorator_logs_wrapped_methods() -> None:
|
|
355
|
+
"""Test that the decorator logs which methods it wraps."""
|
|
356
|
+
|
|
357
|
+
with patch("julee.util.temporal.decorators.logger") as mock_logger:
|
|
358
|
+
|
|
359
|
+
@temporal_activity_registration("test.logging")
|
|
360
|
+
class DecoratedRepository(MockRepository):
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
# Check that debug logs were called for each method
|
|
364
|
+
mock_logger.debug.assert_called()
|
|
365
|
+
|
|
366
|
+
# Should have one info call: decorator applied
|
|
367
|
+
assert mock_logger.info.call_count == 1
|
|
368
|
+
|
|
369
|
+
# Check that the final info log contains the expected information
|
|
370
|
+
final_info_call = mock_logger.info.call_args_list[-1]
|
|
371
|
+
assert (
|
|
372
|
+
"Temporal activity registration decorator applied" in final_info_call[0][0]
|
|
373
|
+
)
|
|
374
|
+
assert "DecoratedRepository" in final_info_call[0][0]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def test_empty_class_decorator() -> None:
|
|
378
|
+
"""Test decorator behavior with a class that has no async methods."""
|
|
379
|
+
|
|
380
|
+
@runtime_checkable
|
|
381
|
+
class EmptyRepositoryProtocol(Protocol):
|
|
382
|
+
def sync_only(self, value: str) -> str: ...
|
|
383
|
+
|
|
384
|
+
class EmptyRepository(EmptyRepositoryProtocol):
|
|
385
|
+
def sync_only(self, value: str) -> str:
|
|
386
|
+
return f"sync_{value}"
|
|
387
|
+
|
|
388
|
+
@temporal_activity_registration("test.empty")
|
|
389
|
+
class DecoratedEmptyRepository(EmptyRepository):
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
# Should still work, just no methods wrapped
|
|
393
|
+
assert "__temporal_activity_definition" not in dir(
|
|
394
|
+
DecoratedEmptyRepository.sync_only
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_decorator_type_preservation() -> None:
|
|
399
|
+
"""Test decorator preserves class type for isinstance checks."""
|
|
400
|
+
|
|
401
|
+
@temporal_activity_registration("test.types")
|
|
402
|
+
class DecoratedRepository(MockRepository):
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
repo = DecoratedRepository()
|
|
406
|
+
|
|
407
|
+
# Check that isinstance still works
|
|
408
|
+
assert isinstance(repo, DecoratedRepository)
|
|
409
|
+
assert isinstance(repo, MockRepository)
|
|
410
|
+
assert isinstance(repo, MockBaseRepositoryProtocol)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_multiple_decorations() -> None:
|
|
414
|
+
"""Test repository can be decorated multiple times with prefixes."""
|
|
415
|
+
|
|
416
|
+
@temporal_activity_registration("test.first")
|
|
417
|
+
class FirstDecoration(MockRepository):
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
@temporal_activity_registration("test.second")
|
|
421
|
+
class SecondDecoration(MockRepository):
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
# Check that each has different activity names
|
|
425
|
+
assert "__temporal_activity_definition" in dir(FirstDecoration.process_payment)
|
|
426
|
+
assert "__temporal_activity_definition" in dir(SecondDecoration.process_payment)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# Test domain models for type substitution tests
|
|
430
|
+
class MockAssemblySpecification(BaseModel):
|
|
431
|
+
"""Mock domain model for type substitution tests."""
|
|
432
|
+
|
|
433
|
+
assembly_specification_id: str
|
|
434
|
+
name: str
|
|
435
|
+
status: str = "active"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class MockDocument(BaseModel):
|
|
439
|
+
"""Another mock domain model."""
|
|
440
|
+
|
|
441
|
+
document_id: str
|
|
442
|
+
title: str
|
|
443
|
+
content: str
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# Test repository protocols
|
|
447
|
+
T = TypeVar("T", bound=BaseModel)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@runtime_checkable
|
|
451
|
+
class MockAssemblySpecificationRepository(
|
|
452
|
+
BaseRepository[MockAssemblySpecification], Protocol
|
|
453
|
+
):
|
|
454
|
+
"""Mock repository inheriting from BaseRepository with concrete type."""
|
|
455
|
+
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@runtime_checkable
|
|
460
|
+
class MockDocumentRepository(BaseRepository[MockDocument], Protocol):
|
|
461
|
+
"""Another mock repository with different concrete type."""
|
|
462
|
+
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@runtime_checkable
|
|
467
|
+
class NonGenericRepository(Protocol):
|
|
468
|
+
"""Repository that doesn't follow BaseRepository[T] pattern."""
|
|
469
|
+
|
|
470
|
+
async def get(self, id: str) -> Optional[MockDocument]: ...
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class TestTypeExtraction:
|
|
474
|
+
"""Tests for _extract_concrete_type_from_base function."""
|
|
475
|
+
|
|
476
|
+
def test_extracts_concrete_type_from_direct_inheritance(self) -> None:
|
|
477
|
+
"""Test extracting type from direct BaseRepository inheritance."""
|
|
478
|
+
concrete_type = _extract_concrete_type_from_base(
|
|
479
|
+
MockAssemblySpecificationRepository
|
|
480
|
+
)
|
|
481
|
+
assert concrete_type == MockAssemblySpecification
|
|
482
|
+
|
|
483
|
+
def test_extracts_different_concrete_types(self) -> None:
|
|
484
|
+
"""Test different repositories extract their concrete types."""
|
|
485
|
+
assembly_type = _extract_concrete_type_from_base(
|
|
486
|
+
MockAssemblySpecificationRepository
|
|
487
|
+
)
|
|
488
|
+
document_type = _extract_concrete_type_from_base(MockDocumentRepository)
|
|
489
|
+
|
|
490
|
+
assert assembly_type == MockAssemblySpecification
|
|
491
|
+
assert document_type == MockDocument
|
|
492
|
+
assert assembly_type != document_type
|
|
493
|
+
|
|
494
|
+
def test_extracts_from_proxy_class_inheritance(self) -> None:
|
|
495
|
+
"""Test extracting concrete type from workflow proxy classes."""
|
|
496
|
+
|
|
497
|
+
class TestWorkflowProxy(MockAssemblySpecificationRepository):
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
concrete_type = _extract_concrete_type_from_base(TestWorkflowProxy)
|
|
501
|
+
assert concrete_type == MockAssemblySpecification
|
|
502
|
+
|
|
503
|
+
def test_returns_none_for_non_generic_repository(self) -> None:
|
|
504
|
+
"""Test that non-generic repositories return None."""
|
|
505
|
+
concrete_type = _extract_concrete_type_from_base(NonGenericRepository)
|
|
506
|
+
assert concrete_type is None
|
|
507
|
+
|
|
508
|
+
def test_returns_none_for_object_class(self) -> None:
|
|
509
|
+
"""Test that base object class returns None."""
|
|
510
|
+
concrete_type = _extract_concrete_type_from_base(object)
|
|
511
|
+
assert concrete_type is None
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class TestTypeSubstitution:
|
|
515
|
+
"""Tests for _substitute_typevar_with_concrete function."""
|
|
516
|
+
|
|
517
|
+
def test_substitutes_direct_typevar(self) -> None:
|
|
518
|
+
"""Test direct TypeVar substitution."""
|
|
519
|
+
result = _substitute_typevar_with_concrete(T, MockAssemblySpecification)
|
|
520
|
+
assert result == MockAssemblySpecification
|
|
521
|
+
|
|
522
|
+
def test_substitutes_optional_typevar(self) -> None:
|
|
523
|
+
"""Test Optional[TypeVar] substitution."""
|
|
524
|
+
optional_t = Optional[T]
|
|
525
|
+
result = _substitute_typevar_with_concrete(
|
|
526
|
+
optional_t, MockAssemblySpecification
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Should be Optional[MockAssemblySpecification]
|
|
530
|
+
origin = get_origin(result)
|
|
531
|
+
args = get_args(result)
|
|
532
|
+
assert origin is not None
|
|
533
|
+
assert MockAssemblySpecification in args
|
|
534
|
+
assert type(None) in args
|
|
535
|
+
|
|
536
|
+
def test_substitutes_nested_generics(self) -> None:
|
|
537
|
+
"""Test substitution in nested generic types."""
|
|
538
|
+
nested_generic = List[Optional[T]]
|
|
539
|
+
result = _substitute_typevar_with_concrete(nested_generic, MockDocument)
|
|
540
|
+
|
|
541
|
+
# Should be List[Optional[MockDocument]]
|
|
542
|
+
outer_origin = get_origin(result)
|
|
543
|
+
outer_args = get_args(result)
|
|
544
|
+
assert outer_origin is list
|
|
545
|
+
assert len(outer_args) == 1
|
|
546
|
+
|
|
547
|
+
inner_type = outer_args[0]
|
|
548
|
+
inner_args = get_args(inner_type)
|
|
549
|
+
assert MockDocument in inner_args
|
|
550
|
+
assert type(None) in inner_args
|
|
551
|
+
|
|
552
|
+
def test_returns_non_generic_types_unchanged(self) -> None:
|
|
553
|
+
"""Test that non-generic types are returned unchanged."""
|
|
554
|
+
result_str = _substitute_typevar_with_concrete(str, MockAssemblySpecification)
|
|
555
|
+
result_int = _substitute_typevar_with_concrete(int, MockAssemblySpecification)
|
|
556
|
+
result_concrete = _substitute_typevar_with_concrete(
|
|
557
|
+
MockDocument, MockAssemblySpecification
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
assert result_str is str
|
|
561
|
+
assert result_int is int
|
|
562
|
+
assert result_concrete == MockDocument
|
|
563
|
+
|
|
564
|
+
def test_handles_none_annotation(self) -> None:
|
|
565
|
+
"""Test handling of None annotations."""
|
|
566
|
+
result = _substitute_typevar_with_concrete(None, MockAssemblySpecification)
|
|
567
|
+
assert result is None
|
|
568
|
+
|
|
569
|
+
def test_handles_signature_empty(self) -> None:
|
|
570
|
+
"""Test handling of inspect.Signature.empty."""
|
|
571
|
+
result = _substitute_typevar_with_concrete(
|
|
572
|
+
inspect.Signature.empty, MockAssemblySpecification
|
|
573
|
+
)
|
|
574
|
+
assert result == inspect.Signature.empty
|
|
575
|
+
|
|
576
|
+
def test_fails_fast_on_reconstruction_error(self) -> None:
|
|
577
|
+
"""Test that reconstruction errors raise informative exceptions."""
|
|
578
|
+
|
|
579
|
+
# Create a mock type that will fail reconstruction
|
|
580
|
+
class FailingOrigin:
|
|
581
|
+
def __getitem__(self, item: Any) -> Any:
|
|
582
|
+
raise TypeError("Mock reconstruction failure")
|
|
583
|
+
|
|
584
|
+
def __str__(self) -> str:
|
|
585
|
+
return "FailingOrigin"
|
|
586
|
+
|
|
587
|
+
# Mock get_origin and get_args to return our failing type
|
|
588
|
+
def mock_get_origin(annotation: Any) -> Any:
|
|
589
|
+
if annotation == "FAILING_TYPE":
|
|
590
|
+
return FailingOrigin()
|
|
591
|
+
return get_origin(annotation)
|
|
592
|
+
|
|
593
|
+
def mock_get_args(annotation: Any) -> tuple[Any, ...]:
|
|
594
|
+
if annotation == "FAILING_TYPE":
|
|
595
|
+
return (T,)
|
|
596
|
+
return get_args(annotation)
|
|
597
|
+
|
|
598
|
+
with (
|
|
599
|
+
patch.object(decorators_module, "get_origin", side_effect=mock_get_origin),
|
|
600
|
+
patch.object(decorators_module, "get_args", side_effect=mock_get_args),
|
|
601
|
+
):
|
|
602
|
+
with pytest.raises(TypeError) as exc_info:
|
|
603
|
+
_substitute_typevar_with_concrete(
|
|
604
|
+
"FAILING_TYPE", MockAssemblySpecification
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
error_message = str(exc_info.value)
|
|
608
|
+
assert "Failed to reconstruct generic type" in error_message
|
|
609
|
+
assert "FailingOrigin" in error_message
|
|
610
|
+
assert "FAILING_TYPE" in error_message
|
|
611
|
+
assert "MockAssemblySpecification" in error_message
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class TestPydanticValidationDetection:
|
|
615
|
+
"""Tests for _needs_pydantic_validation function."""
|
|
616
|
+
|
|
617
|
+
def test_detects_pydantic_model_types(self) -> None:
|
|
618
|
+
"""Test detection of Pydantic model types."""
|
|
619
|
+
assert _needs_pydantic_validation(MockAssemblySpecification)
|
|
620
|
+
assert _needs_pydantic_validation(MockDocument)
|
|
621
|
+
|
|
622
|
+
def test_detects_optional_pydantic_types(self) -> None:
|
|
623
|
+
"""Test detection of Optional[PydanticModel] types."""
|
|
624
|
+
assert _needs_pydantic_validation(Optional[MockAssemblySpecification])
|
|
625
|
+
assert _needs_pydantic_validation(Optional[MockDocument])
|
|
626
|
+
|
|
627
|
+
def test_rejects_non_pydantic_types(self) -> None:
|
|
628
|
+
"""Test that non-Pydantic types are not flagged for validation."""
|
|
629
|
+
assert not _needs_pydantic_validation(str)
|
|
630
|
+
assert not _needs_pydantic_validation(int)
|
|
631
|
+
assert not _needs_pydantic_validation(dict)
|
|
632
|
+
assert not _needs_pydantic_validation(Optional[str])
|
|
633
|
+
|
|
634
|
+
def test_rejects_typevar_types(self) -> None:
|
|
635
|
+
"""Test TypeVar types aren't flagged for validation (the bug)."""
|
|
636
|
+
assert not _needs_pydantic_validation(T)
|
|
637
|
+
assert not _needs_pydantic_validation(Optional[T])
|
|
638
|
+
|
|
639
|
+
def test_handles_none_and_empty(self) -> None:
|
|
640
|
+
"""Test handling of None and Signature.empty."""
|
|
641
|
+
assert not _needs_pydantic_validation(None)
|
|
642
|
+
assert not _needs_pydantic_validation(inspect.Signature.empty)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
class TestWorkflowProxyIntegration:
|
|
646
|
+
"""Integration tests for temporal_workflow_proxy with substitution."""
|
|
647
|
+
|
|
648
|
+
def test_extracts_type_from_proxy_class(self) -> None:
|
|
649
|
+
"""Test decorator extracts concrete types from proxy classes."""
|
|
650
|
+
|
|
651
|
+
@temporal_workflow_proxy(
|
|
652
|
+
activity_base="test.assembly_spec_repo.minio",
|
|
653
|
+
default_timeout_seconds=30,
|
|
654
|
+
)
|
|
655
|
+
class TestWorkflowAssemblySpecificationRepositoryProxy(
|
|
656
|
+
MockAssemblySpecificationRepository
|
|
657
|
+
):
|
|
658
|
+
pass
|
|
659
|
+
|
|
660
|
+
# Verify type extraction works
|
|
661
|
+
concrete_type = _extract_concrete_type_from_base(
|
|
662
|
+
TestWorkflowAssemblySpecificationRepositoryProxy
|
|
663
|
+
)
|
|
664
|
+
assert concrete_type == MockAssemblySpecification
|
|
665
|
+
|
|
666
|
+
def test_creates_proxy_methods(self) -> None:
|
|
667
|
+
"""Test that the decorator creates expected proxy methods."""
|
|
668
|
+
|
|
669
|
+
@temporal_workflow_proxy(
|
|
670
|
+
activity_base="test.document_repo.minio",
|
|
671
|
+
default_timeout_seconds=30,
|
|
672
|
+
)
|
|
673
|
+
class TestWorkflowDocumentRepositoryProxy(MockDocumentRepository):
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
proxy = TestWorkflowDocumentRepositoryProxy() # type: ignore[abstract]
|
|
677
|
+
|
|
678
|
+
# Check that methods exist
|
|
679
|
+
assert hasattr(proxy, "get")
|
|
680
|
+
assert hasattr(proxy, "save")
|
|
681
|
+
assert hasattr(proxy, "generate_id")
|
|
682
|
+
|
|
683
|
+
# Check instance attributes
|
|
684
|
+
assert hasattr(proxy, "activity_timeout")
|
|
685
|
+
assert hasattr(proxy, "activity_fail_fast_retry_policy")
|
|
686
|
+
|
|
687
|
+
def test_different_repositories_get_different_types(self) -> None:
|
|
688
|
+
"""Test that different repositories extract their respective types."""
|
|
689
|
+
|
|
690
|
+
@temporal_workflow_proxy(
|
|
691
|
+
activity_base="test.assembly_spec_repo.minio",
|
|
692
|
+
default_timeout_seconds=30,
|
|
693
|
+
)
|
|
694
|
+
class ProxyA(MockAssemblySpecificationRepository):
|
|
695
|
+
pass
|
|
696
|
+
|
|
697
|
+
@temporal_workflow_proxy(
|
|
698
|
+
activity_base="test.document_repo.minio",
|
|
699
|
+
default_timeout_seconds=30,
|
|
700
|
+
)
|
|
701
|
+
class ProxyB(MockDocumentRepository):
|
|
702
|
+
pass
|
|
703
|
+
|
|
704
|
+
type_a = _extract_concrete_type_from_base(ProxyA)
|
|
705
|
+
type_b = _extract_concrete_type_from_base(ProxyB)
|
|
706
|
+
|
|
707
|
+
assert type_a == MockAssemblySpecification
|
|
708
|
+
assert type_b == MockDocument
|
|
709
|
+
assert type_a != type_b
|
|
710
|
+
|
|
711
|
+
def test_handles_non_generic_repository_gracefully(self) -> None:
|
|
712
|
+
"""Test that non-generic repositories are handled gracefully."""
|
|
713
|
+
|
|
714
|
+
@temporal_workflow_proxy(
|
|
715
|
+
activity_base="test.non_generic_repo.minio",
|
|
716
|
+
default_timeout_seconds=30,
|
|
717
|
+
)
|
|
718
|
+
class TestNonGenericProxy(NonGenericRepository):
|
|
719
|
+
pass
|
|
720
|
+
|
|
721
|
+
# Should not raise an error
|
|
722
|
+
proxy = TestNonGenericProxy() # type: ignore[abstract]
|
|
723
|
+
assert hasattr(proxy, "get")
|
|
724
|
+
|
|
725
|
+
# Should return None for concrete type (logged but not errored)
|
|
726
|
+
concrete_type = _extract_concrete_type_from_base(TestNonGenericProxy)
|
|
727
|
+
assert concrete_type is None
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
class TestEndToEndTypeSubstitution:
|
|
731
|
+
"""End-to-end tests demonstrating the complete type substitution fix."""
|
|
732
|
+
|
|
733
|
+
def test_type_substitution_enables_pydantic_validation(self) -> None:
|
|
734
|
+
"""Test type substitution enables Pydantic validation."""
|
|
735
|
+
# Simulate the problematic method signature: Optional[~T]
|
|
736
|
+
original_annotation = Optional[T]
|
|
737
|
+
|
|
738
|
+
# Before fix: TypeVar prevents validation
|
|
739
|
+
assert not _needs_pydantic_validation(original_annotation)
|
|
740
|
+
|
|
741
|
+
# After substitution: Concrete type enables validation
|
|
742
|
+
substituted_annotation = _substitute_typevar_with_concrete(
|
|
743
|
+
original_annotation, MockAssemblySpecification
|
|
744
|
+
)
|
|
745
|
+
assert _needs_pydantic_validation(substituted_annotation)
|
|
746
|
+
|
|
747
|
+
def test_demonstrates_original_problem_and_solution(self) -> None:
|
|
748
|
+
"""Test dict vs Pydantic object problem and solution."""
|
|
749
|
+
# Create test data
|
|
750
|
+
test_spec = MockAssemblySpecification(
|
|
751
|
+
assembly_specification_id="test-123",
|
|
752
|
+
name="Test Spec",
|
|
753
|
+
status="active",
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# Convert to dict (simulates what Temporal activity returns)
|
|
757
|
+
activity_result_dict = test_spec.model_dump(mode="json")
|
|
758
|
+
|
|
759
|
+
# Demonstrate the problem: dict doesn't have Pydantic attributes
|
|
760
|
+
assert isinstance(activity_result_dict, dict)
|
|
761
|
+
with pytest.raises(AttributeError):
|
|
762
|
+
# This would fail because dict doesn't have the attribute
|
|
763
|
+
getattr(activity_result_dict, "assembly_specification_id")
|
|
764
|
+
|
|
765
|
+
# Demonstrate the solution: reconstruct Pydantic object
|
|
766
|
+
reconstructed = MockAssemblySpecification.model_validate(activity_result_dict)
|
|
767
|
+
assert isinstance(reconstructed, MockAssemblySpecification)
|
|
768
|
+
assert reconstructed.assembly_specification_id == "test-123" # This works
|
|
769
|
+
assert reconstructed.name == "Test Spec"
|
|
770
|
+
assert reconstructed.status == "active"
|