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.
Files changed (161) hide show
  1. julee/__init__.py +3 -0
  2. julee/api/__init__.py +20 -0
  3. julee/api/app.py +180 -0
  4. julee/api/dependencies.py +257 -0
  5. julee/api/requests.py +175 -0
  6. julee/api/responses.py +43 -0
  7. julee/api/routers/__init__.py +43 -0
  8. julee/api/routers/assembly_specifications.py +212 -0
  9. julee/api/routers/documents.py +182 -0
  10. julee/api/routers/knowledge_service_configs.py +79 -0
  11. julee/api/routers/knowledge_service_queries.py +293 -0
  12. julee/api/routers/system.py +137 -0
  13. julee/api/routers/workflows.py +234 -0
  14. julee/api/services/__init__.py +20 -0
  15. julee/api/services/system_initialization.py +214 -0
  16. julee/api/tests/__init__.py +14 -0
  17. julee/api/tests/routers/__init__.py +17 -0
  18. julee/api/tests/routers/test_assembly_specifications.py +749 -0
  19. julee/api/tests/routers/test_documents.py +301 -0
  20. julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
  21. julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
  22. julee/api/tests/routers/test_system.py +179 -0
  23. julee/api/tests/routers/test_workflows.py +393 -0
  24. julee/api/tests/test_app.py +285 -0
  25. julee/api/tests/test_dependencies.py +245 -0
  26. julee/api/tests/test_requests.py +250 -0
  27. julee/domain/__init__.py +22 -0
  28. julee/domain/models/__init__.py +49 -0
  29. julee/domain/models/assembly/__init__.py +17 -0
  30. julee/domain/models/assembly/assembly.py +103 -0
  31. julee/domain/models/assembly/tests/__init__.py +0 -0
  32. julee/domain/models/assembly/tests/factories.py +37 -0
  33. julee/domain/models/assembly/tests/test_assembly.py +430 -0
  34. julee/domain/models/assembly_specification/__init__.py +24 -0
  35. julee/domain/models/assembly_specification/assembly_specification.py +172 -0
  36. julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
  37. julee/domain/models/assembly_specification/tests/__init__.py +0 -0
  38. julee/domain/models/assembly_specification/tests/factories.py +78 -0
  39. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
  40. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
  41. julee/domain/models/custom_fields/__init__.py +0 -0
  42. julee/domain/models/custom_fields/content_stream.py +68 -0
  43. julee/domain/models/custom_fields/tests/__init__.py +0 -0
  44. julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
  45. julee/domain/models/document/__init__.py +17 -0
  46. julee/domain/models/document/document.py +150 -0
  47. julee/domain/models/document/tests/__init__.py +0 -0
  48. julee/domain/models/document/tests/factories.py +76 -0
  49. julee/domain/models/document/tests/test_document.py +297 -0
  50. julee/domain/models/knowledge_service_config/__init__.py +17 -0
  51. julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
  52. julee/domain/models/policy/__init__.py +15 -0
  53. julee/domain/models/policy/document_policy_validation.py +220 -0
  54. julee/domain/models/policy/policy.py +203 -0
  55. julee/domain/models/policy/tests/__init__.py +0 -0
  56. julee/domain/models/policy/tests/factories.py +47 -0
  57. julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
  58. julee/domain/models/policy/tests/test_policy.py +546 -0
  59. julee/domain/repositories/__init__.py +27 -0
  60. julee/domain/repositories/assembly.py +45 -0
  61. julee/domain/repositories/assembly_specification.py +52 -0
  62. julee/domain/repositories/base.py +146 -0
  63. julee/domain/repositories/document.py +49 -0
  64. julee/domain/repositories/document_policy_validation.py +52 -0
  65. julee/domain/repositories/knowledge_service_config.py +54 -0
  66. julee/domain/repositories/knowledge_service_query.py +44 -0
  67. julee/domain/repositories/policy.py +49 -0
  68. julee/domain/use_cases/__init__.py +17 -0
  69. julee/domain/use_cases/decorators.py +107 -0
  70. julee/domain/use_cases/extract_assemble_data.py +649 -0
  71. julee/domain/use_cases/initialize_system_data.py +842 -0
  72. julee/domain/use_cases/tests/__init__.py +7 -0
  73. julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
  74. julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
  75. julee/domain/use_cases/tests/test_validate_document.py +1228 -0
  76. julee/domain/use_cases/validate_document.py +736 -0
  77. julee/fixtures/assembly_specifications.yaml +70 -0
  78. julee/fixtures/documents.yaml +178 -0
  79. julee/fixtures/knowledge_service_configs.yaml +37 -0
  80. julee/fixtures/knowledge_service_queries.yaml +27 -0
  81. julee/repositories/__init__.py +17 -0
  82. julee/repositories/memory/__init__.py +31 -0
  83. julee/repositories/memory/assembly.py +84 -0
  84. julee/repositories/memory/assembly_specification.py +125 -0
  85. julee/repositories/memory/base.py +227 -0
  86. julee/repositories/memory/document.py +149 -0
  87. julee/repositories/memory/document_policy_validation.py +104 -0
  88. julee/repositories/memory/knowledge_service_config.py +123 -0
  89. julee/repositories/memory/knowledge_service_query.py +120 -0
  90. julee/repositories/memory/policy.py +87 -0
  91. julee/repositories/memory/tests/__init__.py +0 -0
  92. julee/repositories/memory/tests/test_document.py +212 -0
  93. julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
  94. julee/repositories/memory/tests/test_policy.py +443 -0
  95. julee/repositories/minio/__init__.py +31 -0
  96. julee/repositories/minio/assembly.py +103 -0
  97. julee/repositories/minio/assembly_specification.py +170 -0
  98. julee/repositories/minio/client.py +570 -0
  99. julee/repositories/minio/document.py +530 -0
  100. julee/repositories/minio/document_policy_validation.py +120 -0
  101. julee/repositories/minio/knowledge_service_config.py +187 -0
  102. julee/repositories/minio/knowledge_service_query.py +211 -0
  103. julee/repositories/minio/policy.py +106 -0
  104. julee/repositories/minio/tests/__init__.py +0 -0
  105. julee/repositories/minio/tests/fake_client.py +213 -0
  106. julee/repositories/minio/tests/test_assembly.py +374 -0
  107. julee/repositories/minio/tests/test_assembly_specification.py +391 -0
  108. julee/repositories/minio/tests/test_client_protocol.py +57 -0
  109. julee/repositories/minio/tests/test_document.py +591 -0
  110. julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
  111. julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
  112. julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
  113. julee/repositories/minio/tests/test_policy.py +559 -0
  114. julee/repositories/temporal/__init__.py +38 -0
  115. julee/repositories/temporal/activities.py +114 -0
  116. julee/repositories/temporal/activity_names.py +34 -0
  117. julee/repositories/temporal/proxies.py +159 -0
  118. julee/services/__init__.py +18 -0
  119. julee/services/knowledge_service/__init__.py +48 -0
  120. julee/services/knowledge_service/anthropic/__init__.py +12 -0
  121. julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
  122. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
  123. julee/services/knowledge_service/factory.py +138 -0
  124. julee/services/knowledge_service/knowledge_service.py +160 -0
  125. julee/services/knowledge_service/memory/__init__.py +13 -0
  126. julee/services/knowledge_service/memory/knowledge_service.py +278 -0
  127. julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
  128. julee/services/knowledge_service/test_factory.py +112 -0
  129. julee/services/temporal/__init__.py +38 -0
  130. julee/services/temporal/activities.py +86 -0
  131. julee/services/temporal/activity_names.py +22 -0
  132. julee/services/temporal/proxies.py +41 -0
  133. julee/util/__init__.py +0 -0
  134. julee/util/domain.py +119 -0
  135. julee/util/repos/__init__.py +0 -0
  136. julee/util/repos/minio/__init__.py +0 -0
  137. julee/util/repos/minio/file_storage.py +213 -0
  138. julee/util/repos/temporal/__init__.py +11 -0
  139. julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
  140. julee/util/repos/temporal/data_converter.py +123 -0
  141. julee/util/repos/temporal/minio_file_storage.py +12 -0
  142. julee/util/repos/temporal/proxies/__init__.py +0 -0
  143. julee/util/repos/temporal/proxies/file_storage.py +58 -0
  144. julee/util/repositories.py +55 -0
  145. julee/util/temporal/__init__.py +22 -0
  146. julee/util/temporal/activities.py +123 -0
  147. julee/util/temporal/decorators.py +473 -0
  148. julee/util/tests/__init__.py +1 -0
  149. julee/util/tests/test_decorators.py +770 -0
  150. julee/util/validation/__init__.py +29 -0
  151. julee/util/validation/repository.py +100 -0
  152. julee/util/validation/type_guards.py +369 -0
  153. julee/worker.py +211 -0
  154. julee/workflows/__init__.py +26 -0
  155. julee/workflows/extract_assemble.py +215 -0
  156. julee/workflows/validate_document.py +228 -0
  157. julee-0.1.0.dist-info/METADATA +195 -0
  158. julee-0.1.0.dist-info/RECORD +161 -0
  159. julee-0.1.0.dist-info/WHEEL +5 -0
  160. julee-0.1.0.dist-info/licenses/LICENSE +674 -0
  161. julee-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,123 @@
