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,250 @@
1
+ """
2
+ Tests for API request models.
3
+
4
+ Since the request models delegate validation to domain models, these tests
5
+ focus on verifying the delegation works correctly and that the API-specific
6
+ behavior (like field copying and conversion methods) functions as expected.
7
+ """
8
+
9
+ import pytest
10
+ from datetime import datetime
11
+ from pydantic import ValidationError
12
+
13
+ from julee.api.requests import (
14
+ CreateAssemblySpecificationRequest,
15
+ CreateKnowledgeServiceQueryRequest,
16
+ )
17
+ from julee.domain.models import (
18
+ AssemblySpecification,
19
+ AssemblySpecificationStatus,
20
+ KnowledgeServiceQuery,
21
+ )
22
+
23
+
24
+ class TestCreateAssemblySpecificationRequest:
25
+ """Test CreateAssemblySpecificationRequest model."""
26
+
27
+ def test_valid_request_creation(self) -> None:
28
+ """Test that a valid request can be created."""
29
+ request = CreateAssemblySpecificationRequest(
30
+ name="Meeting Minutes",
31
+ applicability="Online video meeting transcripts",
32
+ jsonschema={
33
+ "type": "object",
34
+ "properties": {"title": {"type": "string"}},
35
+ },
36
+ )
37
+
38
+ assert request.name == "Meeting Minutes"
39
+ assert request.applicability == "Online video meeting transcripts"
40
+ assert request.jsonschema == {
41
+ "type": "object",
42
+ "properties": {"title": {"type": "string"}},
43
+ }
44
+ assert request.knowledge_service_queries == {} # Default empty dict
45
+ assert request.version == "0.1.0" # Default version
46
+
47
+ def test_validation_delegation_to_domain_model(self) -> None:
48
+ """Test that validation is properly delegated to domain model."""
49
+ # Test that domain model validation errors are raised
50
+ with pytest.raises(ValidationError) as err:
51
+ CreateAssemblySpecificationRequest(
52
+ name="", # Invalid empty name
53
+ applicability="Valid applicability",
54
+ jsonschema={"type": "object"},
55
+ )
56
+ errors = err.value.errors()
57
+ # Check that the error is for the 'name' field and is a value error
58
+ assert any(
59
+ e["loc"] == ("name",)
60
+ and e["type"].startswith("value_error")
61
+ and "name cannot be empty" in e["msg"]
62
+ for e in errors
63
+ )
64
+
65
+ with pytest.raises(ValidationError) as err:
66
+ CreateAssemblySpecificationRequest(
67
+ name="Valid Name",
68
+ applicability="Valid applicability",
69
+ jsonschema={"invalid": "schema"}, # Missing 'type' field
70
+ )
71
+ errors = err.value.errors()
72
+ # Check that the error is for the 'jsonschema' field
73
+ assert any(
74
+ e["loc"] == ("jsonschema",)
75
+ and e["type"].startswith("value_error")
76
+ and "type" in e["msg"]
77
+ for e in errors
78
+ )
79
+
80
+ def test_to_domain_model_conversion(self) -> None:
81
+ """Test conversion from request model to domain model."""
82
+ request = CreateAssemblySpecificationRequest(
83
+ name="Test Assembly",
84
+ applicability="Test documents",
85
+ jsonschema={
86
+ "type": "object",
87
+ "properties": {"content": {"type": "string"}},
88
+ },
89
+ knowledge_service_queries={"/properties/content": "query-123"},
90
+ version="1.0.0",
91
+ )
92
+
93
+ domain_model = request.to_domain_model("spec-456")
94
+
95
+ assert isinstance(domain_model, AssemblySpecification)
96
+ assert domain_model.assembly_specification_id == "spec-456"
97
+ assert domain_model.name == "Test Assembly"
98
+ assert domain_model.applicability == "Test documents"
99
+ assert domain_model.jsonschema == {
100
+ "type": "object",
101
+ "properties": {"content": {"type": "string"}},
102
+ }
103
+ assert domain_model.knowledge_service_queries == {
104
+ "/properties/content": "query-123"
105
+ }
106
+ assert domain_model.version == "1.0.0"
107
+ assert domain_model.status == AssemblySpecificationStatus.DRAFT
108
+ assert isinstance(domain_model.created_at, datetime)
109
+ assert isinstance(domain_model.updated_at, datetime)
110
+
111
+ def test_field_definitions_match_domain_model(self) -> None:
112
+ """Test that field definitions are copied from domain model."""
113
+ request_fields = CreateAssemblySpecificationRequest.model_fields
114
+ domain_fields = AssemblySpecification.model_fields
115
+
116
+ # Verify shared fields have identical definitions
117
+ shared_field_names = [
118
+ "name",
119
+ "applicability",
120
+ "jsonschema",
121
+ "knowledge_service_queries",
122
+ "version",
123
+ ]
124
+
125
+ for field_name in shared_field_names:
126
+ assert field_name in request_fields
127
+ assert field_name in domain_fields
128
+ # Field descriptions should match
129
+ assert (
130
+ request_fields[field_name].description
131
+ == domain_fields[field_name].description
132
+ )
133
+ # Default values should match where applicable
134
+ if (
135
+ hasattr(domain_fields[field_name], "default")
136
+ and domain_fields[field_name].default is not None
137
+ ):
138
+ assert (
139
+ request_fields[field_name].default
140
+ == domain_fields[field_name].default
141
+ )
142
+
143
+
144
+ class TestCreateKnowledgeServiceQueryRequest:
145
+ """Test CreateKnowledgeServiceQueryRequest model."""
146
+
147
+ def test_valid_request_creation(self) -> None:
148
+ """Test that a valid request can be created."""
149
+ request = CreateKnowledgeServiceQueryRequest(
150
+ name="Extract Meeting Summary",
151
+ knowledge_service_id="anthropic-claude",
152
+ prompt="Extract the main summary from this meeting transcript",
153
+ )
154
+
155
+ assert request.name == "Extract Meeting Summary"
156
+ assert request.knowledge_service_id == "anthropic-claude"
157
+ assert request.prompt == "Extract the main summary from this meeting transcript"
158
+ assert request.query_metadata == {} # Default empty dict
159
+ assert request.assistant_prompt is None # Default None
160
+
161
+ def test_validation_delegation_to_domain_model(self) -> None:
162
+ """Test that validation is properly delegated to domain model."""
163
+ # Test that domain model validation errors are raised
164
+ with pytest.raises(ValidationError) as err:
165
+ CreateKnowledgeServiceQueryRequest(
166
+ name="", # Invalid empty name
167
+ knowledge_service_id="valid-service",
168
+ prompt="Valid prompt",
169
+ )
170
+ errors = err.value.errors()
171
+ # Check that the error is for the 'name' field
172
+ assert any(
173
+ e["loc"] == ("name",)
174
+ and e["type"].startswith("value_error")
175
+ and "name cannot be empty" in e["msg"]
176
+ for e in errors
177
+ )
178
+
179
+ with pytest.raises(ValidationError) as err:
180
+ CreateKnowledgeServiceQueryRequest(
181
+ name="Valid Name",
182
+ knowledge_service_id="", # Invalid empty service ID
183
+ prompt="Valid prompt",
184
+ )
185
+ errors = err.value.errors()
186
+ # Check that the error is for the 'knowledge_service_id' field
187
+ assert any(
188
+ e["loc"] == ("knowledge_service_id",)
189
+ and e["type"].startswith("value_error")
190
+ and "service ID cannot be empty" in e["msg"]
191
+ for e in errors
192
+ )
193
+
194
+ def test_to_domain_model_conversion(self) -> None:
195
+ """Test conversion from request model to domain model."""
196
+ request = CreateKnowledgeServiceQueryRequest(
197
+ name="Test Query",
198
+ knowledge_service_id="test-service",
199
+ prompt="Test prompt for extraction",
200
+ query_metadata={"model": "claude-3", "temperature": 0.2},
201
+ assistant_prompt="Please format as JSON",
202
+ )
203
+
204
+ domain_model = request.to_domain_model("query-456")
205
+
206
+ assert isinstance(domain_model, KnowledgeServiceQuery)
207
+ assert domain_model.query_id == "query-456"
208
+ assert domain_model.name == "Test Query"
209
+ assert domain_model.knowledge_service_id == "test-service"
210
+ assert domain_model.prompt == "Test prompt for extraction"
211
+ assert domain_model.query_metadata == {
212
+ "model": "claude-3",
213
+ "temperature": 0.2,
214
+ }
215
+ assert domain_model.assistant_prompt == "Please format as JSON"
216
+ assert isinstance(domain_model.created_at, datetime)
217
+ assert isinstance(domain_model.updated_at, datetime)
218
+ assert domain_model.created_at == domain_model.updated_at
219
+
220
+ def test_field_definitions_match_domain_model(self) -> None:
221
+ """Test that field definitions are copied from domain model."""
222
+ request_fields = CreateKnowledgeServiceQueryRequest.model_fields
223
+ domain_fields = KnowledgeServiceQuery.model_fields
224
+
225
+ # Verify shared fields have identical descriptions
226
+ shared_field_names = [
227
+ "name",
228
+ "knowledge_service_id",
229
+ "prompt",
230
+ "query_metadata",
231
+ "assistant_prompt",
232
+ ]
233
+
234
+ for field_name in shared_field_names:
235
+ assert field_name in request_fields
236
+ assert field_name in domain_fields
237
+ # Field descriptions should match
238
+ assert (
239
+ request_fields[field_name].description
240
+ == domain_fields[field_name].description
241
+ )
242
+ # Default values should match where applicable
243
+ if (
244
+ hasattr(domain_fields[field_name], "default")
245
+ and domain_fields[field_name].default is not None
246
+ ):
247
+ assert (
248
+ request_fields[field_name].default
249
+ == domain_fields[field_name].default
250
+ )
@@ -0,0 +1,22 @@
1
+ """
2
+ Domain layer for julee.
3
+
4
+ This package contains the core business logic and domain models following
5
+ Clean Architecture principles. All domain concerns are framework-independent
6
+ and have no external dependencies.
7
+
8
+ Subpackages:
9
+ - models: Domain entities and value objects
10
+ - repositories: Repository interface protocols
11
+ - use_cases: Business logic and application services
12
+
13
+ Import domain components using package imports for convenience, e.g.:
14
+ # Models from the models package
15
+ from julee.domain.models import Document, Assembly, Policy
16
+
17
+ # Repository protocols from the repositories package
18
+ from julee.domain.repositories import DocumentRepository
19
+
20
+ # Use cases from the use_cases package
21
+ from julee.domain.use_cases import ValidateDocumentUseCase
22
+ """
@@ -0,0 +1,49 @@
1
+ """
2
+ Domain models for julee.
3
+
4
+ This package contains all the domain entities and value objects following
5
+ Clean Architecture principles. These models are framework-independent and
6
+ contain only business logic.
7
+
8
+ Re-exports commonly used models for convenient importing:
9
+ from julee.domain.models import Document, Assembly, Policy
10
+ """
11
+
12
+ # Document models
13
+ from .document import Document, DocumentStatus
14
+
15
+ # Custom field types
16
+ from .custom_fields.content_stream import ContentStream
17
+
18
+ # Assembly models
19
+ from .assembly import Assembly, AssemblyStatus
20
+ from .assembly_specification import (
21
+ AssemblySpecification,
22
+ AssemblySpecificationStatus,
23
+ KnowledgeServiceQuery,
24
+ )
25
+
26
+ # Configuration models
27
+ from .knowledge_service_config import KnowledgeServiceConfig
28
+
29
+ # Policy models
30
+ from .policy import Policy, PolicyStatus, DocumentPolicyValidation
31
+
32
+ __all__ = [
33
+ # Document models
34
+ "Document",
35
+ "DocumentStatus",
36
+ "ContentStream",
37
+ # Assembly models
38
+ "Assembly",
39
+ "AssemblyStatus",
40
+ "AssemblySpecification",
41
+ "AssemblySpecificationStatus",
42
+ "KnowledgeServiceQuery",
43
+ # Configuration models
44
+ "KnowledgeServiceConfig",
45
+ # Policy models
46
+ "Policy",
47
+ "PolicyStatus",
48
+ "DocumentPolicyValidation",
49
+ ]
@@ -0,0 +1,17 @@
1
+ """
2
+ Assembly domain package for the Capture, Extract, Assemble, Publish workflow.
3
+
4
+ This package contains the Assembly domain object that represents assembly
5
+ processes in the CEAP workflow.
6
+
7
+ Assembly represents a specific instance of assembling a document using an
8
+ AssemblySpecification, linking an input document with the specification and
9
+ producing a single assembled document as output.
10
+ """
11
+
12
+ from .assembly import Assembly, AssemblyStatus
13
+
14
+ __all__ = [
15
+ "Assembly",
16
+ "AssemblyStatus",
17
+ ]
@@ -0,0 +1,103 @@
1
+ """
2
+ Assembly domain models for the Capture, Extract, Assemble, Publish workflow.
3
+
4
+ This module contains the Assembly domain object that represents the actual
5
+ assembly process/instance in the CEAP workflow system.
6
+
7
+ An Assembly represents a specific instance of assembling a document using
8
+ an AssemblySpecification. It links an input document with an assembly
9
+ specification and produces a single assembled document as output.
10
+
11
+ All domain models use Pydantic BaseModel for validation, serialization,
12
+ and type safety, following the patterns established in the sample project.
13
+ """
14
+
15
+ from pydantic import BaseModel, Field, field_validator
16
+ from typing import Optional
17
+ from datetime import datetime, timezone
18
+ from enum import Enum
19
+
20
+
21
+ class AssemblyStatus(str, Enum):
22
+ """Status of an assembly process."""
23
+
24
+ PENDING = "pending"
25
+ IN_PROGRESS = "in_progress"
26
+ COMPLETED = "completed"
27
+ FAILED = "failed"
28
+ CANCELLED = "cancelled"
29
+
30
+
31
+ class Assembly(BaseModel):
32
+ """Assembly process that links a specification with input document and
33
+ produces an assembled document.
34
+
35
+ An Assembly represents a specific instance of the document assembly
36
+ process. It connects an AssemblySpecification (which defines how to
37
+ assemble) with an input Document (what to assemble from) and produces
38
+ a single assembled document as output.
39
+ """
40
+
41
+ # Core assembly identification
42
+ assembly_id: str = Field(description="Unique identifier for this assembly instance")
43
+ assembly_specification_id: str = Field(
44
+ description="ID of the AssemblySpecification defining how to assemble"
45
+ )
46
+ input_document_id: str = Field(
47
+ description="ID of the input document to assemble from"
48
+ )
49
+ workflow_id: str = Field(
50
+ description="Temporal workflow ID that created this assembly"
51
+ )
52
+
53
+ # Assembly process tracking
54
+ status: AssemblyStatus = AssemblyStatus.PENDING
55
+ assembled_document_id: Optional[str] = Field(
56
+ default=None,
57
+ description="ID of the assembled document produced by this assembly",
58
+ )
59
+
60
+ # Assembly metadata
61
+ created_at: Optional[datetime] = Field(
62
+ default_factory=lambda: datetime.now(timezone.utc)
63
+ )
64
+ updated_at: Optional[datetime] = Field(
65
+ default_factory=lambda: datetime.now(timezone.utc)
66
+ )
67
+
68
+ @field_validator("assembly_id")
69
+ @classmethod
70
+ def assembly_id_must_not_be_empty(cls, v: str) -> str:
71
+ if not v or not v.strip():
72
+ raise ValueError("Assembly ID cannot be empty")
73
+ return v.strip()
74
+
75
+ @field_validator("assembly_specification_id")
76
+ @classmethod
77
+ def assembly_specification_id_must_not_be_empty(cls, v: str) -> str:
78
+ if not v or not v.strip():
79
+ raise ValueError("Assembly specification ID cannot be empty")
80
+ return v.strip()
81
+
82
+ @field_validator("input_document_id")
83
+ @classmethod
84
+ def input_document_id_must_not_be_empty(cls, v: str) -> str:
85
+ if not v or not v.strip():
86
+ raise ValueError("Input document ID cannot be empty")
87
+ return v.strip()
88
+
89
+ @field_validator("assembled_document_id")
90
+ @classmethod
91
+ def assembled_document_id_must_not_be_empty_if_provided(
92
+ cls, v: Optional[str]
93
+ ) -> Optional[str]:
94
+ if v is not None and (not v or not v.strip()):
95
+ raise ValueError("Assembled document ID cannot be empty string")
96
+ return v.strip() if v else None
97
+
98
+ @field_validator("workflow_id")
99
+ @classmethod
100
+ def workflow_id_must_not_be_empty(cls, v: str) -> str:
101
+ if not v or not v.strip():
102
+ raise ValueError("Workflow ID cannot be empty")
103
+ return v.strip()
File without changes
@@ -0,0 +1,37 @@
1
+ """
2
+ Test factories for Assembly domain objects using factory_boy.
3
+
4
+ This module provides factory_boy factories for creating test instances of
5
+ Assembly domain objects with sensible defaults.
6
+ """
7
+
8
+ from datetime import datetime, timezone
9
+ from factory.base import Factory
10
+ from factory.faker import Faker
11
+ from factory.declarations import LazyFunction
12
+
13
+ from julee.domain.models.assembly import (
14
+ Assembly,
15
+ AssemblyStatus,
16
+ )
17
+
18
+
19
+ class AssemblyFactory(Factory):
20
+ """Factory for creating Assembly instances with sensible test defaults."""
21
+
22
+ class Meta:
23
+ model = Assembly
24
+
25
+ # Core assembly identification
26
+ assembly_id = Faker("uuid4")
27
+ assembly_specification_id = Faker("uuid4")
28
+ input_document_id = Faker("uuid4")
29
+ workflow_id = Faker("uuid4")
30
+
31
+ # Assembly process tracking
32
+ status = AssemblyStatus.PENDING
33
+ assembled_document_id = None
34
+
35
+ # Timestamps
36
+ created_at = LazyFunction(lambda: datetime.now(timezone.utc))
37
+ updated_at = LazyFunction(lambda: datetime.now(timezone.utc))