julee 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. julee/__init__.py +3 -0
  2. julee/api/__init__.py +20 -0
  3. julee/api/app.py +180 -0
  4. julee/api/dependencies.py +257 -0
  5. julee/api/requests.py +175 -0
  6. julee/api/responses.py +43 -0
  7. julee/api/routers/__init__.py +43 -0
  8. julee/api/routers/assembly_specifications.py +212 -0
  9. julee/api/routers/documents.py +182 -0
  10. julee/api/routers/knowledge_service_configs.py +79 -0
  11. julee/api/routers/knowledge_service_queries.py +293 -0
  12. julee/api/routers/system.py +137 -0
  13. julee/api/routers/workflows.py +234 -0
  14. julee/api/services/__init__.py +20 -0
  15. julee/api/services/system_initialization.py +214 -0
  16. julee/api/tests/__init__.py +14 -0
  17. julee/api/tests/routers/__init__.py +17 -0
  18. julee/api/tests/routers/test_assembly_specifications.py +749 -0
  19. julee/api/tests/routers/test_documents.py +301 -0
  20. julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
  21. julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
  22. julee/api/tests/routers/test_system.py +179 -0
  23. julee/api/tests/routers/test_workflows.py +393 -0
  24. julee/api/tests/test_app.py +285 -0
  25. julee/api/tests/test_dependencies.py +245 -0
  26. julee/api/tests/test_requests.py +250 -0
  27. julee/domain/__init__.py +22 -0
  28. julee/domain/models/__init__.py +49 -0
  29. julee/domain/models/assembly/__init__.py +17 -0
  30. julee/domain/models/assembly/assembly.py +103 -0
  31. julee/domain/models/assembly/tests/__init__.py +0 -0
  32. julee/domain/models/assembly/tests/factories.py +37 -0
  33. julee/domain/models/assembly/tests/test_assembly.py +430 -0
  34. julee/domain/models/assembly_specification/__init__.py +24 -0
  35. julee/domain/models/assembly_specification/assembly_specification.py +172 -0
  36. julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
  37. julee/domain/models/assembly_specification/tests/__init__.py +0 -0
  38. julee/domain/models/assembly_specification/tests/factories.py +78 -0
  39. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
  40. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
  41. julee/domain/models/custom_fields/__init__.py +0 -0
  42. julee/domain/models/custom_fields/content_stream.py +68 -0
  43. julee/domain/models/custom_fields/tests/__init__.py +0 -0
  44. julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
  45. julee/domain/models/document/__init__.py +17 -0
  46. julee/domain/models/document/document.py +150 -0
  47. julee/domain/models/document/tests/__init__.py +0 -0
  48. julee/domain/models/document/tests/factories.py +76 -0
  49. julee/domain/models/document/tests/test_document.py +297 -0
  50. julee/domain/models/knowledge_service_config/__init__.py +17 -0
  51. julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
  52. julee/domain/models/policy/__init__.py +15 -0
  53. julee/domain/models/policy/document_policy_validation.py +220 -0
  54. julee/domain/models/policy/policy.py +203 -0
  55. julee/domain/models/policy/tests/__init__.py +0 -0
  56. julee/domain/models/policy/tests/factories.py +47 -0
  57. julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
  58. julee/domain/models/policy/tests/test_policy.py +546 -0
  59. julee/domain/repositories/__init__.py +27 -0
  60. julee/domain/repositories/assembly.py +45 -0
  61. julee/domain/repositories/assembly_specification.py +52 -0
  62. julee/domain/repositories/base.py +146 -0
  63. julee/domain/repositories/document.py +49 -0
  64. julee/domain/repositories/document_policy_validation.py +52 -0
  65. julee/domain/repositories/knowledge_service_config.py +54 -0
  66. julee/domain/repositories/knowledge_service_query.py +44 -0
  67. julee/domain/repositories/policy.py +49 -0
  68. julee/domain/use_cases/__init__.py +17 -0
  69. julee/domain/use_cases/decorators.py +107 -0
  70. julee/domain/use_cases/extract_assemble_data.py +649 -0
  71. julee/domain/use_cases/initialize_system_data.py +842 -0
  72. julee/domain/use_cases/tests/__init__.py +7 -0
  73. julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
  74. julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
  75. julee/domain/use_cases/tests/test_validate_document.py +1228 -0
  76. julee/domain/use_cases/validate_document.py +736 -0
  77. julee/fixtures/assembly_specifications.yaml +70 -0
  78. julee/fixtures/documents.yaml +178 -0
  79. julee/fixtures/knowledge_service_configs.yaml +37 -0
  80. julee/fixtures/knowledge_service_queries.yaml +27 -0
  81. julee/repositories/__init__.py +17 -0
  82. julee/repositories/memory/__init__.py +31 -0
  83. julee/repositories/memory/assembly.py +84 -0
  84. julee/repositories/memory/assembly_specification.py +125 -0
  85. julee/repositories/memory/base.py +227 -0
  86. julee/repositories/memory/document.py +149 -0
  87. julee/repositories/memory/document_policy_validation.py +104 -0
  88. julee/repositories/memory/knowledge_service_config.py +123 -0
  89. julee/repositories/memory/knowledge_service_query.py +120 -0
  90. julee/repositories/memory/policy.py +87 -0
  91. julee/repositories/memory/tests/__init__.py +0 -0
  92. julee/repositories/memory/tests/test_document.py +212 -0
  93. julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
  94. julee/repositories/memory/tests/test_policy.py +443 -0
  95. julee/repositories/minio/__init__.py +31 -0
  96. julee/repositories/minio/assembly.py +103 -0
  97. julee/repositories/minio/assembly_specification.py +170 -0
  98. julee/repositories/minio/client.py +570 -0
  99. julee/repositories/minio/document.py +530 -0
  100. julee/repositories/minio/document_policy_validation.py +120 -0
  101. julee/repositories/minio/knowledge_service_config.py +187 -0
  102. julee/repositories/minio/knowledge_service_query.py +211 -0
  103. julee/repositories/minio/policy.py +106 -0
  104. julee/repositories/minio/tests/__init__.py +0 -0
  105. julee/repositories/minio/tests/fake_client.py +213 -0
  106. julee/repositories/minio/tests/test_assembly.py +374 -0
  107. julee/repositories/minio/tests/test_assembly_specification.py +391 -0
  108. julee/repositories/minio/tests/test_client_protocol.py +57 -0
  109. julee/repositories/minio/tests/test_document.py +591 -0
  110. julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
  111. julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
  112. julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
  113. julee/repositories/minio/tests/test_policy.py +559 -0
  114. julee/repositories/temporal/__init__.py +38 -0
  115. julee/repositories/temporal/activities.py +114 -0
  116. julee/repositories/temporal/activity_names.py +34 -0
  117. julee/repositories/temporal/proxies.py +159 -0
  118. julee/services/__init__.py +18 -0
  119. julee/services/knowledge_service/__init__.py +48 -0
  120. julee/services/knowledge_service/anthropic/__init__.py +12 -0
  121. julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
  122. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
  123. julee/services/knowledge_service/factory.py +138 -0
  124. julee/services/knowledge_service/knowledge_service.py +160 -0
  125. julee/services/knowledge_service/memory/__init__.py +13 -0
  126. julee/services/knowledge_service/memory/knowledge_service.py +278 -0
  127. julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
  128. julee/services/knowledge_service/test_factory.py +112 -0
  129. julee/services/temporal/__init__.py +38 -0
  130. julee/services/temporal/activities.py +86 -0
  131. julee/services/temporal/activity_names.py +22 -0
  132. julee/services/temporal/proxies.py +41 -0
  133. julee/util/__init__.py +0 -0
  134. julee/util/domain.py +119 -0
  135. julee/util/repos/__init__.py +0 -0
  136. julee/util/repos/minio/__init__.py +0 -0
  137. julee/util/repos/minio/file_storage.py +213 -0
  138. julee/util/repos/temporal/__init__.py +11 -0
  139. julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
  140. julee/util/repos/temporal/data_converter.py +123 -0
  141. julee/util/repos/temporal/minio_file_storage.py +12 -0
  142. julee/util/repos/temporal/proxies/__init__.py +0 -0
  143. julee/util/repos/temporal/proxies/file_storage.py +58 -0
  144. julee/util/repositories.py +55 -0
  145. julee/util/temporal/__init__.py +22 -0
  146. julee/util/temporal/activities.py +123 -0
  147. julee/util/temporal/decorators.py +473 -0
  148. julee/util/tests/__init__.py +1 -0
  149. julee/util/tests/test_decorators.py +770 -0
  150. julee/util/validation/__init__.py +29 -0
  151. julee/util/validation/repository.py +100 -0
  152. julee/util/validation/type_guards.py +369 -0
  153. julee/worker.py +211 -0
  154. julee/workflows/__init__.py +26 -0
  155. julee/workflows/extract_assemble.py +215 -0
  156. julee/workflows/validate_document.py +228 -0
  157. julee-0.1.0.dist-info/METADATA +195 -0
  158. julee-0.1.0.dist-info/RECORD +161 -0
  159. julee-0.1.0.dist-info/WHEEL +5 -0
  160. julee-0.1.0.dist-info/licenses/LICENSE +674 -0
  161. julee-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,123 @@
