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,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation utilities for type checking and debugging serialization issues.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for validating runtime types against expected
|
|
5
|
+
types, with special focus on debugging common serialization issues in
|
|
6
|
+
Temporal workflows where Pydantic models get deserialized as dictionaries.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .repository import (
|
|
10
|
+
RepositoryValidationError,
|
|
11
|
+
ensure_repository_protocol,
|
|
12
|
+
validate_repository_protocol,
|
|
13
|
+
)
|
|
14
|
+
from .type_guards import (
|
|
15
|
+
TypeValidationError,
|
|
16
|
+
guard_check,
|
|
17
|
+
validate_parameter_types,
|
|
18
|
+
validate_type,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"TypeValidationError",
|
|
23
|
+
"validate_type",
|
|
24
|
+
"validate_parameter_types",
|
|
25
|
+
"guard_check",
|
|
26
|
+
"RepositoryValidationError",
|
|
27
|
+
"validate_repository_protocol",
|
|
28
|
+
"ensure_repository_protocol",
|
|
29
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository validation utilities for ensuring architectural contracts.
|
|
3
|
+
|
|
4
|
+
This module provides functions to validate repository implementations against
|
|
5
|
+
their defined Protocols using @runtime_checkable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Type, TypeVar
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
P = TypeVar("P")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RepositoryValidationError(Exception):
|
|
17
|
+
"""Raised when repository contract validation fails"""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_repository_protocol(repository: object, protocol: Type[P]) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Validate that a repository implementation satisfies a protocol contract.
|
|
25
|
+
|
|
26
|
+
Uses Python's built-in isinstance() with @runtime_checkable for robust,
|
|
27
|
+
idiomatic protocol validation.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
repository: The repository implementation to validate
|
|
31
|
+
protocol: The protocol class to validate against
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
RepositoryValidationError: If validation fails
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> from julee.util.validation.repository import validate_repository_protocol
|
|
38
|
+
>>> from julee.domain.repositories import DocumentRepository
|
|
39
|
+
>>> repo = MinioDocumentRepository()
|
|
40
|
+
>>> validate_repository_protocol(repo, DocumentRepository)
|
|
41
|
+
"""
|
|
42
|
+
logger.debug(
|
|
43
|
+
"Validating repository protocol",
|
|
44
|
+
extra={
|
|
45
|
+
"repository_type": type(repository).__name__,
|
|
46
|
+
"protocol_name": protocol.__name__,
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if not isinstance(repository, protocol):
|
|
51
|
+
error_message = (
|
|
52
|
+
f"Repository {type(repository).__name__} does not implement "
|
|
53
|
+
f"{protocol.__name__} protocol. Missing or incorrect methods."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
logger.error(
|
|
57
|
+
"Repository protocol validation failed",
|
|
58
|
+
extra={
|
|
59
|
+
"repository_type": type(repository).__name__,
|
|
60
|
+
"protocol_name": protocol.__name__,
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
raise RepositoryValidationError(error_message)
|
|
65
|
+
|
|
66
|
+
logger.info(
|
|
67
|
+
"Repository protocol validation passed",
|
|
68
|
+
extra={
|
|
69
|
+
"repository_type": type(repository).__name__,
|
|
70
|
+
"protocol_name": protocol.__name__,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def ensure_repository_protocol(repository: object, protocol: Type[P]) -> P:
|
|
76
|
+
"""
|
|
77
|
+
Validate and return a repository with proper type annotation.
|
|
78
|
+
|
|
79
|
+
This provides both runtime validation and static type checking benefits.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
repository: The repository implementation to validate
|
|
83
|
+
protocol: The protocol class to validate against
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The validated repository (type checker knows it satisfies the
|
|
87
|
+
protocol)
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
RepositoryValidationError: If validation fails
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> from julee.util.validation.repository import ensure_repository_protocol
|
|
94
|
+
>>> from julee.domain.repositories import DocumentRepository
|
|
95
|
+
>>> repo = MinioDocumentRepository()
|
|
96
|
+
>>> validated_repo = ensure_repository_protocol(repo, DocumentRepository)
|
|
97
|
+
>>> # Type checker now knows validated_repo satisfies DocumentRepository
|
|
98
|
+
"""
|
|
99
|
+
validate_repository_protocol(repository, protocol)
|
|
100
|
+
return repository # type: ignore[return-value]
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic type guard validation system for debugging serialization issues.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for validating that runtime values match their
|
|
5
|
+
expected types, with detailed diagnostics for common serialization issues
|
|
6
|
+
like Pydantic models being deserialized as dictionaries.
|
|
7
|
+
|
|
8
|
+
The system can work with any function by introspecting type hints and
|
|
9
|
+
providing clear error messages when types don't match expectations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
import logging
|
|
14
|
+
from functools import wraps
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
Dict,
|
|
18
|
+
List,
|
|
19
|
+
Union,
|
|
20
|
+
Callable,
|
|
21
|
+
get_type_hints,
|
|
22
|
+
get_origin,
|
|
23
|
+
get_args,
|
|
24
|
+
)
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TypeValidationError(TypeError):
|
|
31
|
+
"""Raised when type validation fails with detailed diagnostics."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_type(
|
|
35
|
+
value: Any,
|
|
36
|
+
expected_type: Any,
|
|
37
|
+
context_name: str = "value",
|
|
38
|
+
allow_none: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Validate that a value matches the expected type with detailed diagnostics.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
value: The actual value to validate
|
|
45
|
+
expected_type: The expected type (from type hints)
|
|
46
|
+
context_name: Name for error messages (e.g., "parameter 'queries'")
|
|
47
|
+
allow_none: Whether None values are acceptable
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
TypeValidationError: With detailed diagnosis if type doesn't match
|
|
51
|
+
"""
|
|
52
|
+
# Handle None values
|
|
53
|
+
if value is None:
|
|
54
|
+
if allow_none:
|
|
55
|
+
return
|
|
56
|
+
raise TypeValidationError(
|
|
57
|
+
f"{context_name}: Expected {_format_type(expected_type)}, " f"got None"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Get the origin type for generic types (List[X] -> list, Dict -> dict)
|
|
61
|
+
origin_type = get_origin(expected_type)
|
|
62
|
+
type_args = get_args(expected_type)
|
|
63
|
+
|
|
64
|
+
# If no origin, it's a simple type
|
|
65
|
+
if origin_type is None:
|
|
66
|
+
_validate_simple_type(value, expected_type, context_name)
|
|
67
|
+
else:
|
|
68
|
+
_validate_generic_type(
|
|
69
|
+
value, expected_type, origin_type, type_args, context_name
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _validate_simple_type(value: Any, expected_type: Any, context_name: str) -> None:
|
|
74
|
+
"""Validate simple (non-generic) types."""
|
|
75
|
+
actual_type = type(value)
|
|
76
|
+
|
|
77
|
+
# Check if it's the expected type
|
|
78
|
+
if isinstance(value, expected_type):
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Special handling for Pydantic models vs dicts (serialization issue)
|
|
82
|
+
if (
|
|
83
|
+
inspect.isclass(expected_type)
|
|
84
|
+
and issubclass(expected_type, BaseModel)
|
|
85
|
+
and isinstance(value, dict)
|
|
86
|
+
):
|
|
87
|
+
_raise_pydantic_dict_error(value, expected_type, context_name)
|
|
88
|
+
|
|
89
|
+
# Generic type mismatch
|
|
90
|
+
raise TypeValidationError(
|
|
91
|
+
f"{context_name}: Type mismatch\n"
|
|
92
|
+
f" Expected: {_format_type(expected_type)}\n"
|
|
93
|
+
f" Actual: {actual_type.__name__}\n"
|
|
94
|
+
f" Value: {_format_value(value)}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _validate_generic_type(
|
|
99
|
+
value: Any,
|
|
100
|
+
expected_type: Any,
|
|
101
|
+
origin_type: Any,
|
|
102
|
+
type_args: tuple,
|
|
103
|
+
context_name: str,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Validate generic types like List[X], Dict[K,V], etc."""
|
|
106
|
+
|
|
107
|
+
# Check the container type first
|
|
108
|
+
if not isinstance(value, origin_type):
|
|
109
|
+
raise TypeValidationError(
|
|
110
|
+
f"{context_name}: Container type mismatch\n"
|
|
111
|
+
f" Expected container: {origin_type.__name__}\n"
|
|
112
|
+
f" Actual container: {type(value).__name__}\n"
|
|
113
|
+
f" Expected full type: {_format_type(expected_type)}\n"
|
|
114
|
+
f" Value: {_format_value(value)}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Validate contents based on container type
|
|
118
|
+
if origin_type is list:
|
|
119
|
+
_validate_list_contents(value, type_args, context_name)
|
|
120
|
+
elif origin_type is dict:
|
|
121
|
+
_validate_dict_contents(value, type_args, context_name)
|
|
122
|
+
elif origin_type is Union:
|
|
123
|
+
_validate_union_type(value, type_args, context_name)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _validate_list_contents(
|
|
127
|
+
value: List[Any], type_args: tuple, context_name: str
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Validate contents of a list."""
|
|
130
|
+
if not type_args:
|
|
131
|
+
return # List without type args, can't validate contents
|
|
132
|
+
|
|
133
|
+
element_type = type_args[0]
|
|
134
|
+
|
|
135
|
+
for i, element in enumerate(value):
|
|
136
|
+
try:
|
|
137
|
+
validate_type(element, element_type, f"{context_name}[{i}]")
|
|
138
|
+
except TypeValidationError as e:
|
|
139
|
+
# Re-raise with additional context
|
|
140
|
+
raise TypeValidationError(
|
|
141
|
+
f"List element validation failed:\n{str(e)}"
|
|
142
|
+
) from e
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _validate_dict_contents(
|
|
146
|
+
value: Dict[Any, Any], type_args: tuple, context_name: str
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Validate contents of a dictionary."""
|
|
149
|
+
if len(type_args) < 2:
|
|
150
|
+
return # Dict without full type args, can't validate contents
|
|
151
|
+
|
|
152
|
+
key_type, value_type = type_args[0], type_args[1]
|
|
153
|
+
|
|
154
|
+
for key, val in value.items():
|
|
155
|
+
# Validate key type
|
|
156
|
+
try:
|
|
157
|
+
validate_type(key, key_type, f"{context_name} key '{key}'")
|
|
158
|
+
except TypeValidationError as e:
|
|
159
|
+
raise TypeValidationError(
|
|
160
|
+
f"Dictionary key validation failed:\n{str(e)}"
|
|
161
|
+
) from e
|
|
162
|
+
|
|
163
|
+
# Validate value type
|
|
164
|
+
try:
|
|
165
|
+
validate_type(val, value_type, f"{context_name}['{key}']")
|
|
166
|
+
except TypeValidationError as e:
|
|
167
|
+
raise TypeValidationError(
|
|
168
|
+
f"Dictionary value validation failed:\n{str(e)}"
|
|
169
|
+
) from e
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _validate_union_type(value: Any, type_args: tuple, context_name: str) -> None:
|
|
173
|
+
"""Validate Union types (including Optional)."""
|
|
174
|
+
# Try each type in the union
|
|
175
|
+
for union_type in type_args:
|
|
176
|
+
try:
|
|
177
|
+
allow_none = union_type is type(None)
|
|
178
|
+
validate_type(value, union_type, context_name, allow_none=allow_none)
|
|
179
|
+
return # If any type matches, we're good
|
|
180
|
+
except TypeValidationError:
|
|
181
|
+
continue # Try the next type
|
|
182
|
+
|
|
183
|
+
# None of the union types matched
|
|
184
|
+
type_names = [_format_type(t) for t in type_args]
|
|
185
|
+
raise TypeValidationError(
|
|
186
|
+
f"{context_name}: Value doesn't match any type in Union\n"
|
|
187
|
+
f" Expected one of: {', '.join(type_names)}\n"
|
|
188
|
+
f" Actual: {type(value).__name__}\n"
|
|
189
|
+
f" Value: {_format_value(value)}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _raise_pydantic_dict_error(
|
|
194
|
+
value: dict, expected_type: type, context_name: str
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Raise a detailed error for Pydantic model vs dict issues."""
|
|
197
|
+
dict_keys = list(value.keys())
|
|
198
|
+
|
|
199
|
+
# Try to identify if this looks like a serialized Pydantic model
|
|
200
|
+
model_fields = []
|
|
201
|
+
if hasattr(expected_type, "model_fields"):
|
|
202
|
+
model_fields = list(expected_type.model_fields.keys())
|
|
203
|
+
|
|
204
|
+
matching_fields = [k for k in dict_keys if k in model_fields]
|
|
205
|
+
|
|
206
|
+
error_msg = (
|
|
207
|
+
f"SERIALIZATION ISSUE DETECTED: {context_name} is dict instead of "
|
|
208
|
+
f"{expected_type.__name__}!\n"
|
|
209
|
+
f" Expected Type: {expected_type.__name__}\n"
|
|
210
|
+
f" Expected Module: {expected_type.__module__}\n"
|
|
211
|
+
f" Actual Type: dict\n"
|
|
212
|
+
f" Dict Keys: {dict_keys}\n"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if model_fields:
|
|
216
|
+
error_msg += (
|
|
217
|
+
f" Expected Model Fields: {model_fields}\n"
|
|
218
|
+
f" Matching Fields: {matching_fields}\n"
|
|
219
|
+
f" Missing Fields: "
|
|
220
|
+
f"{[f for f in model_fields if f not in dict_keys]}\n"
|
|
221
|
+
f" Extra Fields: "
|
|
222
|
+
f"{[k for k in dict_keys if k not in model_fields]}\n"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
error_msg += (
|
|
226
|
+
f" Sample Dict Content: {_format_value(value, max_items=3)}\n"
|
|
227
|
+
f" This indicates a Temporal/serialization issue where a Pydantic "
|
|
228
|
+
f"model\n"
|
|
229
|
+
f" was serialized correctly but deserialized as a plain "
|
|
230
|
+
f"dictionary.\n"
|
|
231
|
+
f" Check your data converter configuration and type hints."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
logger.error(
|
|
235
|
+
"Pydantic serialization issue detected",
|
|
236
|
+
extra={
|
|
237
|
+
"context_name": context_name,
|
|
238
|
+
"expected_type": expected_type.__name__,
|
|
239
|
+
"expected_module": expected_type.__module__,
|
|
240
|
+
"actual_type": "dict",
|
|
241
|
+
"dict_keys": dict_keys,
|
|
242
|
+
"model_fields": model_fields,
|
|
243
|
+
"matching_fields": matching_fields,
|
|
244
|
+
"dict_sample": {k: v for k, v in list(value.items())[:3]},
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
raise TypeValidationError(error_msg)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _format_type(type_hint: Any) -> str:
|
|
252
|
+
"""Format a type hint for display in error messages."""
|
|
253
|
+
if hasattr(type_hint, "__name__"):
|
|
254
|
+
return str(type_hint.__name__)
|
|
255
|
+
return str(type_hint)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _format_value(value: Any, max_items: int = 5) -> str:
|
|
259
|
+
"""Format a value for display in error messages."""
|
|
260
|
+
if isinstance(value, dict):
|
|
261
|
+
if len(value) <= max_items:
|
|
262
|
+
return str(value)
|
|
263
|
+
else:
|
|
264
|
+
items = list(value.items())[:max_items]
|
|
265
|
+
return f"{dict(items)}... ({len(value)} total items)"
|
|
266
|
+
elif isinstance(value, (list, tuple)):
|
|
267
|
+
if len(value) <= max_items:
|
|
268
|
+
return str(value)
|
|
269
|
+
else:
|
|
270
|
+
return f"{value[:max_items]}... ({len(value)} total items)"
|
|
271
|
+
else:
|
|
272
|
+
return repr(value)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def validate_parameter_types(
|
|
276
|
+
**expected_types: Any,
|
|
277
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
278
|
+
"""
|
|
279
|
+
Decorator to validate function parameters against their expected types.
|
|
280
|
+
|
|
281
|
+
Usage:
|
|
282
|
+
@validate_parameter_types(queries=Dict[str, KnowledgeServiceQuery])
|
|
283
|
+
def my_function(self, queries):
|
|
284
|
+
# parameters are validated before function runs
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
Or automatically from type hints:
|
|
288
|
+
@validate_parameter_types()
|
|
289
|
+
def my_function(self, queries: Dict[str, KnowledgeServiceQuery]):
|
|
290
|
+
# type hints are used automatically
|
|
291
|
+
pass
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
295
|
+
@wraps(func)
|
|
296
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
297
|
+
# Get type hints if no explicit types provided
|
|
298
|
+
types_to_check = expected_types
|
|
299
|
+
if not types_to_check:
|
|
300
|
+
try:
|
|
301
|
+
types_to_check = get_type_hints(func)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logger.warning(f"Could not get type hints for {func.__name__}: {e}")
|
|
304
|
+
return func(*args, **kwargs)
|
|
305
|
+
|
|
306
|
+
# Get parameter names
|
|
307
|
+
sig = inspect.signature(func)
|
|
308
|
+
param_names = list(sig.parameters.keys())
|
|
309
|
+
|
|
310
|
+
# Validate positional arguments
|
|
311
|
+
for i, (param_name, arg_value) in enumerate(zip(param_names, args)):
|
|
312
|
+
if param_name in types_to_check and param_name != "self":
|
|
313
|
+
try:
|
|
314
|
+
validate_type(
|
|
315
|
+
arg_value,
|
|
316
|
+
types_to_check[param_name],
|
|
317
|
+
f"parameter '{param_name}'",
|
|
318
|
+
)
|
|
319
|
+
except TypeValidationError:
|
|
320
|
+
logger.error(
|
|
321
|
+
f"Parameter validation failed in {func.__name__}",
|
|
322
|
+
extra={
|
|
323
|
+
"function": func.__name__,
|
|
324
|
+
"parameter": param_name,
|
|
325
|
+
"parameter_index": i,
|
|
326
|
+
},
|
|
327
|
+
)
|
|
328
|
+
raise
|
|
329
|
+
|
|
330
|
+
# Validate keyword arguments
|
|
331
|
+
for param_name, arg_value in kwargs.items():
|
|
332
|
+
if param_name in types_to_check:
|
|
333
|
+
try:
|
|
334
|
+
validate_type(
|
|
335
|
+
arg_value,
|
|
336
|
+
types_to_check[param_name],
|
|
337
|
+
f"parameter '{param_name}'",
|
|
338
|
+
)
|
|
339
|
+
except TypeValidationError:
|
|
340
|
+
logger.error(
|
|
341
|
+
f"Parameter validation failed in {func.__name__}",
|
|
342
|
+
extra={
|
|
343
|
+
"function": func.__name__,
|
|
344
|
+
"parameter": param_name,
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
raise
|
|
348
|
+
|
|
349
|
+
return func(*args, **kwargs)
|
|
350
|
+
|
|
351
|
+
return wrapper
|
|
352
|
+
|
|
353
|
+
return decorator
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def guard_check(value: Any, expected_type: Any, context_name: str = "value") -> None:
|
|
357
|
+
"""
|
|
358
|
+
Simple guard check function for manual validation.
|
|
359
|
+
|
|
360
|
+
Usage:
|
|
361
|
+
guard_check(queries, Dict[str, KnowledgeServiceQuery], "queries")
|
|
362
|
+
guard_check(document, Document, "document")
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
validate_type(value, expected_type, context_name)
|
|
366
|
+
logger.debug(f"Type validation passed for {context_name}")
|
|
367
|
+
except TypeValidationError:
|
|
368
|
+
logger.error(f"Guard check failed for {context_name}")
|
|
369
|
+
raise
|