1
+ """
2
+ Utilities for working with Temporal activities.
3
+
4
+ This module provides helper functions for automatically discovering and
5
+ collecting activity methods from decorated instances, eliminating the need
6
+ for manual activity registration boilerplate.
7
+ """
8
+
9
+ import inspect
10
+ import logging
11
+ from typing import Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def discover_protocol_methods(
17
+ cls_hierarchy: tuple[type, ...],
18
+ ) -> dict[str, Any]:
19
+ """
20
+ Discover protocol methods that should be wrapped as activities.
21
+
22
+ This function finds async methods defined in protocol interfaces within
23
+ a class hierarchy. It's used to automatically collect methods that should
24
+ be registered as Temporal activities.
25
+
26
+ Args:
27
+ cls_hierarchy: The class MRO (method resolution order)
28
+
29
+ Returns:
30
+ Dict mapping method names to method objects from the concrete class
31
+ """
32
+ methods_to_wrap = {}
33
+ concrete_class = cls_hierarchy[0] # The actual class being decorated
34
+
35
+ # Look for protocol interfaces (classes with runtime_checkable/Protocol)
36
+ for base_class in cls_hierarchy:
37
+ # Skip object base class
38
+ if base_class is object:
39
+ continue
40
+
41
+ # Check if this is a protocol class
42
+ has_protocol_attr = hasattr(base_class, "__protocol__")
43
+ has_is_protocol = getattr(base_class, "_is_protocol", False)
44
+ has_protocol_in_str = "Protocol" in str(base_class)
45
+
46
+ is_protocol = has_protocol_attr or has_is_protocol or has_protocol_in_str
47
+
48
+ # Only process protocol classes for architectural compliance
49
+ if is_protocol:
50
+ # Get method names defined in this class, but get the actual
51
+ # implementation from the concrete class
52
+ for name in base_class.__dict__:
53
+ if name in methods_to_wrap:
54
+ continue # Already found this method
55
+
56
+ base_method = getattr(base_class, name)
57
+ # Only wrap async methods that don't start with underscore
58
+ if inspect.iscoroutinefunction(base_method) and not name.startswith(
59
+ "_"
60
+ ):
61
+ # Get the concrete implementation from the actual class
62
+ if hasattr(concrete_class, name):
63
+ concrete_method = getattr(concrete_class, name)
64
+ methods_to_wrap[name] = concrete_method
65
+
66
+ # Log final results
67
+ final_method_names = list(methods_to_wrap.keys())
68
+ logger.debug(f"Method discovery found {len(methods_to_wrap)}: {final_method_names}")
69
+ return methods_to_wrap
70
+
71
+
72
+ def collect_activities_from_instances(*instances: Any) -> list[Any]:
73
+ """Automatically collect all activity methods from decorated instances.
74
+
75
+ Uses protocol method discovery to find and collect all methods that have
76
+ been wrapped as Temporal activities by the @temporal_activity_registration
77
+ decorator. This ensures we don't miss any activities and eliminates
78
+ boilerplate registration code.
79
+
80
+ Args:
81
+ *instances: Repository and service instances decorated with
82
+ @temporal_activity_registration
83
+
84
+ Returns:
85
+ List of activity methods ready for Worker registration
86
+
87
+ Example:
88
+ # Instead of manually listing all activities:
89
+ activities = [
90
+ repo.generate_id,
91
+ repo.save,
92
+ repo.get,
93
+ # ... many more lines
94
+ ]
95
+
96
+ # Use automatic discovery:
97
+ activities = collect_activities_from_instances(
98
+ temporal_assembly_repo,
99
+ temporal_document_repo,
100
+ temporal_knowledge_service,
101
+ )
102
+ """
103
+ activities = []
104
+
105
+ for instance in instances:
106
+ # Use the same discovery logic as the decorator
107
+ methods_to_collect = discover_protocol_methods(instance.__class__.__mro__)
108
+
109
+ # Get the actual bound methods from the instance
110
+ for method_name in methods_to_collect:
111
+ if hasattr(instance, method_name):
112
+ bound_method = getattr(instance, method_name)
113
+ activities.append(bound_method)
114
+ logger.debug(
115
+ f"Collected activity method: "
116
+ f"{instance.__class__.__name__}.{method_name}"
117
+ )
118
+
119
+ logger.info(
120
+ f"Collected {len(activities)} activities from " f"{len(instances)} instances"
121
+ )
122
+
123
+ return activities
@@ -0,0 +1,473 @@
1
+ """
2
+ Temporal decorators for automatically creating activities and workflow proxies
3
+
4
+ This module provides decorators that automatically:
5
+ 1. Wrap protocol methods as Temporal activities
6
+ 2. Generate workflow proxy classes that delegate to activities
7
+ Both reduce boilerplate and ensure consistent patterns.
8
+ """
9
+
10
+ import functools
11
+ import inspect
12
+ import logging
13
+ from datetime import timedelta
14
+ from typing import (
15
+ Any,
16
+ Callable,
17
+ Optional,
18
+ Type,
19
+ TypeVar,
20
+ get_args,
21
+ get_origin,
22
+ )
23
+
24
+ from pydantic import BaseModel
25
+ from temporalio import activity, workflow
26
+ from temporalio.common import RetryPolicy
27
+
28
+ from julee.domain.repositories.base import BaseRepository
29
+
30
+ from .activities import discover_protocol_methods
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ T = TypeVar("T")
35
+
36
+
37
+ def _extract_concrete_type_from_base(cls: type) -> Optional[type]:
38
+ """
39
+ Extract the concrete type argument from a generic base class.
40
+
41
+ For example, if a class inherits from
42
+ BaseRepository[AssemblySpecification], this function will return
43
+ AssemblySpecification.
44
+
45
+ Args:
46
+ cls: Class to analyze for generic base types
47
+
48
+ Returns:
49
+ The concrete type if found, None otherwise
50
+ """
51
+ # Check the class hierarchy for generic base classes
52
+ for base in cls.__mro__:
53
+ # Check if this class has __orig_bases__ (generic type information)
54
+ if hasattr(base, "__orig_bases__"):
55
+ for orig_base in base.__orig_bases__:
56
+ origin = get_origin(orig_base)
57
+ if origin is not None:
58
+ args = get_args(orig_base)
59
+ # Look for BaseRepository[ConcreteType] pattern
60
+ if origin is BaseRepository and len(args) == 1:
61
+ concrete_type = args[0]
62
+ # Make sure it's a concrete type, not another TypeVar
63
+ if not isinstance(concrete_type, TypeVar):
64
+ logger.debug(
65
+ f"Extracted concrete type {concrete_type} "
66
+ f"from {orig_base}"
67
+ )
68
+ return concrete_type # type: ignore[no-any-return]
69
+ return None
70
+
71
+
72
+ def _substitute_typevar_with_concrete(annotation: Any, concrete_type: type) -> Any:
73
+ """
74
+ Substitute TypeVar instances with concrete type in type annotations.
75
+
76
+ Args:
77
+ annotation: Type annotation that may contain TypeVars
78
+ concrete_type: Concrete type to substitute for TypeVars
79
+
80
+ Returns:
81
+ Type annotation with TypeVars replaced by concrete type
82
+
83
+ Raises:
84
+ TypeError: If type reconstruction fails during substitution
85
+ """
86
+ if isinstance(annotation, TypeVar):
87
+ return concrete_type
88
+
89
+ origin = get_origin(annotation)
90
+ if origin is not None:
91
+ args = get_args(annotation)
92
+ if args:
93
+ # Recursively substitute in generic arguments
94
+ new_args = tuple(
95
+ _substitute_typevar_with_concrete(arg, concrete_type) for arg in args
96
+ )
97
+ # Reconstruct the generic type with substituted arguments
98
+ try:
99
+ return origin[new_args] # type: ignore
100
+ except TypeError as e:
101
+ # Fail fast - type reconstruction should work if substituting
102
+ raise TypeError(
103
+ f"Failed to reconstruct generic type {origin} with "
104
+ f"args {new_args}. "
105
+ f"Original annotation: {annotation}, "
106
+ f"concrete type: {concrete_type}. "
107
+ f"This indicates an issue with type substitution logic."
108
+ ) from e
109
+
110
+ # origin is None - normal for non-generic types like str, bool, etc.
111
+ return annotation
112
+
113
+
114
+ def temporal_activity_registration(
115
+ activity_prefix: str,
116
+ ) -> Callable[[Type[T]], Type[T]]:
117
+ """
118
+ Class decorator that wraps async protocol methods as Temporal activities.
119
+
120
+ This decorator inspects the class and wraps all async methods (coroutine
121
+ functions) that don't start with underscore as Temporal activities. The
122
+ activity names are generated using the provided prefix and the method
123
+ name.
124
+
125
+ Args:
126
+ activity_prefix: Prefix for activity names (e.g.,
127
+ "sample.payment_repo.minio") Method names will be appended to create
128
+ full activity names like "sample.payment_repo.minio.process_payment"
129
+
130
+ Returns:
131
+ The decorated class with all async methods wrapped as Temporal
132
+ activities
133
+
134
+ Example:
135
+ @temporal_activity_registration("sample.payment_repo.minio")
136
+ class TemporalMinioPaymentRepository(MinioPaymentRepository):
137
+ pass
138
+
139
+ # This automatically creates activities for all protocol methods:
140
+ # - process_payment ->
141
+ # "sample.payment_repo.minio.process_payment"
142
+ # - get_payment ->
143
+ # "sample.payment_repo.minio.get_payment"
144
+ # - refund_payment ->
145
+ # "sample.payment_repo.minio.refund_payment"
146
+ """
147
+
148
+ def decorator(cls: Type[T]) -> Type[T]:
149
+ logger.debug(
150
+ f"Applying temporal_activity_registration decorator to {cls.__name__}"
151
+ )
152
+
153
+ # Track which methods we wrap for logging
154
+ wrapped_methods = []
155
+
156
+ # Use common method discovery - for activities, wrap protocol methods
157
+ async_methods_to_wrap = discover_protocol_methods(cls.__mro__)
158
+
159
+ # Now wrap all the async methods we found
160
+ for name, method in async_methods_to_wrap.items():
161
+ # Create activity name by combining prefix and method name
162
+ activity_name = f"{activity_prefix}.{name}"
163
+
164
+ logger.debug(f"Wrapping method {name} as activity {activity_name}")
165
+
166
+ # Create a new method that calls the original to avoid decorator
167
+ # conflicts while preserving the exact signature for Pydantic
168
+ def create_wrapper_method(
169
+ original_method: Callable[..., Any], method_name: str
170
+ ) -> Callable[..., Any]:
171
+ # Create wrapper with preserved signature for proper type
172
+ # conversion
173
+
174
+ @functools.wraps(original_method)
175
+ async def wrapper_method(*args: Any, **kwargs: Any) -> Any:
176
+ return await original_method(*args, **kwargs)
177
+
178
+ # Preserve method metadata explicitly
179
+ wrapper_method.__name__ = method_name
180
+ wrapper_method.__qualname__ = f"{cls.__name__}.{method_name}"
181
+ wrapper_method.__doc__ = original_method.__doc__
182
+ wrapper_method.__annotations__ = getattr(
183
+ original_method, "__annotations__", {}
184
+ )
185
+
186
+ return wrapper_method
187
+
188
+ # Create the wrapper and apply activity decorator
189
+ wrapper_method = create_wrapper_method(method, name)
190
+ wrapped_method = activity.defn(name=activity_name)(wrapper_method)
191
+
192
+ # Replace the method on the class with the wrapped version
193
+ setattr(cls, name, wrapped_method)
194
+
195
+ wrapped_methods.append(name)
196
+
197
+ logger.info(
198
+ f"Temporal activity registration decorator applied to {cls.__name__}",
199
+ extra={
200
+ "wrapped_methods": wrapped_methods,
201
+ "activity_prefix": activity_prefix,
202
+ },
203
+ )
204
+
205
+ return cls
206
+
207
+ return decorator
208
+
209
+
210
+ def temporal_workflow_proxy(
211
+ activity_base: str,
212
+ default_timeout_seconds: int = 30,
213
+ retry_methods: Optional[list[str]] = None,
214
+ ) -> Callable[[Type[T]], Type[T]]:
215
+ """
216
+ Class decorator that automatically creates workflow proxy methods that
217
+ delegate to Temporal activities.
218
+
219
+ This decorator inspects the protocol/interface being implemented and
220
+ generates methods that call workflow.execute_activity with the appropriate
221
+ activity names, timeouts, and retry policies.
222
+
223
+ Args:
224
+ activity_base: Base activity name (e.g., "julee.document_repo.minio")
225
+ default_timeout_seconds: Default timeout for activities in seconds
226
+ retry_methods: List of method names that should use retry policies
227
+
228
+ Returns:
229
+ The decorated class with all protocol methods implemented as
230
+ workflow activity calls
231
+
232
+ Example:
233
+ @temporal_workflow_proxy(
234
+ "julee.document_repo.minio",
235
+ default_timeout_seconds=30,
236
+ retry_methods=["save", "generate_id"]
237
+ )
238
+ class WorkflowDocumentRepositoryProxy(DocumentRepository):
239
+ pass
240
+
241
+ # This automatically creates workflow methods for all methods:
242
+ # - get() -> calls "julee.document_repo.minio.get" activity
243
+ # - save() -> calls "julee.document_repo.minio.save" with retry
244
+ # - generate_id() -> calls "julee.document_repo.minio.generate_id"
245
+ # with retry
246
+ """
247
+
248
+ def decorator(cls: Type[T]) -> Type[T]:
249
+ logger.debug(f"Applying temporal_workflow_proxy decorator to {cls.__name__}")
250
+
251
+ retry_methods_set = set(retry_methods or [])
252
+
253
+ # Create default retry policy for methods that need it
254
+ fail_fast_retry_policy = RetryPolicy(
255
+ initial_interval=timedelta(seconds=1),
256
+ maximum_attempts=1,
257
+ backoff_coefficient=1.0,
258
+ maximum_interval=timedelta(seconds=1),
259
+ )
260
+
261
+ # Use method discovery - for workflow proxies, wrap protocol methods
262
+ methods_to_implement = discover_protocol_methods(cls.__mro__)
263
+
264
+ # Generate workflow proxy methods
265
+ wrapped_methods = []
266
+
267
+ for method_name, original_method in methods_to_implement.items():
268
+ logger.debug(
269
+ f"Creating workflow proxy method {method_name} -> "
270
+ f"{activity_base}.{method_name}"
271
+ )
272
+
273
+ # Get method signature for type hints
274
+ sig = inspect.signature(original_method)
275
+ return_annotation = sig.return_annotation
276
+
277
+ # Try to extract concrete type from class inheritance
278
+ concrete_type = _extract_concrete_type_from_base(cls)
279
+
280
+ # Substitute TypeVars with concrete type if found
281
+ if concrete_type is not None:
282
+ return_annotation = _substitute_typevar_with_concrete(
283
+ return_annotation, concrete_type
284
+ )
285
+ logger.debug(
286
+ f"Substituted TypeVar in {method_name} return type: "
287
+ f"{sig.return_annotation} -> {return_annotation}"
288
+ )
289
+ else:
290
+ # Log when we couldn't extract concrete type - might indicate
291
+ # a repository that doesn't follow BaseRepository[T] pattern
292
+ logger.debug(
293
+ f"No concrete type for {cls.__name__}.{method_name}. "
294
+ f"Return annotation: {sig.return_annotation}. "
295
+ f"If Pydantic objects returned, ensure repo inherits "
296
+ f"from BaseRepository[ConcreteType]."
297
+ )
298
+
299
+ # Determine if return type needs Pydantic validation
300
+ needs_validation = _needs_pydantic_validation(return_annotation)
301
+ is_optional = _is_optional_type(return_annotation)
302
+ inner_type = (
303
+ _get_optional_inner_type(return_annotation)
304
+ if is_optional
305
+ else return_annotation
306
+ )
307
+
308
+ def create_workflow_method(
309
+ method_name: str,
310
+ needs_validation: bool,
311
+ is_optional: bool,
312
+ inner_type: Any,
313
+ original_method: Any,
314
+ ) -> Callable[..., Any]:
315
+ @functools.wraps(original_method)
316
+ async def workflow_method(self: Any, *args: Any, **kwargs: Any) -> Any:
317
+ # Create activity name
318
+ activity_name = f"{activity_base}.{method_name}"
319
+
320
+ # Set up activity options
321
+ activity_timeout = timedelta(seconds=default_timeout_seconds)
322
+ retry_policy = None
323
+
324
+ # Add retry policy if this method needs it
325
+ if method_name in retry_methods_set:
326
+ retry_policy = fail_fast_retry_policy
327
+
328
+ # Log the call
329
+ logger.debug(
330
+ f"Workflow: Calling {method_name} activity",
331
+ extra={
332
+ "activity_name": activity_name,
333
+ "args_count": len(args),
334
+ "kwargs_count": len(kwargs),
335
+ },
336
+ )
337
+
338
+ # Prepare arguments (exclude self)
339
+ activity_args = args if args else ()
340
+
341
+ # Handle kwargs - Temporal doesn't support kwargs directly
342
+ if kwargs:
343
+ raise ValueError(
344
+ f"Keyword arguments not supported in workflow "
345
+ f"proxy for {method_name}. Temporal activities "
346
+ f"only accept positional arguments. Please "
347
+ f"modify the calling code to use positional "
348
+ f"arguments instead of: {list(kwargs.keys())}"
349
+ )
350
+
351
+ # Execute the activity
352
+ if activity_args:
353
+ raw_result = await workflow.execute_activity(
354
+ activity_name,
355
+ args=activity_args,
356
+ start_to_close_timeout=activity_timeout,
357
+ retry_policy=retry_policy,
358
+ )
359
+ else:
360
+ raw_result = await workflow.execute_activity(
361
+ activity_name,
362
+ start_to_close_timeout=activity_timeout,
363
+ retry_policy=retry_policy,
364
+ )
365
+
366
+ # Handle return value validation
367
+ result = raw_result
368
+ if needs_validation and raw_result is not None:
369
+ if hasattr(inner_type, "model_validate"):
370
+ result = inner_type.model_validate(
371
+ raw_result,
372
+ context={"temporal_validation": True},
373
+ )
374
+ else:
375
+ # For other types, just return as-is
376
+ result = raw_result
377
+ elif (
378
+ is_optional
379
+ and raw_result is not None
380
+ and hasattr(inner_type, "model_validate")
381
+ ):
382
+ result = inner_type.model_validate(
383
+ raw_result, context={"temporal_validation": True}
384
+ )
385
+
386
+ # Log completion
387
+ logger.debug(
388
+ f"Workflow: {method_name} activity completed",
389
+ extra={
390
+ "activity_name": activity_name,
391
+ "result_type": type(result).__name__,
392
+ },
393
+ )
394
+
395
+ return result
396
+
397
+ return workflow_method
398
+
399
+ # Create and set the method on the class
400
+ workflow_method = create_workflow_method(
401
+ method_name,
402
+ needs_validation,
403
+ is_optional,
404
+ inner_type,
405
+ original_method,
406
+ )
407
+ setattr(cls, method_name, workflow_method)
408
+ wrapped_methods.append(method_name)
409
+
410
+ # Always generate __init__ that calls super() for consistent init
411
+ def __init__(proxy_self: Any) -> None:
412
+ # Call parent __init__ to preserve any existing init logic
413
+ super(cls, proxy_self).__init__()
414
+ # Set instance variables for consistency with manual pattern
415
+ proxy_self.activity_timeout = timedelta(seconds=default_timeout_seconds)
416
+ proxy_self.activity_fail_fast_retry_policy = fail_fast_retry_policy
417
+ logger.debug(f"Initialized {cls.__name__}")
418
+
419
+ setattr(cls, "__init__", __init__)
420
+
421
+ logger.info(
422
+ f"Temporal workflow proxy decorator applied to {cls.__name__}",
423
+ extra={
424
+ "wrapped_methods": wrapped_methods,
425
+ "activity_base": activity_base,
426
+ "default_timeout_seconds": default_timeout_seconds,
427
+ "retry_methods": list(retry_methods_set),
428
+ },
429
+ )
430
+
431
+ return cls
432
+
433
+ return decorator
434
+
435
+
436
+ def _needs_pydantic_validation(annotation: Any) -> bool:
437
+ """Check if a type annotation indicates a Pydantic model."""
438
+ if annotation is None or annotation == inspect.Signature.empty:
439
+ return False
440
+
441
+ # Handle Optional types
442
+ if _is_optional_type(annotation):
443
+ inner_type = _get_optional_inner_type(annotation)
444
+ return _is_pydantic_model(inner_type)
445
+
446
+ return _is_pydantic_model(annotation)
447
+
448
+
449
+ def _is_pydantic_model(type_hint: Any) -> bool:
450
+ """Check if a type is a Pydantic model."""
451
+ if inspect.isclass(type_hint) and issubclass(type_hint, BaseModel):
452
+ return True
453
+ return False
454
+
455
+
456
+ def _is_optional_type(annotation: Any) -> bool:
457
+ """Check if a type annotation is Optional[T]."""
458
+ origin = get_origin(annotation)
459
+ if origin is not None:
460
+ args = get_args(annotation)
461
+ # Optional[T] is Union[T, None]
462
+ return (hasattr(origin, "__name__") and origin.__name__ == "UnionType") or (
463
+ str(origin) == "typing.Union" and len(args) == 2 and type(None) in args
464
+ )
465
+ return False
466
+
467
+
468
+ def _get_optional_inner_type(annotation: Any) -> Any:
469
+ """Get the inner type from Optional[T]."""
470
+ args = get_args(annotation)
471
+ if len(args) == 2:
472
+ return args[0] if args[1] is type(None) else args[1]
473
+ return annotation
@@ -0,0 +1 @@
1
+ # Empty __init__.py file to make util/tests a Python package