1
+ """
2
+ KnowledgeServiceQuery domain models for the Capture, Extract, Assemble,
3
+ Publish
4
+ workflow.
5
+
6
+ This module contains the KnowledgeServiceQuery domain object that represents
7
+ specific queries to knowledge services for data extraction in the CEAP
8
+ workflow system.
9
+
10
+ A KnowledgeServiceQuery defines a specific extraction operation that can be
11
+ performed against a knowledge service to extract data for a particular part
12
+ of an AssemblySpecification's JSON schema.
13
+
14
+ All domain models use Pydantic BaseModel for validation, serialization,
15
+ and type safety, following the patterns established in the sample project.
16
+ """
17
+
18
+ from pydantic import BaseModel, Field, field_validator
19
+ from typing import Optional, Dict, Any
20
+ from datetime import datetime, timezone
21
+
22
+
23
+ class KnowledgeServiceQuery(BaseModel):
24
+ """Knowledge service query configuration for extracting specific data.
25
+
26
+ A KnowledgeServiceQuery represents a specific extraction operation that
27
+ can be performed against a knowledge service. It defines which knowledge
28
+ service to use and what prompt to send for data extraction.
29
+
30
+ When executed, the relevant section of the AssemblySpecification's JSON
31
+ schema will be
32
+ passed along with the prompt to ensure the knowledge service response
33
+ conforms to the expected structure and validation requirements.
34
+
35
+ The mapping between queries and schema sections is handled by the
36
+ AssemblySpecification's knowledge_service_queries field.
37
+
38
+ Examples of query_metadata usage:
39
+
40
+ For Anthropic services:
41
+ query_metadata = {
42
+ "model": "claude-sonnet-4-20250514",
43
+ "max_tokens": 4000,
44
+ "temperature": 0.1
45
+ }
46
+
47
+ For OpenAI services:
48
+ query_metadata = {
49
+ "model": "gpt-4",
50
+ "temperature": 0.2,
51
+ "top_p": 0.9
52
+ }
53
+
54
+ For custom services:
55
+ query_metadata = {
56
+ "endpoint": "custom-model-v2",
57
+ "timeout": 30,
58
+ "retries": 3
59
+ }
60
+ """
61
+
62
+ # Core query identification
63
+ query_id: str = Field(description="Unique identifier for this query")
64
+ name: str = Field(description="Human-readable name describing the query purpose")
65
+
66
+ # Knowledge service configuration
67
+ knowledge_service_id: str = Field(
68
+ description="Identifier of the knowledge service to query"
69
+ )
70
+ prompt: str = Field(
71
+ description="The specific prompt to send to the knowledge service "
72
+ "for this extraction"
73
+ )
74
+
75
+ # Service-specific configuration
76
+ query_metadata: Optional[Dict[str, Any]] = Field(
77
+ default_factory=dict,
78
+ description="Service-specific metadata and configuration options "
79
+ "such as model selection, temperature, max_tokens, etc. "
80
+ "The structure depends on the specific knowledge service being used.",
81
+ )
82
+ assistant_prompt: Optional[str] = Field(
83
+ default=None,
84
+ description="Optional assistant message content to constrain "
85
+ "or prime the model's response. This is added as the final "
86
+ "assistant message before the model generates its response, "
87
+ "allowing control over response format and structure.",
88
+ )
89
+
90
+ created_at: Optional[datetime] = Field(
91
+ default_factory=lambda: datetime.now(timezone.utc)
92
+ )
93
+ updated_at: Optional[datetime] = Field(
94
+ default_factory=lambda: datetime.now(timezone.utc)
95
+ )
96
+
97
+ @field_validator("query_id")
98
+ @classmethod
99
+ def query_id_must_not_be_empty(cls, v: str) -> str:
100
+ if not v or not v.strip():
101
+ raise ValueError("Query ID cannot be empty")
102
+ return v.strip()
103
+
104
+ @field_validator("name")
105
+ @classmethod
106
+ def name_must_not_be_empty(cls, v: str) -> str:
107
+ if not v or not v.strip():
108
+ raise ValueError("Query name cannot be empty")
109
+ return v.strip()
110
+
111
+ @field_validator("knowledge_service_id")
112
+ @classmethod
113
+ def knowledge_service_id_must_not_be_empty(cls, v: str) -> str:
114
+ if not v or not v.strip():
115
+ raise ValueError("Knowledge service ID cannot be empty")
116
+ return v.strip()
117
+
118
+ @field_validator("prompt")
119
+ @classmethod
120
+ def prompt_must_not_be_empty(cls, v: str) -> str:
121
+ if not v or not v.strip():
122
+ raise ValueError("Query prompt cannot be empty")
123
+ return v.strip()
@@ -0,0 +1,78 @@
1
+ """
2
+ Test factories for AssemblySpecification domain objects using factory_boy.
3
+
4
+ This module provides factory_boy factories for creating test instances of
5
+ AssemblySpecification domain objects with sensible defaults.
6
+ """
7
+
8
+ from datetime import datetime, timezone
9
+ from typing import Any
10
+ from factory.base import Factory
11
+ from factory.faker import Faker
12
+ from factory.declarations import LazyAttribute, LazyFunction
13
+
14
+ from julee.domain.models.assembly_specification import (
15
+ AssemblySpecification,
16
+ AssemblySpecificationStatus,
17
+ KnowledgeServiceQuery,
18
+ )
19
+
20
+
21
+ class AssemblyFactory(Factory):
22
+ """Factory for creating AssemblySpecification instances with sensible
23
+ test defaults."""
24
+
25
+ class Meta:
26
+ model = AssemblySpecification
27
+
28
+ # Core assembly identification
29
+ assembly_specification_id = Faker("uuid4")
30
+ name = "Test Assembly"
31
+ applicability = "Test documents for automated testing purposes"
32
+
33
+ # Valid JSON Schema for testing
34
+ @LazyAttribute
35
+ def jsonschema(self) -> dict[str, Any]:
36
+ return {
37
+ "type": "object",
38
+ "properties": {
39
+ "title": {"type": "string"},
40
+ "content": {"type": "string"},
41
+ "metadata": {
42
+ "type": "object",
43
+ "properties": {
44
+ "author": {"type": "string"},
45
+ "created_date": {"type": "string", "format": "date"},
46
+ },
47
+ },
48
+ },
49
+ "required": ["title"],
50
+ }
51
+
52
+ # Assembly configuration
53
+ status = AssemblySpecificationStatus.ACTIVE
54
+ version = "0.1.0"
55
+
56
+ # Timestamps
57
+ created_at = LazyFunction(lambda: datetime.now(timezone.utc))
58
+ updated_at = LazyFunction(lambda: datetime.now(timezone.utc))
59
+
60
+
61
+ class KnowledgeServiceQueryFactory(Factory):
62
+ """Factory for creating KnowledgeServiceQuery instances with sensible
63
+ test defaults."""
64
+
65
+ class Meta:
66
+ model = KnowledgeServiceQuery
67
+
68
+ # Core query identification
69
+ query_id = Faker("uuid4")
70
+ name = "Test Knowledge Service Query"
71
+
72
+ # Knowledge service configuration
73
+ knowledge_service_id = "test-knowledge-service"
74
+ prompt = "Extract test data from the document"
75
+
76
+ # Timestamps
77
+ created_at = LazyFunction(lambda: datetime.now(timezone.utc))
78
+ updated_at = LazyFunction(lambda: datetime.now(timezone.utc))
@@ -0,0 +1,490 @@
1
+ """
2
+ Comprehensive tests for AssemblySpecification domain model.
3
+
4
+ This test module documents the design decisions made for the
5
+ AssemblySpecification domain model using table-based tests. It covers:
6
+
7
+ - AssemblySpecification instantiation with various field combinations
8
+ - JSON Schema validation rules and error conditions
9
+ - JSON serialization behavior
10
+ - Field validation for required fields
11
+
12
+ Design decisions documented:
13
+ - AssemblySpecifications must have all required fields (id, name,
14
+ applicability, prompt, jsonschema)
15
+ - JSON Schema field must be a valid JSON Schema dictionary
16
+ - All text fields must be non-empty and non-whitespace
17
+ - Version field has a default but can be customized
18
+ - Status defaults to ACTIVE
19
+ """
20
+
21
+ import pytest
22
+ import json
23
+ from typing import Dict, Any
24
+
25
+ from julee.domain.models.assembly_specification import (
26
+ AssemblySpecification,
27
+ AssemblySpecificationStatus,
28
+ )
29
+ from .factories import AssemblyFactory
30
+
31
+
32
+ class TestAssemblyInstantiation:
33
+ """Test AssemblySpecification creation with various field combinations."""
34
+
35
+ @pytest.mark.parametrize(
36
+ "assembly_specification_id,name,applicability,jsonschema,expected_success",
37
+ [
38
+ # Valid cases
39
+ (
40
+ "assembly-specification-1",
41
+ "Meeting Minutes",
42
+ "Corporate meeting recordings and transcripts",
43
+ {
44
+ "type": "object",
45
+ "properties": {"title": {"type": "string"}},
46
+ },
47
+ True,
48
+ ),
49
+ (
50
+ "assembly-specification-2",
51
+ "Project Report",
52
+ "Technical project documentation and status reports",
53
+ {
54
+ "type": "object",
55
+ "properties": {
56
+ "project_name": {"type": "string"},
57
+ "status": {
58
+ "type": "string",
59
+ "enum": ["active", "completed"],
60
+ },
61
+ "milestones": {
62
+ "type": "array",
63
+ "items": {"type": "string"},
64
+ },
65
+ },
66
+ "required": ["project_name"],
67
+ },
68
+ True,
69
+ ),
70
+ # Invalid cases - empty required fields
71
+ (
72
+ "",
73
+ "Test Assembly",
74
+ "Test applicability",
75
+ {
76
+ "type": "object",
77
+ "properties": {"test": {"type": "string"}},
78
+ },
79
+ False,
80
+ ), # Empty assembly_specification_id
81
+ (
82
+ "assembly-specification-3",
83
+ "",
84
+ "Test applicability",
85
+ {
86
+ "type": "object",
87
+ "properties": {"test": {"type": "string"}},
88
+ },
89
+ False,
90
+ ), # Empty name
91
+ (
92
+ "assembly-specification-4",
93
+ "Test Assembly",
94
+ "",
95
+ {
96
+ "type": "object",
97
+ "properties": {"test": {"type": "string"}},
98
+ },
99
+ False,
100
+ ), # Empty applicability
101
+ # Invalid cases - whitespace only
102
+ (
103
+ " ",
104
+ "Test Assembly",
105
+ "Test applicability",
106
+ {
107
+ "type": "object",
108
+ "properties": {"test": {"type": "string"}},
109
+ },
110
+ False,
111
+ ), # Whitespace assembly_specification_id
112
+ (
113
+ "assembly-specification-6",
114
+ " ",
115
+ "Test applicability",
116
+ {
117
+ "type": "object",
118
+ "properties": {"test": {"type": "string"}},
119
+ },
120
+ False,
121
+ ), # Whitespace name
122
+ (
123
+ "assembly-specification-7",
124
+ "Test Assembly",
125
+ " ",
126
+ {
127
+ "type": "object",
128
+ "properties": {"test": {"type": "string"}},
129
+ },
130
+ False,
131
+ ), # Whitespace applicability
132
+ ],
133
+ )
134
+ def test_assembly_creation_validation(
135
+ self,
136
+ assembly_specification_id: str,
137
+ name: str,
138
+ applicability: str,
139
+ jsonschema: Dict[str, Any],
140
+ expected_success: bool,
141
+ ) -> None:
142
+ """Test assembly creation with various field validation scenarios."""
143
+ if expected_success:
144
+ # Should create successfully
145
+ assembly = AssemblySpecification(
146
+ assembly_specification_id=assembly_specification_id,
147
+ name=name,
148
+ applicability=applicability,
149
+ jsonschema=jsonschema,
150
+ )
151
+ assert (
152
+ assembly.assembly_specification_id == assembly_specification_id.strip()
153
+ )
154
+ assert assembly.name == name.strip()
155
+ assert assembly.applicability == applicability.strip()
156
+ assert assembly.jsonschema == jsonschema
157
+ assert assembly.status == AssemblySpecificationStatus.ACTIVE # Default
158
+ assert assembly.version == "0.1.0" # Default
159
+ else:
160
+ # Should raise validation error
161
+ with pytest.raises(Exception): # Could be ValueError or ValidationError
162
+ AssemblySpecification(
163
+ assembly_specification_id=assembly_specification_id,
164
+ name=name,
165
+ applicability=applicability,
166
+ jsonschema=jsonschema,
167
+ )
168
+
169
+
170
+ class TestAssemblyKnowledgeServiceQueriesValidation:
171
+ """Test knowledge_service_queries field validation."""
172
+
173
+ @pytest.mark.parametrize(
174
+ "knowledge_service_queries,expected_success",
175
+ [
176
+ # Valid cases - empty dict
177
+ ({}, True),
178
+ # Valid cases - valid JSON pointers that exist in schema
179
+ ({"/properties/test": "query-1"}, True),
180
+ ({"": "root-query"}, True), # Empty string for root
181
+ ({"/properties/test": "query-1", "": "root-query"}, True),
182
+ # Invalid cases - malformed pointers
183
+ ({"invalid-pointer": "query-1"}, False),
184
+ ({"test": "query-1"}, False), # Missing /properties/
185
+ # Invalid cases - pointers that don't exist in schema
186
+ ({"/properties/nonexistent": "query-1"}, False),
187
+ # Invalid cases - wrong types
188
+ ("not-a-dict", False),
189
+ (["/properties/test", "query-1"], False),
190
+ # Invalid cases - invalid query IDs
191
+ ({"/properties/test": ""}, False), # Empty query ID
192
+ ({"/properties/test": 123}, False), # Non-string query ID
193
+ ],
194
+ )
195
+ def test_knowledge_service_queries_validation(
196
+ self,
197
+ knowledge_service_queries: Any,
198
+ expected_success: bool,
199
+ ) -> None:
200
+ """Test knowledge_service_queries field validation."""
201
+ if expected_success:
202
+ # Should create successfully
203
+ assembly = AssemblySpecification(
204
+ assembly_specification_id="test-id",
205
+ name="Test Assembly",
206
+ applicability="Test applicability",
207
+ jsonschema={
208
+ "type": "object",
209
+ "properties": {"test": {"type": "string"}},
210
+ },
211
+ knowledge_service_queries=knowledge_service_queries,
212
+ )
213
+ assert assembly.knowledge_service_queries == knowledge_service_queries
214
+ else:
215
+ # Should raise validation error
216
+ with pytest.raises(Exception):
217
+ AssemblySpecification(
218
+ assembly_specification_id="test-id",
219
+ name="Test Assembly",
220
+ applicability="Test applicability",
221
+ jsonschema={
222
+ "type": "object",
223
+ "properties": {"test": {"type": "string"}},
224
+ },
225
+ knowledge_service_queries=knowledge_service_queries,
226
+ )
227
+
228
+
229
+ class TestAssemblyJsonSchemaValidation:
230
+ """Test JSON Schema field validation."""
231
+
232
+ @pytest.mark.parametrize(
233
+ "jsonschema,error_message_contains",
234
+ [
235
+ # Valid JSON Schemas
236
+ (
237
+ {
238
+ "type": "object",
239
+ "properties": {"name": {"type": "string"}},
240
+ },
241
+ None,
242
+ ),
243
+ (
244
+ {"type": "array", "items": {"type": "string"}, "minItems": 1},
245
+ None,
246
+ ),
247
+ (
248
+ {"type": "string", "pattern": "^[A-Z]+$"},
249
+ None,
250
+ ),
251
+ (
252
+ {
253
+ "type": "object",
254
+ "properties": {
255
+ "user": {
256
+ "type": "object",
257
+ "properties": {
258
+ "name": {"type": "string"},
259
+ "age": {"type": "integer", "minimum": 0},
260
+ },
261
+ "required": ["name"],
262
+ }
263
+ },
264
+ "required": ["user"],
265
+ },
266
+ None,
267
+ ),
268
+ # Invalid cases - not a dict
269
+ ("not a dict", "Input should be a valid dictionary"),
270
+ (123, "Input should be a valid dictionary"),
271
+ ([], "Input should be a valid dictionary"),
272
+ # Invalid cases - missing required fields
273
+ (
274
+ {"properties": {"name": {"type": "string"}}},
275
+ "JSON Schema must have a 'type' field",
276
+ ),
277
+ ({}, "JSON Schema must have a 'type' field"),
278
+ # Invalid JSON Schema structure
279
+ (
280
+ {"type": "invalid_type"},
281
+ "Invalid JSON Schema",
282
+ ),
283
+ (
284
+ {
285
+ "type": "object",
286
+ "properties": {"invalid_prop": {"type": "invalid_type"}},
287
+ },
288
+ "Invalid JSON Schema",
289
+ ),
290
+ (
291
+ {
292
+ "type": "object",
293
+ "additionalProperties": "invalid",
294
+ }, # additionalProperties must be boolean or object
295
+ "Invalid JSON Schema",
296
+ ),
297
+ ],
298
+ )
299
+ def test_jsonschema_validation(
300
+ self,
301
+ jsonschema: Any,
302
+ error_message_contains: str | None,
303
+ ) -> None:
304
+ """Test JSON Schema field validation with various schemas."""
305
+ if error_message_contains is None:
306
+ # Should create successfully
307
+ assembly = AssemblySpecification(
308
+ assembly_specification_id="test-id",
309
+ name="Test Assembly",
310
+ applicability="Test applicability",
311
+ jsonschema=jsonschema,
312
+ )
313
+ assert assembly.jsonschema == jsonschema
314
+ else:
315
+ # Should raise validation error
316
+ with pytest.raises(Exception) as exc_info:
317
+ AssemblySpecification(
318
+ assembly_specification_id="test-id",
319
+ name="Test Assembly",
320
+ applicability="Test applicability",
321
+ jsonschema=jsonschema,
322
+ )
323
+
324
+ assert error_message_contains in str(exc_info.value)
325
+
326
+
327
+ class TestAssemblySerialization:
328
+ """Test AssemblySpecification JSON serialization behavior."""
329
+
330
+ def test_assembly_json_serialization(self) -> None:
331
+ """Test that AssemblySpecification serializes to JSON correctly."""
332
+ complex_schema = {
333
+ "type": "object",
334
+ "properties": {
335
+ "meeting_info": {
336
+ "type": "object",
337
+ "properties": {
338
+ "title": {"type": "string"},
339
+ "date": {"type": "string", "format": "date"},
340
+ "participants": {
341
+ "type": "array",
342
+ "items": {"type": "string"},
343
+ },
344
+ },
345
+ "required": ["title", "date"],
346
+ },
347
+ "action_items": {
348
+ "type": "array",
349
+ "items": {
350
+ "type": "object",
351
+ "properties": {
352
+ "description": {"type": "string"},
353
+ "assignee": {"type": "string"},
354
+ "due_date": {"type": "string", "format": "date"},
355
+ },
356
+ "required": ["description"],
357
+ },
358
+ },
359
+ },
360
+ "required": ["meeting_info"],
361
+ }
362
+
363
+ assembly = AssemblyFactory.build(
364
+ assembly_specification_id="meeting-minutes-v1",
365
+ name="Meeting Minutes",
366
+ applicability="Corporate meeting recordings",
367
+ jsonschema=complex_schema,
368
+ )
369
+
370
+ json_str = assembly.model_dump_json()
371
+ json_data = json.loads(json_str)
372
+
373
+ # All fields should be present in JSON
374
+ assert (
375
+ json_data["assembly_specification_id"] == assembly.assembly_specification_id
376
+ )
377
+ assert json_data["name"] == assembly.name
378
+ assert json_data["applicability"] == assembly.applicability
379
+ assert json_data["status"] == assembly.status.value
380
+ assert json_data["version"] == assembly.version
381
+
382
+ # JSON Schema should be preserved as structured data
383
+ assert json_data["jsonschema"] == complex_schema
384
+ assert json_data["jsonschema"]["type"] == "object"
385
+ assert "meeting_info" in json_data["jsonschema"]["properties"]
386
+ assert "action_items" in json_data["jsonschema"]["properties"]
387
+
388
+ def test_assembly_json_roundtrip(self) -> None:
389
+ """Test that AssemblySpecification can be serialized to JSON and
390
+ deserialized back."""
391
+ original_assembly = AssemblyFactory.build()
392
+
393
+ # Serialize to JSON
394
+ json_str = original_assembly.model_dump_json()
395
+ json_data = json.loads(json_str)
396
+
397
+ # Deserialize back to AssemblySpecification
398
+ reconstructed_assembly = AssemblySpecification(**json_data)
399
+
400
+ # Should be equivalent
401
+ assert (
402
+ reconstructed_assembly.assembly_specification_id
403
+ == original_assembly.assembly_specification_id
404
+ )
405
+ assert reconstructed_assembly.name == original_assembly.name
406
+ assert reconstructed_assembly.applicability == original_assembly.applicability
407
+ assert reconstructed_assembly.jsonschema == original_assembly.jsonschema
408
+ assert reconstructed_assembly.status == original_assembly.status
409
+ assert reconstructed_assembly.version == original_assembly.version
410
+
411
+
412
+ class TestAssemblyDefaults:
413
+ """Test AssemblySpecification default values and behavior."""
414
+
415
+ def test_assembly_default_values(self) -> None:
416
+ """Test that AssemblySpecification has correct default values."""
417
+ minimal_assembly = AssemblySpecification(
418
+ assembly_specification_id="test-id",
419
+ name="Test Assembly",
420
+ applicability="Test applicability",
421
+ jsonschema={
422
+ "type": "object",
423
+ "properties": {"test": {"type": "string"}},
424
+ },
425
+ )
426
+
427
+ assert minimal_assembly.status == AssemblySpecificationStatus.ACTIVE
428
+ assert minimal_assembly.version == "0.1.0"
429
+ assert minimal_assembly.created_at is not None
430
+ assert minimal_assembly.updated_at is not None
431
+
432
+ def test_assembly_custom_values(self) -> None:
433
+ """Test AssemblySpecification with custom non-default values."""
434
+ custom_assembly = AssemblySpecification(
435
+ assembly_specification_id="custom-id",
436
+ name="Custom Assembly",
437
+ applicability="Custom applicability",
438
+ jsonschema={
439
+ "type": "object",
440
+ "properties": {"custom": {"type": "string"}},
441
+ },
442
+ status=AssemblySpecificationStatus.DRAFT,
443
+ version="2.0.0",
444
+ knowledge_service_queries={"/properties/custom": "custom-query-1"},
445
+ )
446
+
447
+ assert custom_assembly.status == AssemblySpecificationStatus.DRAFT
448
+ assert custom_assembly.version == "2.0.0"
449
+ assert custom_assembly.knowledge_service_queries == {
450
+ "/properties/custom": "custom-query-1"
451
+ }
452
+
453
+ @pytest.mark.parametrize(
454
+ "status",
455
+ [
456
+ AssemblySpecificationStatus.ACTIVE,
457
+ AssemblySpecificationStatus.INACTIVE,
458
+ AssemblySpecificationStatus.DRAFT,
459
+ AssemblySpecificationStatus.DEPRECATED,
460
+ ],
461
+ )
462
+ def test_assembly_status_values(self, status: AssemblySpecificationStatus) -> None:
463
+ """Test AssemblySpecification with different status values."""
464
+ assembly = AssemblyFactory.build(status=status)
465
+ assert assembly.status == status
466
+
467
+
468
+ class TestAssemblyVersionValidation:
469
+ """Test AssemblySpecification version field validation."""
470
+
471
+ @pytest.mark.parametrize(
472
+ "version,expected_success",
473
+ [
474
+ ("1.0.0", True),
475
+ ("0.1.0", True),
476
+ ("2.5.1-beta", True),
477
+ ("v1.0", True),
478
+ ("", False), # Empty version
479
+ (" ", False), # Whitespace only
480
+ ],
481
+ )
482
+ def test_version_validation(self, version: str, expected_success: bool) -> None:
483
+ """Test version field validation - we can add semver checks later, not
484
+ needed yet (if at all)."""
485
+ if expected_success:
486
+ assembly = AssemblyFactory.build(version=version)
487
+ assert assembly.version == version.strip()
488
+ else:
489
+ with pytest.raises(Exception):
490
+ AssemblyFactory.build(version=version)