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,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fake Minio client for state-based testing.
|
|
3
|
+
|
|
4
|
+
This module provides a FakeMinioClient that implements the same interface as
|
|
5
|
+
the real Minio client but stores objects in memory for testing. This enables
|
|
6
|
+
state-based testing where you can verify actual storage state rather than
|
|
7
|
+
just mocking method calls.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Dict, Any, Optional, Callable, BinaryIO, Union, List
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from unittest.mock import Mock
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from minio.error import S3Error
|
|
15
|
+
from minio.datatypes import Object
|
|
16
|
+
from minio.api import ObjectWriteResult
|
|
17
|
+
from urllib3.response import BaseHTTPResponse
|
|
18
|
+
from urllib3 import HTTPHeaderDict
|
|
19
|
+
|
|
20
|
+
from ..client import MinioClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def requires_bucket(func: Callable) -> Callable:
|
|
24
|
+
"""Decorator to check if bucket exists before method execution."""
|
|
25
|
+
|
|
26
|
+
@wraps(func)
|
|
27
|
+
def wrapper(self: Any, bucket_name: str, *args: Any, **kwargs: Any) -> Any:
|
|
28
|
+
if bucket_name not in self._buckets:
|
|
29
|
+
raise S3Error(
|
|
30
|
+
code="NoSuchBucket",
|
|
31
|
+
message="Bucket does not exist",
|
|
32
|
+
resource=bucket_name,
|
|
33
|
+
request_id="req123",
|
|
34
|
+
host_id="host123",
|
|
35
|
+
response=Mock(),
|
|
36
|
+
)
|
|
37
|
+
return func(self, bucket_name, *args, **kwargs)
|
|
38
|
+
|
|
39
|
+
return wrapper
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def requires_object(func: Callable) -> Callable:
|
|
43
|
+
"""Decorator to check if object exists before method execution."""
|
|
44
|
+
|
|
45
|
+
@wraps(func)
|
|
46
|
+
def wrapper(
|
|
47
|
+
self: Any,
|
|
48
|
+
bucket_name: str,
|
|
49
|
+
object_name: str,
|
|
50
|
+
*args: Any,
|
|
51
|
+
**kwargs: Any,
|
|
52
|
+
) -> Any:
|
|
53
|
+
if bucket_name not in self._objects:
|
|
54
|
+
raise S3Error(
|
|
55
|
+
code="NoSuchBucket",
|
|
56
|
+
message="Bucket does not exist",
|
|
57
|
+
resource=bucket_name,
|
|
58
|
+
request_id="req123",
|
|
59
|
+
host_id="host123",
|
|
60
|
+
response=Mock(),
|
|
61
|
+
)
|
|
62
|
+
if object_name not in self._objects[bucket_name]:
|
|
63
|
+
raise S3Error(
|
|
64
|
+
code="NoSuchKey",
|
|
65
|
+
message="Object not found",
|
|
66
|
+
resource=object_name,
|
|
67
|
+
request_id="req123",
|
|
68
|
+
host_id="host123",
|
|
69
|
+
response=Mock(),
|
|
70
|
+
)
|
|
71
|
+
return func(self, bucket_name, object_name, *args, **kwargs)
|
|
72
|
+
|
|
73
|
+
return wrapper
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FakeMinioClient(MinioClient):
|
|
77
|
+
"""
|
|
78
|
+
Fake Minio client that stores objects in memory for testing.
|
|
79
|
+
|
|
80
|
+
This client implements the MinioClient protocol and stores all data in
|
|
81
|
+
memory, allowing for fast state-based testing without requiring a real
|
|
82
|
+
MinIO server.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self) -> None:
|
|
86
|
+
self._buckets: Dict[str, Dict[str, Any]] = {}
|
|
87
|
+
self._objects: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
|
88
|
+
|
|
89
|
+
def bucket_exists(self, bucket_name: str) -> bool:
|
|
90
|
+
"""Check if a bucket exists."""
|
|
91
|
+
return bucket_name in self._buckets
|
|
92
|
+
|
|
93
|
+
def make_bucket(self, bucket_name: str) -> None:
|
|
94
|
+
"""Create a bucket."""
|
|
95
|
+
if bucket_name in self._buckets:
|
|
96
|
+
raise S3Error(
|
|
97
|
+
code="BucketAlreadyExists",
|
|
98
|
+
message="Bucket already exists",
|
|
99
|
+
resource=bucket_name,
|
|
100
|
+
request_id="req123",
|
|
101
|
+
host_id="host123",
|
|
102
|
+
response=Mock(),
|
|
103
|
+
)
|
|
104
|
+
self._buckets[bucket_name] = {}
|
|
105
|
+
self._objects[bucket_name] = {}
|
|
106
|
+
|
|
107
|
+
@requires_bucket
|
|
108
|
+
def put_object(
|
|
109
|
+
self,
|
|
110
|
+
bucket_name: str,
|
|
111
|
+
object_name: str,
|
|
112
|
+
data: BinaryIO,
|
|
113
|
+
length: int,
|
|
114
|
+
content_type: str = "application/octet-stream",
|
|
115
|
+
metadata: Optional[Dict[str, Union[str, List[str], tuple[str]]]] = None,
|
|
116
|
+
) -> ObjectWriteResult:
|
|
117
|
+
"""Store an object in the bucket."""
|
|
118
|
+
|
|
119
|
+
# Read the data from stream
|
|
120
|
+
if hasattr(data, "read"):
|
|
121
|
+
if hasattr(data, "seek"):
|
|
122
|
+
data.seek(0) # Ensure we're at the beginning
|
|
123
|
+
content = data.read()
|
|
124
|
+
if hasattr(data, "seek"):
|
|
125
|
+
data.seek(0) # Reset for potential re-use
|
|
126
|
+
else:
|
|
127
|
+
content = data if isinstance(data, bytes) else str(data).encode("utf-8")
|
|
128
|
+
|
|
129
|
+
self._objects[bucket_name][object_name] = {
|
|
130
|
+
"data": content,
|
|
131
|
+
"metadata": metadata or {},
|
|
132
|
+
"content_type": content_type,
|
|
133
|
+
"size": len(content),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Return a proper ObjectWriteResult
|
|
137
|
+
return ObjectWriteResult(
|
|
138
|
+
bucket_name=bucket_name,
|
|
139
|
+
object_name=object_name,
|
|
140
|
+
version_id=None,
|
|
141
|
+
etag="fake-etag",
|
|
142
|
+
http_headers=HTTPHeaderDict(),
|
|
143
|
+
last_modified=datetime.now(timezone.utc),
|
|
144
|
+
location=f"/{bucket_name}/{object_name}",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@requires_object
|
|
148
|
+
def get_object(self, bucket_name: str, object_name: str) -> BaseHTTPResponse:
|
|
149
|
+
"""Retrieve an object from the bucket."""
|
|
150
|
+
|
|
151
|
+
obj_info = self._objects[bucket_name][object_name]
|
|
152
|
+
# Create a mock BaseHTTPResponse with the data
|
|
153
|
+
mock_response = Mock(spec=BaseHTTPResponse)
|
|
154
|
+
mock_response.read = Mock(return_value=obj_info["data"])
|
|
155
|
+
mock_response.close = Mock()
|
|
156
|
+
mock_response.release_conn = Mock()
|
|
157
|
+
return mock_response
|
|
158
|
+
|
|
159
|
+
@requires_object
|
|
160
|
+
def stat_object(self, bucket_name: str, object_name: str) -> Object:
|
|
161
|
+
"""Get object metadata without retrieving the object data."""
|
|
162
|
+
|
|
163
|
+
obj_info = self._objects[bucket_name][object_name]
|
|
164
|
+
# Create a real Minio Object
|
|
165
|
+
return Object(
|
|
166
|
+
bucket_name=bucket_name,
|
|
167
|
+
object_name=object_name,
|
|
168
|
+
last_modified=datetime.now(timezone.utc),
|
|
169
|
+
etag="fake-etag",
|
|
170
|
+
size=obj_info["size"],
|
|
171
|
+
content_type=obj_info["content_type"],
|
|
172
|
+
metadata=obj_info["metadata"],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def list_objects(self, bucket_name: str, prefix: str = "") -> list:
|
|
176
|
+
"""List objects in a bucket with optional prefix filter."""
|
|
177
|
+
if bucket_name not in self._objects:
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
objects = []
|
|
181
|
+
for object_name, obj_info in self._objects[bucket_name].items():
|
|
182
|
+
if object_name.startswith(prefix):
|
|
183
|
+
# Create a simple object info structure
|
|
184
|
+
obj = Mock()
|
|
185
|
+
obj.object_name = object_name
|
|
186
|
+
obj.size = obj_info["size"]
|
|
187
|
+
objects.append(obj)
|
|
188
|
+
|
|
189
|
+
return objects
|
|
190
|
+
|
|
191
|
+
@requires_object
|
|
192
|
+
def remove_object(self, bucket_name: str, object_name: str) -> None:
|
|
193
|
+
"""Remove an object from the bucket."""
|
|
194
|
+
|
|
195
|
+
del self._objects[bucket_name][object_name]
|
|
196
|
+
|
|
197
|
+
# Inspection methods for testing
|
|
198
|
+
def get_stored_objects(self, bucket_name: str) -> Dict[str, Dict[str, Any]]:
|
|
199
|
+
"""Get all stored objects in a bucket (for testing purposes)."""
|
|
200
|
+
return self._objects.get(bucket_name, {}).copy()
|
|
201
|
+
|
|
202
|
+
def get_object_count(self, bucket_name: str) -> int:
|
|
203
|
+
"""Get the number of objects in a bucket (for testing purposes)."""
|
|
204
|
+
return len(self._objects.get(bucket_name, {}))
|
|
205
|
+
|
|
206
|
+
def get_total_object_count(self) -> int:
|
|
207
|
+
"""Get total number of objects across all buckets (for testing)."""
|
|
208
|
+
return sum(len(objects) for objects in self._objects.values())
|
|
209
|
+
|
|
210
|
+
def clear_all_data(self) -> None:
|
|
211
|
+
"""Clear all buckets and objects (for testing purposes)."""
|
|
212
|
+
self._buckets.clear()
|
|
213
|
+
self._objects.clear()
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for MinioAssemblyRepository implementation.
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive tests for the Minio-based assembly
|
|
5
|
+
repository implementation, using the fake client to avoid external
|
|
6
|
+
dependencies during testing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
|
|
12
|
+
from julee.domain.models.assembly import Assembly, AssemblyStatus
|
|
13
|
+
from julee.repositories.minio.assembly import MinioAssemblyRepository
|
|
14
|
+
from .fake_client import FakeMinioClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def fake_client() -> FakeMinioClient:
|
|
19
|
+
"""Create a fresh fake Minio client for each test."""
|
|
20
|
+
return FakeMinioClient()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def assembly_repo(fake_client: FakeMinioClient) -> MinioAssemblyRepository:
|
|
25
|
+
"""Create assembly repository with fake client."""
|
|
26
|
+
return MinioAssemblyRepository(fake_client)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def sample_assembly() -> Assembly:
|
|
31
|
+
"""Create a sample assembly for testing."""
|
|
32
|
+
return Assembly(
|
|
33
|
+
assembly_id="test-assembly-123",
|
|
34
|
+
assembly_specification_id="spec-456",
|
|
35
|
+
input_document_id="input-doc-789",
|
|
36
|
+
workflow_id="test-workflow-123",
|
|
37
|
+
status=AssemblyStatus.PENDING,
|
|
38
|
+
assembled_document_id=None,
|
|
39
|
+
created_at=datetime.now(timezone.utc),
|
|
40
|
+
updated_at=datetime.now(timezone.utc),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestMinioAssemblyRepositoryBasicOperations:
|
|
45
|
+
"""Test basic CRUD operations on assemblies."""
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_save_and_get_assembly(
|
|
49
|
+
self,
|
|
50
|
+
assembly_repo: MinioAssemblyRepository,
|
|
51
|
+
sample_assembly: Assembly,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Test saving and retrieving an assembly."""
|
|
54
|
+
# Save assembly
|
|
55
|
+
await assembly_repo.save(sample_assembly)
|
|
56
|
+
|
|
57
|
+
# Retrieve assembly
|
|
58
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
59
|
+
|
|
60
|
+
assert retrieved is not None
|
|
61
|
+
assert retrieved.assembly_id == sample_assembly.assembly_id
|
|
62
|
+
assert (
|
|
63
|
+
retrieved.assembly_specification_id
|
|
64
|
+
== sample_assembly.assembly_specification_id
|
|
65
|
+
)
|
|
66
|
+
assert retrieved.input_document_id == sample_assembly.input_document_id
|
|
67
|
+
assert retrieved.status == sample_assembly.status
|
|
68
|
+
assert retrieved.assembled_document_id is None
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_get_nonexistent_assembly(
|
|
72
|
+
self, assembly_repo: MinioAssemblyRepository
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Test retrieving a non-existent assembly returns None."""
|
|
75
|
+
result = await assembly_repo.get("nonexistent-assembly")
|
|
76
|
+
assert result is None
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_generate_id(self, assembly_repo: MinioAssemblyRepository) -> None:
|
|
80
|
+
"""Test generating unique assembly IDs."""
|
|
81
|
+
id1 = await assembly_repo.generate_id()
|
|
82
|
+
id2 = await assembly_repo.generate_id()
|
|
83
|
+
|
|
84
|
+
assert isinstance(id1, str)
|
|
85
|
+
assert isinstance(id2, str)
|
|
86
|
+
assert id1 != id2
|
|
87
|
+
assert len(id1) > 0
|
|
88
|
+
assert len(id2) > 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestMinioAssemblyRepositoryDirectCompletion:
|
|
92
|
+
"""Test assembly completion using direct field assignment."""
|
|
93
|
+
|
|
94
|
+
@pytest.mark.asyncio
|
|
95
|
+
async def test_complete_assembly_with_direct_assignment(
|
|
96
|
+
self,
|
|
97
|
+
assembly_repo: MinioAssemblyRepository,
|
|
98
|
+
sample_assembly: Assembly,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Test completing assembly using direct field assignment + save."""
|
|
101
|
+
# Save initial assembly
|
|
102
|
+
await assembly_repo.save(sample_assembly)
|
|
103
|
+
|
|
104
|
+
# Complete assembly using direct assignment (new approach)
|
|
105
|
+
sample_assembly.assembled_document_id = "output-doc-123"
|
|
106
|
+
sample_assembly.status = AssemblyStatus.COMPLETED
|
|
107
|
+
await assembly_repo.save(sample_assembly)
|
|
108
|
+
|
|
109
|
+
# Verify completion by retrieving again
|
|
110
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
111
|
+
assert retrieved is not None
|
|
112
|
+
assert retrieved.assembled_document_id == "output-doc-123"
|
|
113
|
+
assert retrieved.status == AssemblyStatus.COMPLETED
|
|
114
|
+
|
|
115
|
+
@pytest.mark.asyncio
|
|
116
|
+
async def test_assembly_completion_idempotency(
|
|
117
|
+
self,
|
|
118
|
+
assembly_repo: MinioAssemblyRepository,
|
|
119
|
+
sample_assembly: Assembly,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Test that direct assignment approach maintains idempotency."""
|
|
122
|
+
# Save initial assembly
|
|
123
|
+
await assembly_repo.save(sample_assembly)
|
|
124
|
+
|
|
125
|
+
# Complete assembly first time
|
|
126
|
+
sample_assembly.assembled_document_id = "output-doc-456"
|
|
127
|
+
sample_assembly.status = AssemblyStatus.COMPLETED
|
|
128
|
+
await assembly_repo.save(sample_assembly)
|
|
129
|
+
|
|
130
|
+
first_updated_at = sample_assembly.updated_at
|
|
131
|
+
|
|
132
|
+
# "Complete" same assembly again with same values (idempotent)
|
|
133
|
+
sample_assembly.assembled_document_id = "output-doc-456"
|
|
134
|
+
sample_assembly.status = AssemblyStatus.COMPLETED
|
|
135
|
+
await assembly_repo.save(sample_assembly)
|
|
136
|
+
|
|
137
|
+
# Verify state and that updated_at changed (save updates timestamp)
|
|
138
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
139
|
+
assert retrieved is not None
|
|
140
|
+
assert retrieved.assembled_document_id == "output-doc-456"
|
|
141
|
+
assert retrieved.status == AssemblyStatus.COMPLETED
|
|
142
|
+
assert retrieved.updated_at is not None
|
|
143
|
+
assert first_updated_at is not None
|
|
144
|
+
assert retrieved.updated_at > first_updated_at
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestMinioAssemblyRepositoryStatusUpdates:
|
|
148
|
+
"""Test assembly status management."""
|
|
149
|
+
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_update_assembly_status(
|
|
152
|
+
self,
|
|
153
|
+
assembly_repo: MinioAssemblyRepository,
|
|
154
|
+
sample_assembly: Assembly,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Test updating assembly status."""
|
|
157
|
+
# Save initial assembly
|
|
158
|
+
await assembly_repo.save(sample_assembly)
|
|
159
|
+
|
|
160
|
+
# Update status
|
|
161
|
+
sample_assembly.status = AssemblyStatus.IN_PROGRESS
|
|
162
|
+
await assembly_repo.save(sample_assembly)
|
|
163
|
+
|
|
164
|
+
# Verify update
|
|
165
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
166
|
+
assert retrieved is not None
|
|
167
|
+
assert retrieved.status == AssemblyStatus.IN_PROGRESS
|
|
168
|
+
|
|
169
|
+
# Update to completed
|
|
170
|
+
sample_assembly.status = AssemblyStatus.COMPLETED
|
|
171
|
+
await assembly_repo.save(sample_assembly)
|
|
172
|
+
|
|
173
|
+
# Verify final state
|
|
174
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
175
|
+
assert retrieved is not None
|
|
176
|
+
assert retrieved.status == AssemblyStatus.COMPLETED
|
|
177
|
+
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
async def test_save_updates_timestamp(
|
|
180
|
+
self,
|
|
181
|
+
assembly_repo: MinioAssemblyRepository,
|
|
182
|
+
sample_assembly: Assembly,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Test that save operations update the updated_at timestamp."""
|
|
185
|
+
original_updated_at = sample_assembly.updated_at
|
|
186
|
+
|
|
187
|
+
# Save assembly
|
|
188
|
+
await assembly_repo.save(sample_assembly)
|
|
189
|
+
|
|
190
|
+
# Retrieve and check timestamp was updated
|
|
191
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
192
|
+
assert retrieved is not None
|
|
193
|
+
assert retrieved.updated_at is not None
|
|
194
|
+
assert original_updated_at is not None
|
|
195
|
+
assert retrieved.updated_at > original_updated_at
|
|
196
|
+
|
|
197
|
+
@pytest.mark.asyncio
|
|
198
|
+
async def test_save_preserves_assembled_document_id(
|
|
199
|
+
self,
|
|
200
|
+
assembly_repo: MinioAssemblyRepository,
|
|
201
|
+
sample_assembly: Assembly,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Test that save operations preserve assembled_document_id."""
|
|
204
|
+
# Set assembled document first
|
|
205
|
+
sample_assembly.assembled_document_id = "test-doc-123"
|
|
206
|
+
sample_assembly.status = AssemblyStatus.COMPLETED
|
|
207
|
+
await assembly_repo.save(sample_assembly)
|
|
208
|
+
|
|
209
|
+
# Update other fields and save again
|
|
210
|
+
sample_assembly.status = AssemblyStatus.FAILED
|
|
211
|
+
await assembly_repo.save(sample_assembly)
|
|
212
|
+
|
|
213
|
+
# Verify assembled_document_id is preserved
|
|
214
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
215
|
+
assert retrieved is not None
|
|
216
|
+
assert retrieved.assembled_document_id == "test-doc-123"
|
|
217
|
+
assert retrieved.status == AssemblyStatus.FAILED
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestMinioAssemblyRepositoryEdgeCases:
|
|
221
|
+
"""Test edge cases and error conditions."""
|
|
222
|
+
|
|
223
|
+
@pytest.mark.asyncio
|
|
224
|
+
async def test_assembly_with_no_assembled_document(
|
|
225
|
+
self,
|
|
226
|
+
assembly_repo: MinioAssemblyRepository,
|
|
227
|
+
sample_assembly: Assembly,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Test handling assembly with no assembled document."""
|
|
230
|
+
await assembly_repo.save(sample_assembly)
|
|
231
|
+
|
|
232
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
233
|
+
assert retrieved is not None
|
|
234
|
+
assert retrieved.assembled_document_id is None
|
|
235
|
+
|
|
236
|
+
@pytest.mark.asyncio
|
|
237
|
+
async def test_complex_workflow_scenario(
|
|
238
|
+
self,
|
|
239
|
+
assembly_repo: MinioAssemblyRepository,
|
|
240
|
+
sample_assembly: Assembly,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Test complex scenario with multiple operations."""
|
|
243
|
+
# Save initial assembly
|
|
244
|
+
await assembly_repo.save(sample_assembly)
|
|
245
|
+
|
|
246
|
+
# Start processing
|
|
247
|
+
sample_assembly.status = AssemblyStatus.IN_PROGRESS
|
|
248
|
+
await assembly_repo.save(sample_assembly)
|
|
249
|
+
|
|
250
|
+
# Set assembled document using direct assignment
|
|
251
|
+
sample_assembly.assembled_document_id = "final-output-doc"
|
|
252
|
+
sample_assembly.status = AssemblyStatus.COMPLETED
|
|
253
|
+
await assembly_repo.save(sample_assembly)
|
|
254
|
+
|
|
255
|
+
# Verify final state
|
|
256
|
+
updated_assembly = await assembly_repo.get(sample_assembly.assembly_id)
|
|
257
|
+
assert updated_assembly is not None
|
|
258
|
+
|
|
259
|
+
# Verify final state
|
|
260
|
+
assert updated_assembly.assembled_document_id == "final-output-doc"
|
|
261
|
+
assert updated_assembly.status == AssemblyStatus.COMPLETED
|
|
262
|
+
|
|
263
|
+
# Double-check by retrieving fresh
|
|
264
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
265
|
+
assert retrieved is not None
|
|
266
|
+
assert retrieved.assembled_document_id == "final-output-doc"
|
|
267
|
+
assert retrieved.status == AssemblyStatus.COMPLETED
|
|
268
|
+
|
|
269
|
+
@pytest.mark.asyncio
|
|
270
|
+
async def test_assembly_failure_scenario(
|
|
271
|
+
self,
|
|
272
|
+
assembly_repo: MinioAssemblyRepository,
|
|
273
|
+
sample_assembly: Assembly,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Test assembly that fails during processing."""
|
|
276
|
+
# Save initial assembly
|
|
277
|
+
await assembly_repo.save(sample_assembly)
|
|
278
|
+
|
|
279
|
+
# Start processing
|
|
280
|
+
sample_assembly.status = AssemblyStatus.IN_PROGRESS
|
|
281
|
+
await assembly_repo.save(sample_assembly)
|
|
282
|
+
|
|
283
|
+
# Mark as failed (without setting assembled document)
|
|
284
|
+
sample_assembly.status = AssemblyStatus.FAILED
|
|
285
|
+
await assembly_repo.save(sample_assembly)
|
|
286
|
+
|
|
287
|
+
# Verify final state - no assembled document
|
|
288
|
+
retrieved = await assembly_repo.get(sample_assembly.assembly_id)
|
|
289
|
+
assert retrieved is not None
|
|
290
|
+
assert retrieved.assembled_document_id is None
|
|
291
|
+
assert retrieved.status == AssemblyStatus.FAILED
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TestMinioAssemblyRepositoryRoundtrip:
|
|
295
|
+
"""Test full round-trip scenarios."""
|
|
296
|
+
|
|
297
|
+
@pytest.mark.asyncio
|
|
298
|
+
async def test_full_assembly_lifecycle_success(
|
|
299
|
+
self, assembly_repo: MinioAssemblyRepository
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Test complete successful assembly lifecycle from creation to
|
|
302
|
+
completion."""
|
|
303
|
+
# Generate new assembly
|
|
304
|
+
assembly_id = await assembly_repo.generate_id()
|
|
305
|
+
|
|
306
|
+
# Create and save initial assembly
|
|
307
|
+
assembly = Assembly(
|
|
308
|
+
assembly_id=assembly_id,
|
|
309
|
+
assembly_specification_id="spec-test",
|
|
310
|
+
input_document_id="input-test",
|
|
311
|
+
workflow_id="test-workflow-success",
|
|
312
|
+
status=AssemblyStatus.PENDING,
|
|
313
|
+
assembled_document_id=None,
|
|
314
|
+
created_at=datetime.now(timezone.utc),
|
|
315
|
+
updated_at=datetime.now(timezone.utc),
|
|
316
|
+
)
|
|
317
|
+
await assembly_repo.save(assembly)
|
|
318
|
+
|
|
319
|
+
# Start processing
|
|
320
|
+
assembly.status = AssemblyStatus.IN_PROGRESS
|
|
321
|
+
await assembly_repo.save(assembly)
|
|
322
|
+
|
|
323
|
+
# Complete assembly with output document
|
|
324
|
+
assembly.assembled_document_id = "final-output-document"
|
|
325
|
+
assembly.status = AssemblyStatus.COMPLETED
|
|
326
|
+
await assembly_repo.save(assembly)
|
|
327
|
+
|
|
328
|
+
final_assembly = await assembly_repo.get(assembly_id)
|
|
329
|
+
assert final_assembly is not None
|
|
330
|
+
|
|
331
|
+
# Final verification
|
|
332
|
+
assert final_assembly.status == AssemblyStatus.COMPLETED
|
|
333
|
+
assert final_assembly.assembled_document_id == "final-output-document"
|
|
334
|
+
|
|
335
|
+
# Verify persistence
|
|
336
|
+
retrieved = await assembly_repo.get(assembly_id)
|
|
337
|
+
assert retrieved is not None
|
|
338
|
+
assert retrieved.status == AssemblyStatus.COMPLETED
|
|
339
|
+
assert retrieved.assembled_document_id == "final-output-document"
|
|
340
|
+
|
|
341
|
+
@pytest.mark.asyncio
|
|
342
|
+
async def test_full_assembly_lifecycle_failure(
|
|
343
|
+
self, assembly_repo: MinioAssemblyRepository
|
|
344
|
+
) -> None:
|
|
345
|
+
"""Test complete failed assembly lifecycle."""
|
|
346
|
+
# Generate new assembly
|
|
347
|
+
assembly_id = await assembly_repo.generate_id()
|
|
348
|
+
|
|
349
|
+
# Create and save initial assembly
|
|
350
|
+
assembly = Assembly(
|
|
351
|
+
assembly_id=assembly_id,
|
|
352
|
+
assembly_specification_id="spec-test",
|
|
353
|
+
input_document_id="input-test",
|
|
354
|
+
workflow_id="test-workflow-failure",
|
|
355
|
+
status=AssemblyStatus.PENDING,
|
|
356
|
+
assembled_document_id=None,
|
|
357
|
+
created_at=datetime.now(timezone.utc),
|
|
358
|
+
updated_at=datetime.now(timezone.utc),
|
|
359
|
+
)
|
|
360
|
+
await assembly_repo.save(assembly)
|
|
361
|
+
|
|
362
|
+
# Start processing
|
|
363
|
+
assembly.status = AssemblyStatus.IN_PROGRESS
|
|
364
|
+
await assembly_repo.save(assembly)
|
|
365
|
+
|
|
366
|
+
# Fail assembly (without setting output document)
|
|
367
|
+
assembly.status = AssemblyStatus.FAILED
|
|
368
|
+
await assembly_repo.save(assembly)
|
|
369
|
+
|
|
370
|
+
# Final verification
|
|
371
|
+
retrieved = await assembly_repo.get(assembly_id)
|
|
372
|
+
assert retrieved is not None
|
|
373
|
+
assert retrieved.status == AssemblyStatus.FAILED
|
|
374
|
+
assert retrieved.assembled_document_id is None
|