julee 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- julee/__init__.py +3 -0
- julee/api/__init__.py +20 -0
- julee/api/app.py +180 -0
- julee/api/dependencies.py +257 -0
- julee/api/requests.py +175 -0
- julee/api/responses.py +43 -0
- julee/api/routers/__init__.py +43 -0
- julee/api/routers/assembly_specifications.py +212 -0
- julee/api/routers/documents.py +182 -0
- julee/api/routers/knowledge_service_configs.py +79 -0
- julee/api/routers/knowledge_service_queries.py +293 -0
- julee/api/routers/system.py +137 -0
- julee/api/routers/workflows.py +234 -0
- julee/api/services/__init__.py +20 -0
- julee/api/services/system_initialization.py +214 -0
- julee/api/tests/__init__.py +14 -0
- julee/api/tests/routers/__init__.py +17 -0
- julee/api/tests/routers/test_assembly_specifications.py +749 -0
- julee/api/tests/routers/test_documents.py +301 -0
- julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
- julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
- julee/api/tests/routers/test_system.py +179 -0
- julee/api/tests/routers/test_workflows.py +393 -0
- julee/api/tests/test_app.py +285 -0
- julee/api/tests/test_dependencies.py +245 -0
- julee/api/tests/test_requests.py +250 -0
- julee/domain/__init__.py +22 -0
- julee/domain/models/__init__.py +49 -0
- julee/domain/models/assembly/__init__.py +17 -0
- julee/domain/models/assembly/assembly.py +103 -0
- julee/domain/models/assembly/tests/__init__.py +0 -0
- julee/domain/models/assembly/tests/factories.py +37 -0
- julee/domain/models/assembly/tests/test_assembly.py +430 -0
- julee/domain/models/assembly_specification/__init__.py +24 -0
- julee/domain/models/assembly_specification/assembly_specification.py +172 -0
- julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
- julee/domain/models/assembly_specification/tests/__init__.py +0 -0
- julee/domain/models/assembly_specification/tests/factories.py +78 -0
- julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
- julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
- julee/domain/models/custom_fields/__init__.py +0 -0
- julee/domain/models/custom_fields/content_stream.py +68 -0
- julee/domain/models/custom_fields/tests/__init__.py +0 -0
- julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
- julee/domain/models/document/__init__.py +17 -0
- julee/domain/models/document/document.py +150 -0
- julee/domain/models/document/tests/__init__.py +0 -0
- julee/domain/models/document/tests/factories.py +76 -0
- julee/domain/models/document/tests/test_document.py +297 -0
- julee/domain/models/knowledge_service_config/__init__.py +17 -0
- julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
- julee/domain/models/policy/__init__.py +15 -0
- julee/domain/models/policy/document_policy_validation.py +220 -0
- julee/domain/models/policy/policy.py +203 -0
- julee/domain/models/policy/tests/__init__.py +0 -0
- julee/domain/models/policy/tests/factories.py +47 -0
- julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
- julee/domain/models/policy/tests/test_policy.py +546 -0
- julee/domain/repositories/__init__.py +27 -0
- julee/domain/repositories/assembly.py +45 -0
- julee/domain/repositories/assembly_specification.py +52 -0
- julee/domain/repositories/base.py +146 -0
- julee/domain/repositories/document.py +49 -0
- julee/domain/repositories/document_policy_validation.py +52 -0
- julee/domain/repositories/knowledge_service_config.py +54 -0
- julee/domain/repositories/knowledge_service_query.py +44 -0
- julee/domain/repositories/policy.py +49 -0
- julee/domain/use_cases/__init__.py +17 -0
- julee/domain/use_cases/decorators.py +107 -0
- julee/domain/use_cases/extract_assemble_data.py +649 -0
- julee/domain/use_cases/initialize_system_data.py +842 -0
- julee/domain/use_cases/tests/__init__.py +7 -0
- julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
- julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
- julee/domain/use_cases/tests/test_validate_document.py +1228 -0
- julee/domain/use_cases/validate_document.py +736 -0
- julee/fixtures/assembly_specifications.yaml +70 -0
- julee/fixtures/documents.yaml +178 -0
- julee/fixtures/knowledge_service_configs.yaml +37 -0
- julee/fixtures/knowledge_service_queries.yaml +27 -0
- julee/repositories/__init__.py +17 -0
- julee/repositories/memory/__init__.py +31 -0
- julee/repositories/memory/assembly.py +84 -0
- julee/repositories/memory/assembly_specification.py +125 -0
- julee/repositories/memory/base.py +227 -0
- julee/repositories/memory/document.py +149 -0
- julee/repositories/memory/document_policy_validation.py +104 -0
- julee/repositories/memory/knowledge_service_config.py +123 -0
- julee/repositories/memory/knowledge_service_query.py +120 -0
- julee/repositories/memory/policy.py +87 -0
- julee/repositories/memory/tests/__init__.py +0 -0
- julee/repositories/memory/tests/test_document.py +212 -0
- julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
- julee/repositories/memory/tests/test_policy.py +443 -0
- julee/repositories/minio/__init__.py +31 -0
- julee/repositories/minio/assembly.py +103 -0
- julee/repositories/minio/assembly_specification.py +170 -0
- julee/repositories/minio/client.py +570 -0
- julee/repositories/minio/document.py +530 -0
- julee/repositories/minio/document_policy_validation.py +120 -0
- julee/repositories/minio/knowledge_service_config.py +187 -0
- julee/repositories/minio/knowledge_service_query.py +211 -0
- julee/repositories/minio/policy.py +106 -0
- julee/repositories/minio/tests/__init__.py +0 -0
- julee/repositories/minio/tests/fake_client.py +213 -0
- julee/repositories/minio/tests/test_assembly.py +374 -0
- julee/repositories/minio/tests/test_assembly_specification.py +391 -0
- julee/repositories/minio/tests/test_client_protocol.py +57 -0
- julee/repositories/minio/tests/test_document.py +591 -0
- julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
- julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
- julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
- julee/repositories/minio/tests/test_policy.py +559 -0
- julee/repositories/temporal/__init__.py +38 -0
- julee/repositories/temporal/activities.py +114 -0
- julee/repositories/temporal/activity_names.py +34 -0
- julee/repositories/temporal/proxies.py +159 -0
- julee/services/__init__.py +18 -0
- julee/services/knowledge_service/__init__.py +48 -0
- julee/services/knowledge_service/anthropic/__init__.py +12 -0
- julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
- julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
- julee/services/knowledge_service/factory.py +138 -0
- julee/services/knowledge_service/knowledge_service.py +160 -0
- julee/services/knowledge_service/memory/__init__.py +13 -0
- julee/services/knowledge_service/memory/knowledge_service.py +278 -0
- julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
- julee/services/knowledge_service/test_factory.py +112 -0
- julee/services/temporal/__init__.py +38 -0
- julee/services/temporal/activities.py +86 -0
- julee/services/temporal/activity_names.py +22 -0
- julee/services/temporal/proxies.py +41 -0
- julee/util/__init__.py +0 -0
- julee/util/domain.py +119 -0
- julee/util/repos/__init__.py +0 -0
- julee/util/repos/minio/__init__.py +0 -0
- julee/util/repos/minio/file_storage.py +213 -0
- julee/util/repos/temporal/__init__.py +11 -0
- julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
- julee/util/repos/temporal/data_converter.py +123 -0
- julee/util/repos/temporal/minio_file_storage.py +12 -0
- julee/util/repos/temporal/proxies/__init__.py +0 -0
- julee/util/repos/temporal/proxies/file_storage.py +58 -0
- julee/util/repositories.py +55 -0
- julee/util/temporal/__init__.py +22 -0
- julee/util/temporal/activities.py +123 -0
- julee/util/temporal/decorators.py +473 -0
- julee/util/tests/__init__.py +1 -0
- julee/util/tests/test_decorators.py +770 -0
- julee/util/validation/__init__.py +29 -0
- julee/util/validation/repository.py +100 -0
- julee/util/validation/type_guards.py +369 -0
- julee/worker.py +211 -0
- julee/workflows/__init__.py +26 -0
- julee/workflows/extract_assemble.py +215 -0
- julee/workflows/validate_document.py +228 -0
- julee-0.1.0.dist-info/METADATA +195 -0
- julee-0.1.0.dist-info/RECORD +161 -0
- julee-0.1.0.dist-info/WHEEL +5 -0
- julee-0.1.0.dist-info/licenses/LICENSE +674 -0
- julee-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|