julee 0.1.1__py3-none-any.whl → 0.1.3__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/api/app.py +9 -8
- julee/api/dependencies.py +15 -15
- julee/api/requests.py +10 -9
- julee/api/responses.py +2 -1
- julee/api/routers/__init__.py +5 -5
- julee/api/routers/assembly_specifications.py +5 -4
- julee/api/routers/documents.py +1 -1
- julee/api/routers/knowledge_service_configs.py +4 -3
- julee/api/routers/knowledge_service_queries.py +7 -6
- julee/api/routers/system.py +4 -3
- julee/api/routers/workflows.py +4 -5
- julee/api/services/system_initialization.py +6 -6
- julee/api/tests/routers/test_assembly_specifications.py +4 -3
- julee/api/tests/routers/test_documents.py +5 -4
- julee/api/tests/routers/test_knowledge_service_configs.py +7 -6
- julee/api/tests/routers/test_knowledge_service_queries.py +4 -3
- julee/api/tests/routers/test_system.py +5 -4
- julee/api/tests/routers/test_workflows.py +5 -4
- julee/api/tests/test_app.py +5 -4
- julee/api/tests/test_dependencies.py +3 -2
- julee/api/tests/test_requests.py +2 -1
- julee/contrib/__init__.py +15 -0
- julee/contrib/polling/__init__.py +47 -0
- julee/contrib/polling/domain/__init__.py +17 -0
- julee/contrib/polling/domain/models/__init__.py +13 -0
- julee/contrib/polling/domain/models/polling_config.py +39 -0
- julee/contrib/polling/domain/services/__init__.py +11 -0
- julee/contrib/polling/domain/services/poller.py +39 -0
- julee/contrib/polling/infrastructure/__init__.py +15 -0
- julee/contrib/polling/infrastructure/services/__init__.py +12 -0
- julee/contrib/polling/infrastructure/services/polling/__init__.py +12 -0
- julee/contrib/polling/infrastructure/services/polling/http/__init__.py +12 -0
- julee/contrib/polling/infrastructure/services/polling/http/http_poller_service.py +80 -0
- julee/contrib/polling/infrastructure/temporal/__init__.py +20 -0
- julee/contrib/polling/infrastructure/temporal/activities.py +42 -0
- julee/contrib/polling/infrastructure/temporal/activity_names.py +20 -0
- julee/contrib/polling/infrastructure/temporal/proxies.py +45 -0
- julee/contrib/polling/tests/__init__.py +6 -0
- julee/contrib/polling/tests/unit/__init__.py +6 -0
- julee/contrib/polling/tests/unit/infrastructure/__init__.py +7 -0
- julee/contrib/polling/tests/unit/infrastructure/services/__init__.py +6 -0
- julee/contrib/polling/tests/unit/infrastructure/services/polling/__init__.py +6 -0
- julee/contrib/polling/tests/unit/infrastructure/services/polling/http/__init__.py +7 -0
- julee/contrib/polling/tests/unit/infrastructure/services/polling/http/test_http_poller_service.py +163 -0
- julee/docs/__init__.py +5 -0
- julee/docs/sphinx_hcd/__init__.py +82 -0
- julee/docs/sphinx_hcd/accelerators.py +1078 -0
- julee/docs/sphinx_hcd/apps.py +499 -0
- julee/docs/sphinx_hcd/config.py +148 -0
- julee/docs/sphinx_hcd/epics.py +448 -0
- julee/docs/sphinx_hcd/integrations.py +306 -0
- julee/docs/sphinx_hcd/journeys.py +783 -0
- julee/docs/sphinx_hcd/personas.py +435 -0
- julee/docs/sphinx_hcd/stories.py +932 -0
- julee/docs/sphinx_hcd/utils.py +180 -0
- julee/domain/models/__init__.py +5 -6
- julee/domain/models/assembly/assembly.py +7 -7
- julee/domain/models/assembly/tests/factories.py +2 -1
- julee/domain/models/assembly/tests/test_assembly.py +16 -13
- julee/domain/models/assembly_specification/assembly_specification.py +11 -10
- julee/domain/models/assembly_specification/knowledge_service_query.py +14 -9
- julee/domain/models/assembly_specification/tests/factories.py +2 -1
- julee/domain/models/assembly_specification/tests/test_assembly_specification.py +9 -6
- julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +3 -1
- julee/domain/models/custom_fields/content_stream.py +3 -2
- julee/domain/models/custom_fields/tests/test_custom_fields.py +2 -1
- julee/domain/models/document/document.py +12 -10
- julee/domain/models/document/tests/factories.py +3 -2
- julee/domain/models/document/tests/test_document.py +6 -3
- julee/domain/models/knowledge_service_config/knowledge_service_config.py +4 -4
- julee/domain/models/policy/__init__.py +4 -4
- julee/domain/models/policy/document_policy_validation.py +17 -17
- julee/domain/models/policy/policy.py +12 -10
- julee/domain/models/policy/tests/factories.py +2 -1
- julee/domain/models/policy/tests/test_document_policy_validation.py +3 -1
- julee/domain/models/policy/tests/test_policy.py +2 -1
- julee/domain/repositories/__init__.py +3 -3
- julee/domain/repositories/assembly.py +3 -1
- julee/domain/repositories/assembly_specification.py +2 -0
- julee/domain/repositories/base.py +33 -16
- julee/domain/repositories/document.py +3 -1
- julee/domain/repositories/document_policy_validation.py +3 -1
- julee/domain/repositories/knowledge_service_config.py +2 -0
- julee/domain/repositories/knowledge_service_query.py +1 -0
- julee/domain/repositories/policy.py +3 -1
- julee/domain/use_cases/decorators.py +3 -2
- julee/domain/use_cases/extract_assemble_data.py +23 -13
- julee/domain/use_cases/initialize_system_data.py +13 -13
- julee/domain/use_cases/tests/test_extract_assemble_data.py +10 -10
- julee/domain/use_cases/tests/test_initialize_system_data.py +2 -2
- julee/domain/use_cases/tests/test_validate_document.py +11 -11
- julee/domain/use_cases/validate_document.py +26 -15
- julee/maintenance/__init__.py +1 -0
- julee/maintenance/release.py +188 -0
- julee/repositories/__init__.py +4 -1
- julee/repositories/memory/assembly.py +6 -5
- julee/repositories/memory/assembly_specification.py +9 -9
- julee/repositories/memory/base.py +12 -11
- julee/repositories/memory/document.py +8 -7
- julee/repositories/memory/document_policy_validation.py +7 -6
- julee/repositories/memory/knowledge_service_config.py +9 -7
- julee/repositories/memory/knowledge_service_query.py +9 -7
- julee/repositories/memory/policy.py +6 -5
- julee/repositories/memory/tests/test_document.py +6 -4
- julee/repositories/memory/tests/test_document_policy_validation.py +2 -1
- julee/repositories/memory/tests/test_policy.py +2 -1
- julee/repositories/minio/assembly.py +4 -4
- julee/repositories/minio/assembly_specification.py +6 -8
- julee/repositories/minio/client.py +22 -25
- julee/repositories/minio/document.py +11 -11
- julee/repositories/minio/document_policy_validation.py +5 -5
- julee/repositories/minio/knowledge_service_config.py +8 -8
- julee/repositories/minio/knowledge_service_query.py +6 -9
- julee/repositories/minio/policy.py +4 -4
- julee/repositories/minio/tests/fake_client.py +11 -9
- julee/repositories/minio/tests/test_assembly.py +3 -1
- julee/repositories/minio/tests/test_assembly_specification.py +2 -1
- julee/repositories/minio/tests/test_client_protocol.py +5 -5
- julee/repositories/minio/tests/test_document.py +7 -6
- julee/repositories/minio/tests/test_document_policy_validation.py +3 -1
- julee/repositories/minio/tests/test_knowledge_service_config.py +4 -2
- julee/repositories/minio/tests/test_knowledge_service_query.py +3 -2
- julee/repositories/minio/tests/test_policy.py +3 -1
- julee/repositories/temporal/activities.py +5 -5
- julee/repositories/temporal/proxies.py +5 -5
- julee/services/knowledge_service/__init__.py +1 -2
- julee/services/knowledge_service/anthropic/knowledge_service.py +8 -7
- julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +11 -10
- julee/services/knowledge_service/factory.py +8 -8
- julee/services/knowledge_service/knowledge_service.py +22 -18
- julee/services/knowledge_service/memory/knowledge_service.py +13 -12
- julee/services/knowledge_service/memory/test_knowledge_service.py +10 -7
- julee/services/knowledge_service/test_factory.py +11 -10
- julee/services/temporal/activities.py +10 -10
- julee/services/temporal/proxies.py +2 -2
- julee/util/domain.py +6 -6
- julee/util/repos/minio/file_storage.py +8 -9
- julee/util/repos/temporal/client_proxies/file_storage.py +3 -4
- julee/util/repos/temporal/data_converter.py +6 -6
- julee/util/repos/temporal/minio_file_storage.py +1 -1
- julee/util/repos/temporal/proxies/file_storage.py +2 -3
- julee/util/repositories.py +6 -7
- julee/util/temporal/activities.py +1 -1
- julee/util/temporal/decorators.py +28 -33
- julee/util/tests/test_decorators.py +13 -15
- julee/util/validation/repository.py +3 -3
- julee/util/validation/type_guards.py +12 -11
- julee/worker.py +9 -8
- julee/workflows/__init__.py +2 -2
- julee/workflows/extract_assemble.py +2 -1
- julee/workflows/validate_document.py +3 -2
- {julee-0.1.1.dist-info → julee-0.1.3.dist-info}/METADATA +4 -1
- julee-0.1.3.dist-info/RECORD +197 -0
- julee-0.1.1.dist-info/RECORD +0 -161
- {julee-0.1.1.dist-info → julee-0.1.3.dist-info}/WHEEL +0 -0
- {julee-0.1.1.dist-info → julee-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {julee-0.1.1.dist-info → julee-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -11,13 +11,13 @@ workflow.execute_activity() with the appropriate activity names, timeouts,
|
|
|
11
11
|
and retry policies.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from julee.util.temporal.decorators import temporal_workflow_proxy
|
|
15
14
|
from julee.services.knowledge_service import KnowledgeService
|
|
16
15
|
|
|
17
16
|
# Import activity name bases from shared module
|
|
18
17
|
from julee.services.temporal.activity_names import (
|
|
19
18
|
KNOWLEDGE_SERVICE_ACTIVITY_BASE,
|
|
20
19
|
)
|
|
20
|
+
from julee.util.temporal.decorators import temporal_workflow_proxy
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@temporal_workflow_proxy(
|
|
@@ -35,7 +35,7 @@ class WorkflowKnowledgeServiceProxy(KnowledgeService):
|
|
|
35
35
|
pass
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
# Export the workflow proxy
|
|
38
|
+
# Export the workflow proxy classes
|
|
39
39
|
__all__ = [
|
|
40
40
|
"WorkflowKnowledgeServiceProxy",
|
|
41
41
|
]
|
julee/util/domain.py
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
1
3
|
from pydantic import (
|
|
2
4
|
BaseModel,
|
|
3
5
|
Field,
|
|
4
6
|
field_validator,
|
|
5
7
|
)
|
|
6
|
-
from typing import Optional, Dict
|
|
7
|
-
from datetime import datetime, timezone
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class FileMetadata(BaseModel):
|
|
11
11
|
"""Metadata about a stored file."""
|
|
12
12
|
|
|
13
13
|
file_id: str
|
|
14
|
-
filename:
|
|
15
|
-
content_type:
|
|
16
|
-
size_bytes:
|
|
14
|
+
filename: str | None = None
|
|
15
|
+
content_type: str | None = None
|
|
16
|
+
size_bytes: int | None = None
|
|
17
17
|
uploaded_at: str = Field(
|
|
18
18
|
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
19
19
|
)
|
|
20
|
-
metadata:
|
|
20
|
+
metadata: dict[str, str] = Field(default_factory=dict)
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class FileUploadArgs(BaseModel):
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from minio import Minio # type: ignore[import-untyped]
|
|
7
6
|
from minio.error import S3Error # type: ignore[import-untyped]
|
|
@@ -20,11 +19,11 @@ class MinioFileStorageRepository(FileStorageRepository):
|
|
|
20
19
|
|
|
21
20
|
def __init__(
|
|
22
21
|
self,
|
|
23
|
-
endpoint:
|
|
24
|
-
access_key:
|
|
25
|
-
secret_key:
|
|
22
|
+
endpoint: str | None = None,
|
|
23
|
+
access_key: str | None = None,
|
|
24
|
+
secret_key: str | None = None,
|
|
26
25
|
secure: bool = False,
|
|
27
|
-
bucket_name:
|
|
26
|
+
bucket_name: str | None = None,
|
|
28
27
|
):
|
|
29
28
|
self._endpoint = (
|
|
30
29
|
endpoint
|
|
@@ -48,7 +47,7 @@ class MinioFileStorageRepository(FileStorageRepository):
|
|
|
48
47
|
else os.environ.get("MINIO_BUCKET_NAME", "file-storage")
|
|
49
48
|
)
|
|
50
49
|
|
|
51
|
-
self._client:
|
|
50
|
+
self._client: Minio | None = None
|
|
52
51
|
logger.debug(
|
|
53
52
|
"MinioFileStorageRepository initialized",
|
|
54
53
|
extra={
|
|
@@ -134,7 +133,7 @@ class MinioFileStorageRepository(FileStorageRepository):
|
|
|
134
133
|
)
|
|
135
134
|
raise
|
|
136
135
|
|
|
137
|
-
async def download_file(self, file_id: str) ->
|
|
136
|
+
async def download_file(self, file_id: str) -> bytes | None:
|
|
138
137
|
"""Download a file from Minio storage by its ID."""
|
|
139
138
|
client = await self._get_client()
|
|
140
139
|
logger.info(
|
|
@@ -161,7 +160,7 @@ class MinioFileStorageRepository(FileStorageRepository):
|
|
|
161
160
|
)
|
|
162
161
|
raise
|
|
163
162
|
|
|
164
|
-
async def get_file_metadata(self, file_id: str) ->
|
|
163
|
+
async def get_file_metadata(self, file_id: str) -> FileMetadata | None:
|
|
165
164
|
"""Retrieve metadata for a stored file from Minio."""
|
|
166
165
|
client = await self._get_client()
|
|
167
166
|
logger.info(
|
|
@@ -178,7 +177,7 @@ class MinioFileStorageRepository(FileStorageRepository):
|
|
|
178
177
|
"content_type": stat.content_type,
|
|
179
178
|
},
|
|
180
179
|
)
|
|
181
|
-
uploaded_at_str:
|
|
180
|
+
uploaded_at_str: str | None = (
|
|
182
181
|
stat.last_modified.isoformat() if stat.last_modified else None
|
|
183
182
|
)
|
|
184
183
|
# Extract filename and metadata more explicitly
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from temporalio.client import Client
|
|
5
4
|
|
|
@@ -19,7 +18,7 @@ class TemporalFileStorageRepository(FileStorageRepository):
|
|
|
19
18
|
def __init__(
|
|
20
19
|
self,
|
|
21
20
|
client: Client,
|
|
22
|
-
concrete_repo:
|
|
21
|
+
concrete_repo: FileStorageRepository | None = None,
|
|
23
22
|
):
|
|
24
23
|
self.client = client
|
|
25
24
|
self.concrete_repo = concrete_repo
|
|
@@ -39,7 +38,7 @@ class TemporalFileStorageRepository(FileStorageRepository):
|
|
|
39
38
|
result = await handle.result()
|
|
40
39
|
return result # type: ignore[no-any-return]
|
|
41
40
|
|
|
42
|
-
async def download_file(self, file_id: str) ->
|
|
41
|
+
async def download_file(self, file_id: str) -> bytes | None:
|
|
43
42
|
"""Download a file via Temporal activity."""
|
|
44
43
|
logger.debug(f"Client calling activity to download file: {file_id}")
|
|
45
44
|
|
|
@@ -53,7 +52,7 @@ class TemporalFileStorageRepository(FileStorageRepository):
|
|
|
53
52
|
result = await handle.result()
|
|
54
53
|
return result # type: ignore[no-any-return]
|
|
55
54
|
|
|
56
|
-
async def get_file_metadata(self, file_id: str) ->
|
|
55
|
+
async def get_file_metadata(self, file_id: str) -> FileMetadata | None:
|
|
57
56
|
"""Retrieve file metadata via Temporal activity."""
|
|
58
57
|
logger.debug(f"Client calling activity to get file metadata: {file_id}")
|
|
59
58
|
|
|
@@ -7,20 +7,20 @@ This allows domain models to implement context-aware validation that can
|
|
|
7
7
|
be more permissive during Temporal serialization/deserialization.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
|
+
import temporalio.api.common.v1
|
|
12
13
|
from pydantic import TypeAdapter
|
|
13
14
|
from temporalio.contrib.pydantic import (
|
|
14
15
|
PydanticJSONPlainPayloadConverter,
|
|
15
16
|
ToJsonOptions,
|
|
16
17
|
)
|
|
17
18
|
from temporalio.converter import (
|
|
18
|
-
DataConverter,
|
|
19
19
|
CompositePayloadConverter,
|
|
20
|
+
DataConverter,
|
|
20
21
|
DefaultPayloadConverter,
|
|
21
22
|
JSONPlainPayloadConverter,
|
|
22
23
|
)
|
|
23
|
-
import temporalio.api.common.v1
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class TemporalValidationPydanticConverter(PydanticJSONPlainPayloadConverter):
|
|
@@ -36,7 +36,7 @@ class TemporalValidationPydanticConverter(PydanticJSONPlainPayloadConverter):
|
|
|
36
36
|
def from_payload(
|
|
37
37
|
self,
|
|
38
38
|
payload: temporalio.api.common.v1.Payload,
|
|
39
|
-
type_hint:
|
|
39
|
+
type_hint: type | None = None,
|
|
40
40
|
) -> Any:
|
|
41
41
|
"""Deserialize payload with temporal_validation context.
|
|
42
42
|
|
|
@@ -69,7 +69,7 @@ class TemporalValidationPayloadConverter(CompositePayloadConverter):
|
|
|
69
69
|
ensuring all Pydantic models get temporal_validation context.
|
|
70
70
|
"""
|
|
71
71
|
|
|
72
|
-
def __init__(self, to_json_options:
|
|
72
|
+
def __init__(self, to_json_options: ToJsonOptions | None = None) -> None:
|
|
73
73
|
"""Initialize with custom JSON converter adding temporal context."""
|
|
74
74
|
# Create our custom JSON converter with temporal validation
|
|
75
75
|
json_payload_converter = TemporalValidationPydanticConverter(to_json_options)
|
|
@@ -89,7 +89,7 @@ class TemporalValidationPayloadConverter(CompositePayloadConverter):
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def create_temporal_data_converter(
|
|
92
|
-
to_json_options:
|
|
92
|
+
to_json_options: ToJsonOptions | None = None,
|
|
93
93
|
) -> DataConverter:
|
|
94
94
|
"""Create a data converter with temporal validation support.
|
|
95
95
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from julee.util.temporal.decorators import temporal_activity_registration
|
|
2
1
|
from julee.util.repos.minio.file_storage import MinioFileStorageRepository
|
|
2
|
+
from julee.util.temporal.decorators import temporal_activity_registration
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
@temporal_activity_registration("util.file_storage.minio")
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from temporalio import workflow
|
|
5
4
|
|
|
@@ -35,7 +34,7 @@ class WorkflowFileStorageRepositoryProxy(FileStorageRepository):
|
|
|
35
34
|
)
|
|
36
35
|
return FileMetadata.model_validate(result)
|
|
37
36
|
|
|
38
|
-
async def download_file(self, file_id: str) ->
|
|
37
|
+
async def download_file(self, file_id: str) -> bytes | None:
|
|
39
38
|
"""Download a file from storage via Temporal activity."""
|
|
40
39
|
logger.debug(f"Workflow calling activity to download file: {file_id}")
|
|
41
40
|
result = await workflow.execute_activity(
|
|
@@ -45,7 +44,7 @@ class WorkflowFileStorageRepositoryProxy(FileStorageRepository):
|
|
|
45
44
|
)
|
|
46
45
|
return result # type: ignore[no-any-return]
|
|
47
46
|
|
|
48
|
-
async def get_file_metadata(self, file_id: str) ->
|
|
47
|
+
async def get_file_metadata(self, file_id: str) -> FileMetadata | None:
|
|
49
48
|
"""Retrieve file metadata via Temporal activity."""
|
|
50
49
|
logger.debug(f"Workflow calling activity to get file metadata: {file_id}")
|
|
51
50
|
result = await workflow.execute_activity(
|
julee/util/repositories.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from typing import Protocol,
|
|
1
|
+
from typing import Protocol, runtime_checkable
|
|
2
|
+
|
|
2
3
|
from julee.util.domain import FileMetadata, FileUploadArgs
|
|
3
4
|
|
|
4
5
|
|
|
@@ -23,16 +24,14 @@ class FileStorageRepository(Protocol):
|
|
|
23
24
|
FileMetadata object with details about the uploaded file.
|
|
24
25
|
|
|
25
26
|
Implementation Notes:
|
|
26
|
-
- Must be idempotent: uploading the same file_id multiple times is
|
|
27
|
-
safe.
|
|
27
|
+
- Must be idempotent: uploading the same file_id multiple times is safe.
|
|
28
28
|
- Should return metadata including the actual size and content type.
|
|
29
|
-
- Must perform security validation: file size limits, content type
|
|
30
|
-
verification, and filename sanitization.
|
|
29
|
+
- Must perform security validation: file size limits, content type verification, and filename sanitization.
|
|
31
30
|
- Should reject files that don't match declared content type.
|
|
32
31
|
"""
|
|
33
32
|
...
|
|
34
33
|
|
|
35
|
-
async def download_file(self, file_id: str) ->
|
|
34
|
+
async def download_file(self, file_id: str) -> bytes | None:
|
|
36
35
|
"""Download a file from storage by its ID.
|
|
37
36
|
|
|
38
37
|
Args:
|
|
@@ -43,7 +42,7 @@ class FileStorageRepository(Protocol):
|
|
|
43
42
|
"""
|
|
44
43
|
...
|
|
45
44
|
|
|
46
|
-
async def get_file_metadata(self, file_id: str) ->
|
|
45
|
+
async def get_file_metadata(self, file_id: str) -> FileMetadata | None:
|
|
47
46
|
"""Retrieve metadata for a stored file.
|
|
48
47
|
|
|
49
48
|
Args:
|
|
@@ -79,7 +79,7 @@ def collect_activities_from_instances(*instances: Any) -> list[Any]:
|
|
|
79
79
|
|
|
80
80
|
Args:
|
|
81
81
|
*instances: Repository and service instances decorated with
|
|
82
|
-
|
|
82
|
+
@temporal_activity_registration
|
|
83
83
|
|
|
84
84
|
Returns:
|
|
85
85
|
List of activity methods ready for Worker registration
|
|
@@ -10,12 +10,10 @@ Both reduce boilerplate and ensure consistent patterns.
|
|
|
10
10
|
import functools
|
|
11
11
|
import inspect
|
|
12
12
|
import logging
|
|
13
|
+
from collections.abc import Callable
|
|
13
14
|
from datetime import timedelta
|
|
14
15
|
from typing import (
|
|
15
16
|
Any,
|
|
16
|
-
Callable,
|
|
17
|
-
Optional,
|
|
18
|
-
Type,
|
|
19
17
|
TypeVar,
|
|
20
18
|
get_args,
|
|
21
19
|
get_origin,
|
|
@@ -34,7 +32,7 @@ logger = logging.getLogger(__name__)
|
|
|
34
32
|
T = TypeVar("T")
|
|
35
33
|
|
|
36
34
|
|
|
37
|
-
def _extract_concrete_type_from_base(cls: type) ->
|
|
35
|
+
def _extract_concrete_type_from_base(cls: type) -> type | None:
|
|
38
36
|
"""
|
|
39
37
|
Extract the concrete type argument from a generic base class.
|
|
40
38
|
|
|
@@ -113,7 +111,7 @@ def _substitute_typevar_with_concrete(annotation: Any, concrete_type: type) -> A
|
|
|
113
111
|
|
|
114
112
|
def temporal_activity_registration(
|
|
115
113
|
activity_prefix: str,
|
|
116
|
-
) -> Callable[[
|
|
114
|
+
) -> Callable[[type[T]], type[T]]:
|
|
117
115
|
"""
|
|
118
116
|
Class decorator that wraps async protocol methods as Temporal activities.
|
|
119
117
|
|
|
@@ -123,13 +121,10 @@ def temporal_activity_registration(
|
|
|
123
121
|
name.
|
|
124
122
|
|
|
125
123
|
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"
|
|
124
|
+
activity_prefix: Prefix for activity names (e.g., "sample.payment_repo.minio"). Method names will be appended to create full activity names like "sample.payment_repo.minio.process_payment"
|
|
129
125
|
|
|
130
126
|
Returns:
|
|
131
|
-
The decorated class with all async methods wrapped as Temporal
|
|
132
|
-
activities
|
|
127
|
+
The decorated class with all async methods wrapped as Temporal activities
|
|
133
128
|
|
|
134
129
|
Example:
|
|
135
130
|
@temporal_activity_registration("sample.payment_repo.minio")
|
|
@@ -137,15 +132,12 @@ def temporal_activity_registration(
|
|
|
137
132
|
pass
|
|
138
133
|
|
|
139
134
|
# This automatically creates activities for all protocol methods:
|
|
140
|
-
# - process_payment ->
|
|
141
|
-
#
|
|
142
|
-
# -
|
|
143
|
-
# "sample.payment_repo.minio.get_payment"
|
|
144
|
-
# - refund_payment ->
|
|
145
|
-
# "sample.payment_repo.minio.refund_payment"
|
|
135
|
+
# - process_payment -> "sample.payment_repo.minio.process_payment"
|
|
136
|
+
# - get_payment -> "sample.payment_repo.minio.get_payment"
|
|
137
|
+
# - refund_payment -> "sample.payment_repo.minio.refund_payment"
|
|
146
138
|
"""
|
|
147
139
|
|
|
148
|
-
def decorator(cls:
|
|
140
|
+
def decorator(cls: type[T]) -> type[T]:
|
|
149
141
|
logger.debug(
|
|
150
142
|
f"Applying temporal_activity_registration decorator to {cls.__name__}"
|
|
151
143
|
)
|
|
@@ -186,8 +178,9 @@ def temporal_activity_registration(
|
|
|
186
178
|
return wrapper_method
|
|
187
179
|
|
|
188
180
|
# Create the wrapper and apply activity decorator
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
wrapped_method = activity.defn(name=activity_name)(
|
|
182
|
+
create_wrapper_method(method, name)
|
|
183
|
+
)
|
|
191
184
|
|
|
192
185
|
# Replace the method on the class with the wrapped version
|
|
193
186
|
setattr(cls, name, wrapped_method)
|
|
@@ -210,8 +203,8 @@ def temporal_activity_registration(
|
|
|
210
203
|
def temporal_workflow_proxy(
|
|
211
204
|
activity_base: str,
|
|
212
205
|
default_timeout_seconds: int = 30,
|
|
213
|
-
retry_methods:
|
|
214
|
-
) -> Callable[[
|
|
206
|
+
retry_methods: list[str] | None = None,
|
|
207
|
+
) -> Callable[[type[T]], type[T]]:
|
|
215
208
|
"""
|
|
216
209
|
Class decorator that automatically creates workflow proxy methods that
|
|
217
210
|
delegate to Temporal activities.
|
|
@@ -226,8 +219,8 @@ def temporal_workflow_proxy(
|
|
|
226
219
|
retry_methods: List of method names that should use retry policies
|
|
227
220
|
|
|
228
221
|
Returns:
|
|
229
|
-
The decorated class with all protocol methods implemented as
|
|
230
|
-
|
|
222
|
+
The decorated class with all protocol methods implemented as workflow
|
|
223
|
+
activity calls
|
|
231
224
|
|
|
232
225
|
Example:
|
|
233
226
|
@temporal_workflow_proxy(
|
|
@@ -241,11 +234,10 @@ def temporal_workflow_proxy(
|
|
|
241
234
|
# This automatically creates workflow methods for all methods:
|
|
242
235
|
# - get() -> calls "julee.document_repo.minio.get" activity
|
|
243
236
|
# - save() -> calls "julee.document_repo.minio.save" with retry
|
|
244
|
-
# - generate_id() -> calls "julee.document_repo.minio.generate_id"
|
|
245
|
-
# with retry
|
|
237
|
+
# - generate_id() -> calls "julee.document_repo.minio.generate_id" with retry
|
|
246
238
|
"""
|
|
247
239
|
|
|
248
|
-
def decorator(cls:
|
|
240
|
+
def decorator(cls: type[T]) -> type[T]:
|
|
249
241
|
logger.debug(f"Applying temporal_workflow_proxy decorator to {cls.__name__}")
|
|
250
242
|
|
|
251
243
|
retry_methods_set = set(retry_methods or [])
|
|
@@ -397,14 +389,17 @@ def temporal_workflow_proxy(
|
|
|
397
389
|
return workflow_method
|
|
398
390
|
|
|
399
391
|
# Create and set the method on the class
|
|
400
|
-
|
|
392
|
+
setattr(
|
|
393
|
+
cls,
|
|
401
394
|
method_name,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
395
|
+
create_workflow_method(
|
|
396
|
+
method_name,
|
|
397
|
+
needs_validation,
|
|
398
|
+
is_optional,
|
|
399
|
+
inner_type,
|
|
400
|
+
original_method,
|
|
401
|
+
),
|
|
406
402
|
)
|
|
407
|
-
setattr(cls, method_name, workflow_method)
|
|
408
403
|
wrapped_methods.append(method_name)
|
|
409
404
|
|
|
410
405
|
# Always generate __init__ that calls super() for consistent init
|
|
@@ -416,7 +411,7 @@ def temporal_workflow_proxy(
|
|
|
416
411
|
proxy_self.activity_fail_fast_retry_policy = fail_fast_retry_policy
|
|
417
412
|
logger.debug(f"Initialized {cls.__name__}")
|
|
418
413
|
|
|
419
|
-
|
|
414
|
+
cls.__init__ = __init__
|
|
420
415
|
|
|
421
416
|
logger.info(
|
|
422
417
|
f"Temporal workflow proxy decorator applied to {cls.__name__}",
|
|
@@ -10,8 +10,6 @@ import asyncio
|
|
|
10
10
|
import inspect
|
|
11
11
|
from typing import (
|
|
12
12
|
Any,
|
|
13
|
-
List,
|
|
14
|
-
Optional,
|
|
15
13
|
Protocol,
|
|
16
14
|
TypeVar,
|
|
17
15
|
get_args,
|
|
@@ -62,7 +60,7 @@ class MockRepositoryProtocol(MockBaseRepositoryProtocol, Protocol):
|
|
|
62
60
|
"""Mock payment processing method."""
|
|
63
61
|
...
|
|
64
62
|
|
|
65
|
-
async def get_payment(self, payment_id: str) ->
|
|
63
|
+
async def get_payment(self, payment_id: str) -> dict | None:
|
|
66
64
|
"""Mock get payment method."""
|
|
67
65
|
...
|
|
68
66
|
|
|
@@ -98,7 +96,7 @@ class MockRepository(MockRepositoryProtocol):
|
|
|
98
96
|
"""Mock payment processing method."""
|
|
99
97
|
return {"status": "success", "order_id": order_id, "amount": amount}
|
|
100
98
|
|
|
101
|
-
async def get_payment(self, payment_id: str) ->
|
|
99
|
+
async def get_payment(self, payment_id: str) -> dict | None:
|
|
102
100
|
"""Mock get payment method."""
|
|
103
101
|
if payment_id == "not_found":
|
|
104
102
|
return None
|
|
@@ -239,7 +237,7 @@ def test_activity_names_with_different_prefixes() -> None:
|
|
|
239
237
|
captured_activity_names = []
|
|
240
238
|
original_activity_defn = activity.defn
|
|
241
239
|
|
|
242
|
-
def mock_activity_defn(name:
|
|
240
|
+
def mock_activity_defn(name: str | None = None, **kwargs: Any) -> Any:
|
|
243
241
|
"""Mock activity.defn to capture the activity names being created."""
|
|
244
242
|
if name:
|
|
245
243
|
captured_activity_names.append(name)
|
|
@@ -317,7 +315,7 @@ def test_decorator_handles_inheritance_correctly() -> None:
|
|
|
317
315
|
"amount": amount,
|
|
318
316
|
}
|
|
319
317
|
|
|
320
|
-
async def get_payment(self, payment_id: str) ->
|
|
318
|
+
async def get_payment(self, payment_id: str) -> dict | None:
|
|
321
319
|
if payment_id == "not_found":
|
|
322
320
|
return None
|
|
323
321
|
return {"payment_id": payment_id, "status": "completed"}
|
|
@@ -467,7 +465,7 @@ class MockDocumentRepository(BaseRepository[MockDocument], Protocol):
|
|
|
467
465
|
class NonGenericRepository(Protocol):
|
|
468
466
|
"""Repository that doesn't follow BaseRepository[T] pattern."""
|
|
469
467
|
|
|
470
|
-
async def get(self, id: str) ->
|
|
468
|
+
async def get(self, id: str) -> MockDocument | None: ...
|
|
471
469
|
|
|
472
470
|
|
|
473
471
|
class TestTypeExtraction:
|
|
@@ -521,7 +519,7 @@ class TestTypeSubstitution:
|
|
|
521
519
|
|
|
522
520
|
def test_substitutes_optional_typevar(self) -> None:
|
|
523
521
|
"""Test Optional[TypeVar] substitution."""
|
|
524
|
-
optional_t =
|
|
522
|
+
optional_t = T | None
|
|
525
523
|
result = _substitute_typevar_with_concrete(
|
|
526
524
|
optional_t, MockAssemblySpecification
|
|
527
525
|
)
|
|
@@ -535,7 +533,7 @@ class TestTypeSubstitution:
|
|
|
535
533
|
|
|
536
534
|
def test_substitutes_nested_generics(self) -> None:
|
|
537
535
|
"""Test substitution in nested generic types."""
|
|
538
|
-
nested_generic =
|
|
536
|
+
nested_generic = list[T | None]
|
|
539
537
|
result = _substitute_typevar_with_concrete(nested_generic, MockDocument)
|
|
540
538
|
|
|
541
539
|
# Should be List[Optional[MockDocument]]
|
|
@@ -621,20 +619,20 @@ class TestPydanticValidationDetection:
|
|
|
621
619
|
|
|
622
620
|
def test_detects_optional_pydantic_types(self) -> None:
|
|
623
621
|
"""Test detection of Optional[PydanticModel] types."""
|
|
624
|
-
assert _needs_pydantic_validation(
|
|
625
|
-
assert _needs_pydantic_validation(
|
|
622
|
+
assert _needs_pydantic_validation(MockAssemblySpecification | None)
|
|
623
|
+
assert _needs_pydantic_validation(MockDocument | None)
|
|
626
624
|
|
|
627
625
|
def test_rejects_non_pydantic_types(self) -> None:
|
|
628
626
|
"""Test that non-Pydantic types are not flagged for validation."""
|
|
629
627
|
assert not _needs_pydantic_validation(str)
|
|
630
628
|
assert not _needs_pydantic_validation(int)
|
|
631
629
|
assert not _needs_pydantic_validation(dict)
|
|
632
|
-
assert not _needs_pydantic_validation(
|
|
630
|
+
assert not _needs_pydantic_validation(str | None)
|
|
633
631
|
|
|
634
632
|
def test_rejects_typevar_types(self) -> None:
|
|
635
633
|
"""Test TypeVar types aren't flagged for validation (the bug)."""
|
|
636
634
|
assert not _needs_pydantic_validation(T)
|
|
637
|
-
assert not _needs_pydantic_validation(
|
|
635
|
+
assert not _needs_pydantic_validation(T | None)
|
|
638
636
|
|
|
639
637
|
def test_handles_none_and_empty(self) -> None:
|
|
640
638
|
"""Test handling of None and Signature.empty."""
|
|
@@ -733,7 +731,7 @@ class TestEndToEndTypeSubstitution:
|
|
|
733
731
|
def test_type_substitution_enables_pydantic_validation(self) -> None:
|
|
734
732
|
"""Test type substitution enables Pydantic validation."""
|
|
735
733
|
# Simulate the problematic method signature: Optional[~T]
|
|
736
|
-
original_annotation =
|
|
734
|
+
original_annotation = T | None
|
|
737
735
|
|
|
738
736
|
# Before fix: TypeVar prevents validation
|
|
739
737
|
assert not _needs_pydantic_validation(original_annotation)
|
|
@@ -760,7 +758,7 @@ class TestEndToEndTypeSubstitution:
|
|
|
760
758
|
assert isinstance(activity_result_dict, dict)
|
|
761
759
|
with pytest.raises(AttributeError):
|
|
762
760
|
# This would fail because dict doesn't have the attribute
|
|
763
|
-
|
|
761
|
+
_ = activity_result_dict.assembly_specification_id
|
|
764
762
|
|
|
765
763
|
# Demonstrate the solution: reconstruct Pydantic object
|
|
766
764
|
reconstructed = MockAssemblySpecification.model_validate(activity_result_dict)
|
|
@@ -6,7 +6,7 @@ their defined Protocols using @runtime_checkable.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import TypeVar
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ class RepositoryValidationError(Exception):
|
|
|
19
19
|
pass
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def validate_repository_protocol(repository: object, protocol:
|
|
22
|
+
def validate_repository_protocol(repository: object, protocol: type[P]) -> None:
|
|
23
23
|
"""
|
|
24
24
|
Validate that a repository implementation satisfies a protocol contract.
|
|
25
25
|
|
|
@@ -72,7 +72,7 @@ def validate_repository_protocol(repository: object, protocol: Type[P]) -> None:
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def ensure_repository_protocol(repository: object, protocol:
|
|
75
|
+
def ensure_repository_protocol(repository: object, protocol: type[P]) -> P:
|
|
76
76
|
"""
|
|
77
77
|
Validate and return a repository with proper type annotation.
|
|
78
78
|
|
|
@@ -11,17 +11,16 @@ providing clear error messages when types don't match expectations.
|
|
|
11
11
|
|
|
12
12
|
import inspect
|
|
13
13
|
import logging
|
|
14
|
+
from collections.abc import Callable
|
|
14
15
|
from functools import wraps
|
|
15
16
|
from typing import (
|
|
16
17
|
Any,
|
|
17
|
-
Dict,
|
|
18
|
-
List,
|
|
19
18
|
Union,
|
|
20
|
-
Callable,
|
|
21
|
-
get_type_hints,
|
|
22
|
-
get_origin,
|
|
23
19
|
get_args,
|
|
20
|
+
get_origin,
|
|
21
|
+
get_type_hints,
|
|
24
22
|
)
|
|
23
|
+
|
|
25
24
|
from pydantic import BaseModel
|
|
26
25
|
|
|
27
26
|
logger = logging.getLogger(__name__)
|
|
@@ -54,7 +53,7 @@ def validate_type(
|
|
|
54
53
|
if allow_none:
|
|
55
54
|
return
|
|
56
55
|
raise TypeValidationError(
|
|
57
|
-
f"{context_name}: Expected {_format_type(expected_type)},
|
|
56
|
+
f"{context_name}: Expected {_format_type(expected_type)}, got None"
|
|
58
57
|
)
|
|
59
58
|
|
|
60
59
|
# Get the origin type for generic types (List[X] -> list, Dict -> dict)
|
|
@@ -124,7 +123,7 @@ def _validate_generic_type(
|
|
|
124
123
|
|
|
125
124
|
|
|
126
125
|
def _validate_list_contents(
|
|
127
|
-
value:
|
|
126
|
+
value: list[Any], type_args: tuple, context_name: str
|
|
128
127
|
) -> None:
|
|
129
128
|
"""Validate contents of a list."""
|
|
130
129
|
if not type_args:
|
|
@@ -143,7 +142,7 @@ def _validate_list_contents(
|
|
|
143
142
|
|
|
144
143
|
|
|
145
144
|
def _validate_dict_contents(
|
|
146
|
-
value:
|
|
145
|
+
value: dict[Any, Any], type_args: tuple, context_name: str
|
|
147
146
|
) -> None:
|
|
148
147
|
"""Validate contents of a dictionary."""
|
|
149
148
|
if len(type_args) < 2:
|
|
@@ -241,7 +240,7 @@ def _raise_pydantic_dict_error(
|
|
|
241
240
|
"dict_keys": dict_keys,
|
|
242
241
|
"model_fields": model_fields,
|
|
243
242
|
"matching_fields": matching_fields,
|
|
244
|
-
"dict_sample":
|
|
243
|
+
"dict_sample": dict(list(value.items())[:3]),
|
|
245
244
|
},
|
|
246
245
|
)
|
|
247
246
|
|
|
@@ -263,7 +262,7 @@ def _format_value(value: Any, max_items: int = 5) -> str:
|
|
|
263
262
|
else:
|
|
264
263
|
items = list(value.items())[:max_items]
|
|
265
264
|
return f"{dict(items)}... ({len(value)} total items)"
|
|
266
|
-
elif isinstance(value,
|
|
265
|
+
elif isinstance(value, list | tuple):
|
|
267
266
|
if len(value) <= max_items:
|
|
268
267
|
return str(value)
|
|
269
268
|
else:
|
|
@@ -308,7 +307,9 @@ def validate_parameter_types(
|
|
|
308
307
|
param_names = list(sig.parameters.keys())
|
|
309
308
|
|
|
310
309
|
# Validate positional arguments
|
|
311
|
-
for i, (param_name, arg_value) in enumerate(
|
|
310
|
+
for i, (param_name, arg_value) in enumerate(
|
|
311
|
+
zip(param_names, args, strict=False)
|
|
312
|
+
):
|
|
312
313
|
if param_name in types_to_check and param_name != "self":
|
|
313
314
|
try:
|
|
314
315
|
validate_type(
|