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,443 @@
1
+ """
2
+ Tests for MemoryPolicyRepository implementation.
3
+
4
+ This module provides comprehensive tests for the memory-based policy
5
+ repository implementation, following the testing patterns established in the
6
+ project.
7
+ """
8
+
9
+ import pytest
10
+ from datetime import datetime, timezone
11
+
12
+ from julee.domain.models.policy import Policy, PolicyStatus
13
+ from julee.repositories.memory.policy import MemoryPolicyRepository
14
+
15
+
16
+ @pytest.fixture
17
+ def policy_repo() -> MemoryPolicyRepository:
18
+ """Create a fresh policy repository for each test."""
19
+ return MemoryPolicyRepository()
20
+
21
+
22
+ @pytest.fixture
23
+ def sample_policy() -> Policy:
24
+ """Create a sample policy for testing."""
25
+ return Policy(
26
+ policy_id="policy-test-123",
27
+ title="Content Quality Policy",
28
+ description="Validates content meets quality standards",
29
+ status=PolicyStatus.ACTIVE,
30
+ validation_scores=[
31
+ ("quality-check-query", 80),
32
+ ("completeness-check", 90),
33
+ ],
34
+ transformation_queries=["improve-quality", "fix-grammar"],
35
+ version="1.0.0",
36
+ created_at=datetime.now(timezone.utc),
37
+ updated_at=datetime.now(timezone.utc),
38
+ )
39
+
40
+
41
+ @pytest.fixture
42
+ def validation_only_policy() -> Policy:
43
+ """Create a validation-only policy (no transformations) for testing."""
44
+ return Policy(
45
+ policy_id="policy-validation-only",
46
+ title="Validation Only Policy",
47
+ description="Only validates content without transformations",
48
+ status=PolicyStatus.ACTIVE,
49
+ validation_scores=[
50
+ ("basic-validation", 70),
51
+ ],
52
+ transformation_queries=[], # Empty list - validation only
53
+ version="1.0.0",
54
+ created_at=datetime.now(timezone.utc),
55
+ updated_at=datetime.now(timezone.utc),
56
+ )
57
+
58
+
59
+ class TestMemoryPolicyRepositoryBasicOperations:
60
+ """Test basic CRUD operations on policies."""
61
+
62
+ @pytest.mark.asyncio
63
+ async def test_save_and_get_policy(
64
+ self,
65
+ policy_repo: MemoryPolicyRepository,
66
+ sample_policy: Policy,
67
+ ) -> None:
68
+ """Test saving and retrieving a policy."""
69
+ # Save policy
70
+ await policy_repo.save(sample_policy)
71
+
72
+ # Retrieve policy
73
+ retrieved = await policy_repo.get(sample_policy.policy_id)
74
+
75
+ assert retrieved is not None
76
+ assert retrieved.policy_id == sample_policy.policy_id
77
+ assert retrieved.title == sample_policy.title
78
+ assert retrieved.description == sample_policy.description
79
+ assert retrieved.status == sample_policy.status
80
+ assert retrieved.validation_scores == sample_policy.validation_scores
81
+ assert retrieved.transformation_queries == sample_policy.transformation_queries
82
+ assert retrieved.version == sample_policy.version
83
+
84
+ @pytest.mark.asyncio
85
+ async def test_get_nonexistent_policy(
86
+ self, policy_repo: MemoryPolicyRepository
87
+ ) -> None:
88
+ """Test retrieving a non-existent policy returns None."""
89
+ result = await policy_repo.get("nonexistent-policy")
90
+ assert result is None
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_generate_id(self, policy_repo: MemoryPolicyRepository) -> None:
94
+ """Test generating unique policy IDs."""
95
+ id1 = await policy_repo.generate_id()
96
+ id2 = await policy_repo.generate_id()
97
+
98
+ assert isinstance(id1, str)
99
+ assert isinstance(id2, str)
100
+ assert id1 != id2
101
+ assert len(id1) > 0
102
+ assert len(id2) > 0
103
+ assert id1.startswith("policy-")
104
+ assert id2.startswith("policy-")
105
+
106
+
107
+ class TestMemoryPolicyRepositoryPolicyTypes:
108
+ """Test handling of different policy types."""
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_validation_only_policy(
112
+ self,
113
+ policy_repo: MemoryPolicyRepository,
114
+ validation_only_policy: Policy,
115
+ ) -> None:
116
+ """Test storing and retrieving validation-only policy."""
117
+ # Save validation-only policy
118
+ await policy_repo.save(validation_only_policy)
119
+
120
+ # Retrieve and verify
121
+ retrieved = await policy_repo.get(validation_only_policy.policy_id)
122
+ assert retrieved is not None
123
+ assert retrieved.is_validation_only is True
124
+ assert retrieved.has_transformations is False
125
+ assert retrieved.transformation_queries == []
126
+
127
+ @pytest.mark.asyncio
128
+ async def test_transformation_policy(
129
+ self,
130
+ policy_repo: MemoryPolicyRepository,
131
+ sample_policy: Policy,
132
+ ) -> None:
133
+ """Test storing and retrieving policy with transformations."""
134
+ # Save transformation policy
135
+ await policy_repo.save(sample_policy)
136
+
137
+ # Retrieve and verify
138
+ retrieved = await policy_repo.get(sample_policy.policy_id)
139
+ assert retrieved is not None
140
+ assert retrieved.is_validation_only is False
141
+ assert retrieved.has_transformations is True
142
+ assert retrieved.transformation_queries is not None
143
+ assert len(retrieved.transformation_queries) == 2
144
+ assert "improve-quality" in retrieved.transformation_queries
145
+ assert "fix-grammar" in retrieved.transformation_queries
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_policy_with_none_transformations(
149
+ self, policy_repo: MemoryPolicyRepository
150
+ ) -> None:
151
+ """Test policy with None transformation queries."""
152
+ policy = Policy(
153
+ policy_id="policy-none-transforms",
154
+ title="Policy with None Transformations",
155
+ description="Policy where transformation_queries is None",
156
+ validation_scores=[("test-query", 75)],
157
+ transformation_queries=None, # Explicitly None
158
+ )
159
+
160
+ await policy_repo.save(policy)
161
+ retrieved = await policy_repo.get(policy.policy_id)
162
+
163
+ assert retrieved is not None
164
+ assert retrieved.transformation_queries is None
165
+ assert retrieved.is_validation_only is True
166
+ assert retrieved.has_transformations is False
167
+
168
+
169
+ class TestMemoryPolicyRepositoryUpdates:
170
+ """Test policy update operations."""
171
+
172
+ @pytest.mark.asyncio
173
+ async def test_update_policy_status(
174
+ self,
175
+ policy_repo: MemoryPolicyRepository,
176
+ sample_policy: Policy,
177
+ ) -> None:
178
+ """Test updating policy status."""
179
+ # Save initial policy
180
+ await policy_repo.save(sample_policy)
181
+
182
+ # Update status
183
+ sample_policy.status = PolicyStatus.INACTIVE
184
+ await policy_repo.save(sample_policy)
185
+
186
+ # Verify update
187
+ retrieved = await policy_repo.get(sample_policy.policy_id)
188
+ assert retrieved is not None
189
+ assert retrieved.status == PolicyStatus.INACTIVE
190
+
191
+ @pytest.mark.asyncio
192
+ async def test_update_policy_validation_scores(
193
+ self,
194
+ policy_repo: MemoryPolicyRepository,
195
+ sample_policy: Policy,
196
+ ) -> None:
197
+ """Test updating policy validation scores."""
198
+ # Save initial policy
199
+ await policy_repo.save(sample_policy)
200
+
201
+ # Update validation scores
202
+ sample_policy.validation_scores = [
203
+ ("new-quality-check", 85),
204
+ ("advanced-validation", 95),
205
+ ]
206
+ await policy_repo.save(sample_policy)
207
+
208
+ # Verify update
209
+ retrieved = await policy_repo.get(sample_policy.policy_id)
210
+ assert retrieved is not None
211
+ assert len(retrieved.validation_scores) == 2
212
+ assert ("new-quality-check", 85) in retrieved.validation_scores
213
+ assert ("advanced-validation", 95) in retrieved.validation_scores
214
+
215
+ @pytest.mark.asyncio
216
+ async def test_update_transformation_queries(
217
+ self,
218
+ policy_repo: MemoryPolicyRepository,
219
+ sample_policy: Policy,
220
+ ) -> None:
221
+ """Test updating transformation queries."""
222
+ # Save initial policy
223
+ await policy_repo.save(sample_policy)
224
+
225
+ # Update transformation queries
226
+ sample_policy.transformation_queries = ["new-transform"]
227
+ await policy_repo.save(sample_policy)
228
+
229
+ # Verify update
230
+ retrieved = await policy_repo.get(sample_policy.policy_id)
231
+ assert retrieved is not None
232
+ assert retrieved.transformation_queries == ["new-transform"]
233
+ assert retrieved.has_transformations is True
234
+
235
+ @pytest.mark.asyncio
236
+ async def test_save_updates_timestamp(
237
+ self,
238
+ policy_repo: MemoryPolicyRepository,
239
+ sample_policy: Policy,
240
+ ) -> None:
241
+ """Test that save operations update the updated_at timestamp."""
242
+ original_updated_at = sample_policy.updated_at
243
+
244
+ # Save policy
245
+ await policy_repo.save(sample_policy)
246
+
247
+ # Retrieve and check timestamp was updated
248
+ retrieved = await policy_repo.get(sample_policy.policy_id)
249
+ assert retrieved is not None
250
+ assert retrieved.updated_at is not None
251
+ assert original_updated_at is not None
252
+ assert retrieved.updated_at > original_updated_at
253
+
254
+
255
+ class TestMemoryPolicyRepositoryEdgeCases:
256
+ """Test edge cases and complex scenarios."""
257
+
258
+ @pytest.mark.asyncio
259
+ async def test_policy_with_complex_validation_scores(
260
+ self, policy_repo: MemoryPolicyRepository
261
+ ) -> None:
262
+ """Test policy with many validation scores."""
263
+ policy = Policy(
264
+ policy_id="complex-policy",
265
+ title="Complex Validation Policy",
266
+ description="Policy with multiple validation criteria",
267
+ validation_scores=[
268
+ ("grammar-check", 80),
269
+ ("completeness-check", 85),
270
+ ("accuracy-check", 90),
271
+ ("style-check", 75),
272
+ ("readability-check", 70),
273
+ ],
274
+ transformation_queries=["improve-all-aspects"],
275
+ )
276
+
277
+ await policy_repo.save(policy)
278
+ retrieved = await policy_repo.get("complex-policy")
279
+
280
+ assert retrieved is not None
281
+ assert len(retrieved.validation_scores) == 5
282
+ assert retrieved.has_transformations is True
283
+ assert retrieved.transformation_queries is not None
284
+ assert len(retrieved.transformation_queries) == 1
285
+
286
+ @pytest.mark.asyncio
287
+ async def test_policy_lifecycle_management(
288
+ self, policy_repo: MemoryPolicyRepository
289
+ ) -> None:
290
+ """Test complete policy lifecycle from draft to deprecated."""
291
+ # Create draft policy
292
+ policy = Policy(
293
+ policy_id="lifecycle-policy",
294
+ title="Lifecycle Test Policy",
295
+ description="Testing policy lifecycle",
296
+ status=PolicyStatus.DRAFT,
297
+ validation_scores=[("test-check", 80)],
298
+ version="0.1.0",
299
+ )
300
+
301
+ # Save as draft
302
+ await policy_repo.save(policy)
303
+ retrieved = await policy_repo.get("lifecycle-policy")
304
+ assert retrieved is not None
305
+ assert retrieved.status == PolicyStatus.DRAFT
306
+
307
+ # Activate policy
308
+ policy.status = PolicyStatus.ACTIVE
309
+ policy.version = "1.0.0"
310
+ await policy_repo.save(policy)
311
+
312
+ retrieved = await policy_repo.get("lifecycle-policy")
313
+ assert retrieved is not None
314
+ assert retrieved.status == PolicyStatus.ACTIVE
315
+ assert retrieved.version == "1.0.0"
316
+
317
+ # Deprecate policy
318
+ policy.status = PolicyStatus.DEPRECATED
319
+ await policy_repo.save(policy)
320
+
321
+ retrieved = await policy_repo.get("lifecycle-policy")
322
+ assert retrieved is not None
323
+ assert retrieved.status == PolicyStatus.DEPRECATED
324
+
325
+ @pytest.mark.asyncio
326
+ async def test_multiple_policies_independence(
327
+ self, policy_repo: MemoryPolicyRepository
328
+ ) -> None:
329
+ """Test that multiple policies are stored independently."""
330
+ # Create multiple policies
331
+ policy1 = Policy(
332
+ policy_id="policy-1",
333
+ title="First Policy",
334
+ description="First test policy",
335
+ validation_scores=[("check-1", 80)],
336
+ )
337
+
338
+ policy2 = Policy(
339
+ policy_id="policy-2",
340
+ title="Second Policy",
341
+ description="Second test policy",
342
+ validation_scores=[("check-2", 90)],
343
+ transformation_queries=["transform-2"],
344
+ )
345
+
346
+ # Save both policies
347
+ await policy_repo.save(policy1)
348
+ await policy_repo.save(policy2)
349
+
350
+ # Retrieve both and verify independence
351
+ retrieved1 = await policy_repo.get("policy-1")
352
+ retrieved2 = await policy_repo.get("policy-2")
353
+
354
+ assert retrieved1 is not None
355
+ assert retrieved2 is not None
356
+ assert retrieved1.title == "First Policy"
357
+ assert retrieved2.title == "Second Policy"
358
+ assert retrieved1.is_validation_only is True
359
+ assert retrieved2.has_transformations is True
360
+
361
+ # Update one policy and verify the other is unchanged
362
+ policy1.title = "Updated First Policy"
363
+ await policy_repo.save(policy1)
364
+
365
+ retrieved1_updated = await policy_repo.get("policy-1")
366
+ retrieved2_unchanged = await policy_repo.get("policy-2")
367
+
368
+ assert retrieved1_updated is not None
369
+ assert retrieved2_unchanged is not None
370
+ assert retrieved1_updated.title == "Updated First Policy"
371
+ assert retrieved2_unchanged.title == "Second Policy"
372
+
373
+
374
+ class TestMemoryPolicyRepositoryIdempotency:
375
+ """Test idempotent operations."""
376
+
377
+ @pytest.mark.asyncio
378
+ async def test_save_idempotency(
379
+ self,
380
+ policy_repo: MemoryPolicyRepository,
381
+ sample_policy: Policy,
382
+ ) -> None:
383
+ """Test that saving the same policy multiple times is idempotent."""
384
+ # Save policy first time
385
+ await policy_repo.save(sample_policy)
386
+
387
+ # Get the policy to check initial state
388
+ retrieved1 = await policy_repo.get(sample_policy.policy_id)
389
+ assert retrieved1 is not None
390
+ first_updated_at = retrieved1.updated_at
391
+
392
+ # Save same policy again (should update timestamp but maintain data)
393
+ await policy_repo.save(sample_policy)
394
+
395
+ # Verify policy is still there with updated timestamp
396
+ retrieved2 = await policy_repo.get(sample_policy.policy_id)
397
+ assert retrieved2 is not None
398
+ assert retrieved2.policy_id == sample_policy.policy_id
399
+ assert retrieved2.title == sample_policy.title
400
+ assert retrieved2.validation_scores == sample_policy.validation_scores
401
+ assert retrieved2.updated_at is not None
402
+ assert first_updated_at is not None
403
+ assert retrieved2.updated_at > first_updated_at
404
+
405
+
406
+ class TestMemoryPolicyRepositoryRoundtrip:
407
+ """Test full round-trip scenarios."""
408
+
409
+ @pytest.mark.asyncio
410
+ async def test_full_policy_lifecycle_success(
411
+ self, policy_repo: MemoryPolicyRepository
412
+ ) -> None:
413
+ """Test complete successful policy lifecycle."""
414
+ # Generate new policy ID
415
+ policy_id = await policy_repo.generate_id()
416
+
417
+ # Create and save initial policy
418
+ policy = Policy(
419
+ policy_id=policy_id,
420
+ title="Round-trip Test Policy",
421
+ description="Testing complete policy round-trip",
422
+ status=PolicyStatus.DRAFT,
423
+ validation_scores=[("round-trip-check", 85)],
424
+ transformation_queries=["round-trip-transform"],
425
+ version="0.1.0",
426
+ created_at=datetime.now(timezone.utc),
427
+ updated_at=datetime.now(timezone.utc),
428
+ )
429
+ await policy_repo.save(policy)
430
+
431
+ # Activate policy
432
+ policy.status = PolicyStatus.ACTIVE
433
+ policy.version = "1.0.0"
434
+ await policy_repo.save(policy)
435
+
436
+ # Final verification
437
+ retrieved = await policy_repo.get(policy_id)
438
+ assert retrieved is not None
439
+ assert retrieved.status == PolicyStatus.ACTIVE
440
+ assert retrieved.version == "1.0.0"
441
+ assert retrieved.has_transformations is True
442
+ assert len(retrieved.validation_scores) == 1
443
+ assert retrieved.validation_scores[0] == ("round-trip-check", 85)
@@ -0,0 +1,31 @@
1
+ """
2
+ Minio repository implementations for julee domain.
3
+
4
+ This module exports Minio-based implementations of all repository protocols
5
+ for the Capture, Extract, Assemble, Publish workflow. These implementations
6
+ use Minio for object storage and are suitable for production environments
7
+ where persistent, scalable storage is required.
8
+
9
+ All implementations maintain the same async interfaces as their memory
10
+ counterparts while providing durable, distributed storage capabilities.
11
+ """
12
+
13
+ from .assembly import MinioAssemblyRepository
14
+ from .assembly_specification import MinioAssemblySpecificationRepository
15
+ from .document import MinioDocumentRepository
16
+ from .document_policy_validation import (
17
+ MinioDocumentPolicyValidationRepository,
18
+ )
19
+ from .knowledge_service_config import MinioKnowledgeServiceConfigRepository
20
+ from .knowledge_service_query import MinioKnowledgeServiceQueryRepository
21
+ from .policy import MinioPolicyRepository
22
+
23
+ __all__ = [
24
+ "MinioAssemblyRepository",
25
+ "MinioAssemblySpecificationRepository",
26
+ "MinioDocumentRepository",
27
+ "MinioDocumentPolicyValidationRepository",
28
+ "MinioKnowledgeServiceConfigRepository",
29
+ "MinioKnowledgeServiceQueryRepository",
30
+ "MinioPolicyRepository",
31
+ ]
@@ -0,0 +1,103 @@
1
+ """
2
+ Minio implementation of AssemblyRepository.
3
+
4
+ This module provides a Minio-based implementation of the AssemblyRepository
5
+ protocol that follows the Clean Architecture patterns defined in the
6
+ Fun-Police Framework. It handles assembly storage, ensuring idempotency and
7
+ proper error handling.
8
+
9
+ The implementation stores assembly data as JSON objects in Minio, following
10
+ the large payload handling pattern from the architectural guidelines.
11
+ """
12
+
13
+ import logging
14
+ from typing import Optional, List, Dict
15
+
16
+ from julee.domain.models.assembly import Assembly
17
+ from julee.domain.repositories.assembly import AssemblyRepository
18
+ from .client import MinioClient, MinioRepositoryMixin
19
+
20
+
21
+ class MinioAssemblyRepository(AssemblyRepository, MinioRepositoryMixin):
22
+ """
23
+ Minio implementation of AssemblyRepository using Minio for persistence.
24
+
25
+ This implementation stores assembly data as JSON objects in the
26
+ "assemblies" bucket.
27
+ """
28
+
29
+ def __init__(self, client: MinioClient) -> None:
30
+ """Initialize repository with Minio client.
31
+
32
+ Args:
33
+ client: MinioClient protocol implementation (real or fake)
34
+ """
35
+ self.client = client
36
+ self.logger = logging.getLogger("MinioAssemblyRepository")
37
+ self.assembly_bucket = "assemblies"
38
+ self.ensure_buckets_exist([self.assembly_bucket])
39
+
40
+ async def get(self, assembly_id: str) -> Optional[Assembly]:
41
+ """Retrieve an assembly by ID."""
42
+ # Get the assembly using mixin methods
43
+ assembly = self.get_json_object(
44
+ bucket_name=self.assembly_bucket,
45
+ object_name=assembly_id,
46
+ model_class=Assembly,
47
+ not_found_log_message="Assembly not found",
48
+ error_log_message="Error retrieving assembly",
49
+ extra_log_data={"assembly_id": assembly_id},
50
+ )
51
+
52
+ return assembly
53
+
54
+ async def save(self, assembly: Assembly) -> None:
55
+ """Save assembly metadata (status, updated_at, etc.)."""
56
+ # Update timestamp
57
+ self.update_timestamps(assembly)
58
+
59
+ self.put_json_object(
60
+ bucket_name=self.assembly_bucket,
61
+ object_name=assembly.assembly_id,
62
+ model=assembly,
63
+ success_log_message="Assembly saved successfully",
64
+ error_log_message="Error saving assembly",
65
+ extra_log_data={
66
+ "assembly_id": assembly.assembly_id,
67
+ "status": assembly.status.value,
68
+ "assembled_document_id": assembly.assembled_document_id,
69
+ },
70
+ )
71
+
72
+ async def get_many(self, assembly_ids: List[str]) -> Dict[str, Optional[Assembly]]:
73
+ """Retrieve multiple assemblies by ID.
74
+
75
+ Args:
76
+ assembly_ids: List of unique assembly identifiers
77
+
78
+ Returns:
79
+ Dict mapping assembly_id to Assembly (or None if not found)
80
+ """
81
+ # Convert assembly IDs to object names (direct mapping in this case)
82
+ object_names = assembly_ids
83
+
84
+ # Get objects from Minio using batch method
85
+ object_results = self.get_many_json_objects(
86
+ bucket_name=self.assembly_bucket,
87
+ object_names=object_names,
88
+ model_class=Assembly,
89
+ not_found_log_message="Assembly not found",
90
+ error_log_message="Error retrieving assembly",
91
+ extra_log_data={"assembly_ids": assembly_ids},
92
+ )
93
+
94
+ # Convert object names back to assembly IDs for the result
95
+ result: Dict[str, Optional[Assembly]] = {}
96
+ for assembly_id in assembly_ids:
97
+ result[assembly_id] = object_results[assembly_id]
98
+
99
+ return result
100
+
101
+ async def generate_id(self) -> str:
102
+ """Generate a unique assembly identifier."""
103
+ return self.generate_id_with_prefix("assembly")