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,203 @@
1
+ """
2
+ Policy domain models for the Capture, Extract, Assemble,
3
+ Publish workflow.
4
+
5
+ This module contains the Policy domain object that represents
6
+ policy configurations in the CEAP workflow system.
7
+
8
+ A Policy defines validation criteria and optional transformations
9
+ for documents. It includes validation scores that must be met and optional
10
+ transformation queries that can be applied to improve document quality.
11
+
12
+ All domain models use Pydantic BaseModel for validation, serialization,
13
+ and type safety, following the patterns established in the sample project.
14
+ """
15
+
16
+ from pydantic import BaseModel, Field, field_validator
17
+ from typing import Optional, List, Tuple
18
+ from datetime import datetime, timezone
19
+ from enum import Enum
20
+
21
+
22
+ class PolicyStatus(str, Enum):
23
+ """Status of a policy configuration."""
24
+
25
+ ACTIVE = "active"
26
+ INACTIVE = "inactive"
27
+ DRAFT = "draft"
28
+ DEPRECATED = "deprecated"
29
+
30
+
31
+ class Policy(BaseModel):
32
+ """Policy configuration that defines validation and
33
+ transformation criteria for documents.
34
+
35
+ A Policy represents a set of quality criteria that documents
36
+ must meet. It includes validation scores that are calculated using
37
+ knowledge service queries, and optional transformation queries that can
38
+ be applied to improve document quality before re-validation.
39
+
40
+ The policy operates in two modes:
41
+ 1. Validation-only: Calculates scores and passes/fails based on criteria
42
+ 2. Validation with transformation: Calculates scores, applies
43
+ transformations, then re-calculates scores for final pass/fail
44
+ """
45
+
46
+ # Core policy identification
47
+ policy_id: str = Field(description="Unique identifier for this policy")
48
+ title: str = Field(description="Human-readable title for the policy")
49
+ description: str = Field(
50
+ description="Detailed description of what this policy validates "
51
+ "and optionally transforms"
52
+ )
53
+
54
+ # Policy configuration
55
+ status: PolicyStatus = PolicyStatus.ACTIVE
56
+ validation_scores: List[Tuple[str, int]] = Field(
57
+ description="List of (knowledge_service_query_id, required_score) "
58
+ "tuples where required_score is between 0 and 100. All scores "
59
+ "must be met or exceeded for the policy to pass"
60
+ )
61
+ transformation_queries: Optional[List[str]] = Field(
62
+ default=None,
63
+ description="Optional list of knowledge service query IDs for "
64
+ "transformations to apply before re-validation. If not provided "
65
+ "or empty, policy operates in validation-only mode",
66
+ )
67
+
68
+ # Policy metadata
69
+ version: str = Field(default="0.1.0", description="Policy version")
70
+ created_at: Optional[datetime] = Field(
71
+ default_factory=lambda: datetime.now(timezone.utc)
72
+ )
73
+ updated_at: Optional[datetime] = Field(default=None)
74
+
75
+ @field_validator("policy_id")
76
+ @classmethod
77
+ def policy_id_must_not_be_empty(cls, v: str) -> str:
78
+ if not v or not v.strip():
79
+ raise ValueError("Policy ID cannot be empty")
80
+ return v.strip()
81
+
82
+ @field_validator("title")
83
+ @classmethod
84
+ def title_must_not_be_empty(cls, v: str) -> str:
85
+ if not v or not v.strip():
86
+ raise ValueError("Policy title cannot be empty")
87
+ return v.strip()
88
+
89
+ @field_validator("description")
90
+ @classmethod
91
+ def description_must_not_be_empty(cls, v: str) -> str:
92
+ if not v or not v.strip():
93
+ raise ValueError("Policy description cannot be empty")
94
+ return v.strip()
95
+
96
+ @field_validator("validation_scores")
97
+ @classmethod
98
+ def validation_scores_must_be_valid(
99
+ cls, v: List[Tuple[str, int]]
100
+ ) -> List[Tuple[str, int]]:
101
+ if not isinstance(v, list):
102
+ raise ValueError("Validation scores must be a list")
103
+
104
+ if not v:
105
+ raise ValueError("Validation scores list cannot be empty")
106
+
107
+ validated_scores = []
108
+ query_ids_seen = set()
109
+
110
+ for item in v:
111
+ if not isinstance(item, tuple) or len(item) != 2:
112
+ raise ValueError(
113
+ "Each validation score must be a 2-tuple of "
114
+ "(query_id, required_score)"
115
+ )
116
+
117
+ query_id, required_score = item
118
+
119
+ # Validate query ID
120
+ if not isinstance(query_id, str) or not query_id.strip():
121
+ raise ValueError(
122
+ "Query ID in validation scores must be a non-empty string"
123
+ )
124
+ query_id = query_id.strip()
125
+
126
+ # Check for duplicate query IDs
127
+ if query_id in query_ids_seen:
128
+ raise ValueError(
129
+ f"Duplicate query ID '{query_id}' in validation scores"
130
+ )
131
+ query_ids_seen.add(query_id)
132
+
133
+ # Validate required score
134
+ if not isinstance(required_score, int):
135
+ raise ValueError("Required score must be an integer between 0 and 100")
136
+ if required_score < 0 or required_score > 100:
137
+ raise ValueError(
138
+ f"Required score {required_score} must be between " f"0 and 100"
139
+ )
140
+
141
+ validated_scores.append((query_id, required_score))
142
+
143
+ return validated_scores
144
+
145
+ @field_validator("transformation_queries")
146
+ @classmethod
147
+ def transformation_queries_must_be_valid(
148
+ cls, v: Optional[List[str]]
149
+ ) -> Optional[List[str]]:
150
+ if v is None:
151
+ return v
152
+
153
+ if not isinstance(v, list):
154
+ raise ValueError("Transformation queries must be a list or None")
155
+
156
+ # Empty list is valid - means no transformations
157
+ if not v:
158
+ return v
159
+
160
+ validated_queries = []
161
+ query_ids_seen = set()
162
+
163
+ for query_id in v:
164
+ if not isinstance(query_id, str) or not query_id.strip():
165
+ raise ValueError(
166
+ "Each transformation query ID must be a non-empty string"
167
+ )
168
+ query_id = query_id.strip()
169
+
170
+ # Check for duplicate query IDs
171
+ if query_id in query_ids_seen:
172
+ raise ValueError(
173
+ f"Duplicate query ID '{query_id}' in transformation " f"queries"
174
+ )
175
+ query_ids_seen.add(query_id)
176
+
177
+ validated_queries.append(query_id)
178
+
179
+ return validated_queries
180
+
181
+ @field_validator("version")
182
+ @classmethod
183
+ def version_must_not_be_empty(cls, v: str) -> str:
184
+ if not v or not v.strip():
185
+ raise ValueError("Policy version cannot be empty")
186
+ return v.strip()
187
+
188
+ @property
189
+ def is_validation_only(self) -> bool:
190
+ """Check if this policy operates in validation-only mode.
191
+
192
+ Returns True if no transformation queries are defined or if the
193
+ transformation queries list is empty.
194
+ """
195
+ return not self.transformation_queries
196
+
197
+ @property
198
+ def has_transformations(self) -> bool:
199
+ """Check if this policy includes transformation queries.
200
+
201
+ Returns True if transformation queries are defined and non-empty.
202
+ """
203
+ return not self.is_validation_only
File without changes
@@ -0,0 +1,47 @@
1
+ """
2
+ Test factories for Policy domain objects using factory_boy.
3
+
4
+ This module provides factory_boy factories for creating test instances of
5
+ Policy 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.policy import (
14
+ DocumentPolicyValidation,
15
+ DocumentPolicyValidationStatus,
16
+ )
17
+
18
+
19
+ class DocumentPolicyValidationFactory(Factory):
20
+ """Factory for creating DocumentPolicyValidation instances with sensible
21
+ test defaults."""
22
+
23
+ class Meta:
24
+ model = DocumentPolicyValidation
25
+
26
+ # Core validation identification
27
+ validation_id = Faker("uuid4")
28
+ input_document_id = Faker("uuid4")
29
+ policy_id = Faker("uuid4")
30
+
31
+ # Validation process status
32
+ status = DocumentPolicyValidationStatus.PENDING
33
+
34
+ # Initial validation results (empty by default)
35
+ validation_scores: list[tuple[str, int]] = []
36
+
37
+ # Transformation results (None by default)
38
+ transformed_document_id = None
39
+ post_transform_validation_scores = None
40
+
41
+ # Validation metadata
42
+ started_at = LazyFunction(lambda: datetime.now(timezone.utc))
43
+ completed_at = None
44
+ error_message = None
45
+
46
+ # Results summary
47
+ passed = None
@@ -0,0 +1,420 @@
1
+ """
2
+ Tests for DocumentPolicyValidation domain model.
3
+
4
+ This module tests the DocumentPolicyValidation domain object validation,
5
+ field requirements, and business rules following the same testing patterns
6
+ as the Policy model tests.
7
+
8
+ Tests focus on:
9
+ - Required field validation
10
+ - Field format validation
11
+ - Score tuple validation
12
+ - Status enum validation
13
+ - Edge cases and error conditions
14
+ """
15
+
16
+ import pytest
17
+ from datetime import datetime, timezone
18
+ from pydantic import ValidationError
19
+
20
+ from julee.domain.models.policy import (
21
+ DocumentPolicyValidation,
22
+ DocumentPolicyValidationStatus,
23
+ )
24
+ from .factories import DocumentPolicyValidationFactory
25
+
26
+
27
+ class TestDocumentPolicyValidationValidation:
28
+ """Test validation rules for DocumentPolicyValidation model."""
29
+
30
+ def test_minimal_valid_validation(self) -> None:
31
+ """Test creating a minimal valid DocumentPolicyValidation."""
32
+ validation = DocumentPolicyValidationFactory()
33
+
34
+ assert validation.input_document_id is not None
35
+ assert validation.policy_id is not None
36
+ assert validation.status == DocumentPolicyValidationStatus.PENDING
37
+ assert validation.validation_scores == []
38
+ assert validation.transformed_document_id is None
39
+ assert validation.post_transform_validation_scores is None
40
+ assert validation.validation_id is not None
41
+ assert validation.passed is None
42
+ assert validation.error_message is None
43
+ assert validation.completed_at is None
44
+ assert isinstance(validation.started_at, datetime)
45
+
46
+ def test_full_valid_validation(self) -> None:
47
+ """Test creating a fully populated DocumentPolicyValidation."""
48
+ started_at = datetime.now(timezone.utc)
49
+ completed_at = datetime.now(timezone.utc)
50
+
51
+ validation = DocumentPolicyValidation(
52
+ validation_id="val-789",
53
+ input_document_id="doc-123",
54
+ policy_id="policy-456",
55
+ status=DocumentPolicyValidationStatus.PASSED,
56
+ validation_scores=[("query1", 85), ("query2", 92)],
57
+ transformed_document_id="doc-123-transformed",
58
+ post_transform_validation_scores=[("query1", 95), ("query2", 88)],
59
+ started_at=started_at,
60
+ completed_at=completed_at,
61
+ error_message=None,
62
+ passed=True,
63
+ )
64
+
65
+ assert validation.validation_id == "val-789"
66
+ assert validation.input_document_id == "doc-123"
67
+ assert validation.policy_id == "policy-456"
68
+ assert validation.status == DocumentPolicyValidationStatus.PASSED
69
+ assert validation.validation_scores == [
70
+ ("query1", 85),
71
+ ("query2", 92),
72
+ ]
73
+ assert validation.transformed_document_id == "doc-123-transformed"
74
+ assert validation.post_transform_validation_scores == [
75
+ ("query1", 95),
76
+ ("query2", 88),
77
+ ]
78
+ assert validation.started_at == started_at
79
+ assert validation.completed_at == completed_at
80
+ assert validation.passed is True
81
+
82
+
83
+ class TestDocumentPolicyValidationRequiredFields:
84
+ """Test required field validation."""
85
+
86
+ def test_required_fields_present_in_factory(self) -> None:
87
+ """Test that factory creates validation with required fields."""
88
+ validation = DocumentPolicyValidationFactory()
89
+ assert validation.input_document_id is not None
90
+ assert validation.policy_id is not None
91
+ assert isinstance(validation.input_document_id, str)
92
+ assert isinstance(validation.policy_id, str)
93
+
94
+
95
+ class TestDocumentPolicyValidationFieldValidation:
96
+ """Test individual field validation rules."""
97
+
98
+ def test_input_document_id_cannot_be_empty(self) -> None:
99
+ """Test that input_document_id cannot be empty or whitespace."""
100
+ test_cases = ["", " ", "\t", "\n"]
101
+
102
+ for empty_value in test_cases:
103
+ with pytest.raises(ValidationError) as exc_info:
104
+ DocumentPolicyValidation(
105
+ validation_id="val-123",
106
+ input_document_id=empty_value,
107
+ policy_id="policy-456",
108
+ )
109
+
110
+ errors = exc_info.value.errors()
111
+ assert any(
112
+ "Input document ID cannot be empty" in str(error) for error in errors
113
+ )
114
+
115
+ def test_input_document_id_strips_whitespace(self) -> None:
116
+ """Test that input_document_id strips whitespace."""
117
+ validation = DocumentPolicyValidation(
118
+ validation_id="val-123",
119
+ input_document_id=" doc-123 ",
120
+ policy_id="policy-456",
121
+ )
122
+ assert validation.input_document_id == "doc-123"
123
+
124
+ def test_policy_id_cannot_be_empty(self) -> None:
125
+ """Test that policy_id cannot be empty or whitespace."""
126
+ test_cases = ["", " ", "\t", "\n"]
127
+
128
+ for empty_value in test_cases:
129
+ with pytest.raises(ValidationError) as exc_info:
130
+ DocumentPolicyValidation(
131
+ validation_id="val-123",
132
+ input_document_id="doc-123",
133
+ policy_id=empty_value,
134
+ )
135
+
136
+ errors = exc_info.value.errors()
137
+ assert any("Policy ID cannot be empty" in str(error) for error in errors)
138
+
139
+ def test_policy_id_strips_whitespace(self) -> None:
140
+ """Test that policy_id strips whitespace."""
141
+ validation = DocumentPolicyValidation(
142
+ validation_id="val-123",
143
+ input_document_id="doc-123",
144
+ policy_id=" policy-456 ",
145
+ )
146
+ assert validation.policy_id == "policy-456"
147
+
148
+ def test_transformed_document_id_validation(self) -> None:
149
+ """Test transformed_document_id field validation."""
150
+ # None is valid
151
+ validation = DocumentPolicyValidation(
152
+ validation_id="val-123",
153
+ input_document_id="doc-123",
154
+ policy_id="policy-456",
155
+ transformed_document_id=None,
156
+ )
157
+ assert validation.transformed_document_id is None
158
+
159
+ # Non-empty string is valid
160
+ validation = DocumentPolicyValidation(
161
+ validation_id="val-123",
162
+ input_document_id="doc-123",
163
+ policy_id="policy-456",
164
+ transformed_document_id="doc-123-transformed",
165
+ )
166
+ assert validation.transformed_document_id == "doc-123-transformed"
167
+
168
+ # Empty string should be invalid
169
+ with pytest.raises(ValidationError) as exc_info:
170
+ DocumentPolicyValidation(
171
+ validation_id="val-123",
172
+ input_document_id="doc-123",
173
+ policy_id="policy-456",
174
+ transformed_document_id="",
175
+ )
176
+
177
+ errors = exc_info.value.errors()
178
+ assert any(
179
+ "must be a non-empty string or None" in str(error) for error in errors
180
+ )
181
+
182
+ def test_error_message_validation(self) -> None:
183
+ """Test error_message field validation."""
184
+ # None is valid
185
+ validation = DocumentPolicyValidation(
186
+ validation_id="val-123",
187
+ input_document_id="doc-123",
188
+ policy_id="policy-456",
189
+ error_message=None,
190
+ )
191
+ assert validation.error_message is None
192
+
193
+ # Non-empty string is valid
194
+ validation = DocumentPolicyValidation(
195
+ validation_id="val-123",
196
+ input_document_id="doc-123",
197
+ policy_id="policy-456",
198
+ error_message="Something went wrong",
199
+ )
200
+ assert validation.error_message == "Something went wrong"
201
+
202
+ # Empty/whitespace string becomes None
203
+ validation = DocumentPolicyValidation(
204
+ validation_id="val-123",
205
+ input_document_id="doc-123",
206
+ policy_id="policy-456",
207
+ error_message=" ",
208
+ )
209
+ assert validation.error_message is None
210
+
211
+
212
+ class TestValidationScores:
213
+ """Test validation_scores field validation."""
214
+
215
+ def test_empty_validation_scores_valid(self) -> None:
216
+ """Test that empty validation_scores list is valid."""
217
+ validation = DocumentPolicyValidation(
218
+ validation_id="val-123",
219
+ input_document_id="doc-123",
220
+ policy_id="policy-456",
221
+ validation_scores=[],
222
+ )
223
+ assert validation.validation_scores == []
224
+
225
+ def test_valid_validation_scores(self) -> None:
226
+ """Test valid validation_scores."""
227
+ scores = [("query1", 85), ("query2", 92), ("query3", 78)]
228
+ validation = DocumentPolicyValidation(
229
+ validation_id="val-123",
230
+ input_document_id="doc-123",
231
+ policy_id="policy-456",
232
+ validation_scores=scores,
233
+ )
234
+ assert validation.validation_scores == scores
235
+
236
+ def test_validation_scores_with_valid_integers(self) -> None:
237
+ """Test that validation_scores work with valid integer scores."""
238
+ validation = DocumentPolicyValidation(
239
+ validation_id="val-123",
240
+ input_document_id="doc-123",
241
+ policy_id="policy-456",
242
+ validation_scores=[("query1", 85), ("query2", 92)],
243
+ )
244
+ assert validation.validation_scores == [
245
+ ("query1", 85),
246
+ ("query2", 92),
247
+ ]
248
+
249
+ def test_validation_scores_query_id_validation(self) -> None:
250
+ """Test validation_scores query_id validation."""
251
+ # Empty query_id should fail
252
+ with pytest.raises(ValidationError) as exc_info:
253
+ DocumentPolicyValidation(
254
+ validation_id="val-123",
255
+ input_document_id="doc-123",
256
+ policy_id="policy-456",
257
+ validation_scores=[("", 85)],
258
+ )
259
+
260
+ errors = exc_info.value.errors()
261
+ assert any("must be a non-empty string" in str(error) for error in errors)
262
+
263
+ # Whitespace-only query_id should fail
264
+ with pytest.raises(ValidationError) as exc_info:
265
+ DocumentPolicyValidation(
266
+ validation_id="val-123",
267
+ input_document_id="doc-123",
268
+ policy_id="policy-456",
269
+ validation_scores=[(" ", 85)],
270
+ )
271
+
272
+ errors = exc_info.value.errors()
273
+ assert any("must be a non-empty string" in str(error) for error in errors)
274
+
275
+ def test_validation_scores_score_range(self) -> None:
276
+ """Test validation_scores score range validation."""
277
+ # Score too low
278
+ with pytest.raises(ValidationError) as exc_info:
279
+ DocumentPolicyValidation(
280
+ validation_id="val-123",
281
+ input_document_id="doc-123",
282
+ policy_id="policy-456",
283
+ validation_scores=[("query1", -1)],
284
+ )
285
+
286
+ errors = exc_info.value.errors()
287
+ assert any("must be between 0 and 100" in str(error) for error in errors)
288
+
289
+ # Score too high
290
+ with pytest.raises(ValidationError) as exc_info:
291
+ DocumentPolicyValidation(
292
+ validation_id="val-123",
293
+ input_document_id="doc-123",
294
+ policy_id="policy-456",
295
+ validation_scores=[("query1", 101)],
296
+ )
297
+
298
+ errors = exc_info.value.errors()
299
+ assert any("must be between 0 and 100" in str(error) for error in errors)
300
+
301
+ # Valid edge cases
302
+ validation = DocumentPolicyValidation(
303
+ validation_id="val-123",
304
+ input_document_id="doc-123",
305
+ policy_id="policy-456",
306
+ validation_scores=[("query1", 0), ("query2", 100)],
307
+ )
308
+ assert validation.validation_scores == [
309
+ ("query1", 0),
310
+ ("query2", 100),
311
+ ]
312
+
313
+ def test_validation_scores_no_duplicates(self) -> None:
314
+ """Test that validation_scores cannot have duplicate query_ids."""
315
+ with pytest.raises(ValidationError) as exc_info:
316
+ DocumentPolicyValidation(
317
+ validation_id="val-123",
318
+ input_document_id="doc-123",
319
+ policy_id="policy-456",
320
+ validation_scores=[("query1", 85), ("query1", 92)],
321
+ )
322
+
323
+ errors = exc_info.value.errors()
324
+ assert any("Duplicate query ID 'query1'" in str(error) for error in errors)
325
+
326
+
327
+ class TestPostTransformValidationScores:
328
+ """Test post_transform_validation_scores field validation."""
329
+
330
+ def test_none_post_transform_scores_valid(self) -> None:
331
+ """Test that None post_transform_validation_scores is valid."""
332
+ validation = DocumentPolicyValidation(
333
+ validation_id="val-123",
334
+ input_document_id="doc-123",
335
+ policy_id="policy-456",
336
+ post_transform_validation_scores=None,
337
+ )
338
+ assert validation.post_transform_validation_scores is None
339
+
340
+ def test_empty_post_transform_scores_valid(self) -> None:
341
+ """Test that empty post_transform_validation_scores list is valid."""
342
+ validation = DocumentPolicyValidation(
343
+ validation_id="val-123",
344
+ input_document_id="doc-123",
345
+ policy_id="policy-456",
346
+ post_transform_validation_scores=[],
347
+ )
348
+ assert validation.post_transform_validation_scores == []
349
+
350
+ def test_valid_post_transform_scores(self) -> None:
351
+ """Test valid post_transform_validation_scores."""
352
+ scores = [("query1", 95), ("query2", 88)]
353
+ validation = DocumentPolicyValidation(
354
+ validation_id="val-123",
355
+ input_document_id="doc-123",
356
+ policy_id="policy-456",
357
+ post_transform_validation_scores=scores,
358
+ )
359
+ assert validation.post_transform_validation_scores == scores
360
+
361
+ def test_post_transform_scores_validation_rules(self) -> None:
362
+ """Test that post_transform_validation_scores follows same rules as
363
+ validation_scores."""
364
+ # Same score range validation
365
+ with pytest.raises(ValidationError) as exc_info:
366
+ DocumentPolicyValidation(
367
+ validation_id="val-123",
368
+ input_document_id="doc-123",
369
+ policy_id="policy-456",
370
+ post_transform_validation_scores=[("query1", -5)],
371
+ )
372
+
373
+ errors = exc_info.value.errors()
374
+ assert any("must be between 0 and 100" in str(error) for error in errors)
375
+
376
+ # Same duplicate detection
377
+ with pytest.raises(ValidationError) as exc_info:
378
+ DocumentPolicyValidation(
379
+ validation_id="val-123",
380
+ input_document_id="doc-123",
381
+ policy_id="policy-456",
382
+ post_transform_validation_scores=[
383
+ ("query1", 85),
384
+ ("query1", 92),
385
+ ],
386
+ )
387
+
388
+ errors = exc_info.value.errors()
389
+ assert any("Duplicate query ID 'query1'" in str(error) for error in errors)
390
+
391
+
392
+ class TestDocumentPolicyValidationStatusEnum:
393
+ """Test DocumentPolicyValidationStatus enum usage."""
394
+
395
+ def test_default_status_is_pending(self) -> None:
396
+ """Test that default status is PENDING."""
397
+ validation = DocumentPolicyValidation(
398
+ validation_id="val-123",
399
+ input_document_id="doc-123",
400
+ policy_id="policy-456",
401
+ )
402
+ assert validation.status == DocumentPolicyValidationStatus.PENDING
403
+
404
+ def test_all_valid_statuses_work(self) -> None:
405
+ """Test that all valid status enum values work correctly."""
406
+ valid_statuses = [
407
+ DocumentPolicyValidationStatus.PENDING,
408
+ DocumentPolicyValidationStatus.IN_PROGRESS,
409
+ DocumentPolicyValidationStatus.PASSED,
410
+ DocumentPolicyValidationStatus.FAILED,
411
+ ]
412
+
413
+ for status in valid_statuses:
414
+ validation = DocumentPolicyValidation(
415
+ validation_id="val-123",
416
+ input_document_id="doc-123",
417
+ policy_id="policy-456",
418
+ status=status,
419
+ )
420
+ assert validation.status == status