pycharter 0.0.18__py3-none-any.whl → 0.0.19__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.
- api/main.py +21 -1
- api/models/contracts.py +155 -5
- api/models/metadata.py +25 -1
- api/routes/v1/contracts.py +443 -112
- api/routes/v1/metadata.py +67 -1
- api/routes/v1/templates.py +142 -3
- api/routes/v1/validation.py +54 -88
- pycharter/contract_builder/builder.py +51 -12
- pycharter/contract_parser/parser.py +1 -2
- pycharter/data/__init__.py +1 -0
- pycharter/data/templates/template_coercion_rules.yaml +15 -0
- pycharter/data/templates/template_contract.yaml +587 -0
- pycharter/data/templates/template_metadata.yaml +38 -0
- pycharter/data/templates/template_schema.yaml +22 -0
- pycharter/data/templates/template_validation_rules.yaml +29 -0
- pycharter/db/migrations/versions/20250115120000_add_tier2_tier3_entities.py +12 -0
- pycharter/db/migrations/versions/20260120000000_add_name_title_validation_constraints.py +107 -0
- pycharter/db/migrations/versions/20260121000000_remove_artifact_versions_from_data_contracts.py +65 -0
- pycharter/db/migrations/versions/20260122000000_change_artifact_unique_constraints_to_title_version.py +158 -0
- pycharter/db/models/coercion_rule.py +1 -1
- pycharter/db/models/data_contract.py +2 -6
- pycharter/db/models/dlq.py +1 -1
- pycharter/db/models/metadata_record.py +1 -1
- pycharter/db/models/schema.py +1 -1
- pycharter/db/models/validation_rule.py +1 -1
- pycharter/etl_generator/database.py +46 -28
- pycharter/metadata_store/client.py +64 -15
- pycharter/metadata_store/in_memory.py +9 -12
- pycharter/metadata_store/mongodb.py +184 -43
- pycharter/metadata_store/postgres.py +341 -43
- pycharter/metadata_store/redis.py +12 -15
- pycharter/metadata_store/sqlite.py +174 -34
- pycharter/quality/check.py +2 -9
- pycharter/quality/violations.py +1 -1
- pycharter/runtime_validator/__init__.py +2 -2
- pycharter/runtime_validator/wrappers.py +4 -5
- pycharter/shared/__init__.py +9 -0
- pycharter/shared/name_validator.py +144 -0
- pycharter/utils/value_injector.py +1 -2
- {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/METADATA +1 -1
- pycharter-0.0.19.dist-info/RECORD +222 -0
- ui/server.py +3 -1
- ui/static/404/index.html +1 -1
- ui/static/404.html +1 -1
- ui/static/__next.__PAGE__.txt +2 -2
- ui/static/__next._full.txt +7 -7
- ui/static/__next._head.txt +1 -1
- ui/static/__next._index.txt +6 -6
- ui/static/__next._tree.txt +2 -2
- ui/static/_next/static/chunks/4e310fe5005770a3.css +1 -0
- ui/static/_not-found/__next._full.txt +12 -12
- ui/static/_not-found/__next._head.txt +3 -3
- ui/static/_not-found/__next._index.txt +8 -8
- ui/static/_not-found/__next._not-found.__PAGE__.txt +2 -2
- ui/static/_not-found/__next._not-found.txt +3 -3
- ui/static/_not-found/__next._tree.txt +2 -2
- ui/static/_not-found/index.html +1 -1
- ui/static/_not-found/index.txt +12 -12
- ui/static/contracts/__next._full.txt +7 -7
- ui/static/contracts/__next._head.txt +1 -1
- ui/static/contracts/__next._index.txt +6 -6
- ui/static/contracts/__next._tree.txt +2 -2
- ui/static/contracts/__next.contracts.__PAGE__.txt +2 -2
- ui/static/contracts/__next.contracts.txt +1 -1
- ui/static/contracts/index.html +1 -1
- ui/static/contracts/index.txt +7 -7
- ui/static/documentation/__next._full.txt +7 -7
- ui/static/documentation/__next._head.txt +1 -1
- ui/static/documentation/__next._index.txt +6 -6
- ui/static/documentation/__next._tree.txt +2 -2
- ui/static/documentation/__next.documentation.__PAGE__.txt +2 -2
- ui/static/documentation/__next.documentation.txt +1 -1
- ui/static/documentation/index.html +70 -9
- ui/static/documentation/index.txt +7 -7
- ui/static/index.html +1 -1
- ui/static/index.txt +7 -7
- ui/static/metadata/__next._full.txt +7 -7
- ui/static/metadata/__next._head.txt +1 -1
- ui/static/metadata/__next._index.txt +6 -6
- ui/static/metadata/__next._tree.txt +2 -2
- ui/static/metadata/__next.metadata.__PAGE__.txt +2 -2
- ui/static/metadata/__next.metadata.txt +1 -1
- ui/static/metadata/index.html +1 -1
- ui/static/metadata/index.txt +7 -7
- ui/static/quality/__next._full.txt +7 -7
- ui/static/quality/__next._head.txt +1 -1
- ui/static/quality/__next._index.txt +6 -6
- ui/static/quality/__next._tree.txt +2 -2
- ui/static/quality/__next.quality.__PAGE__.txt +2 -2
- ui/static/quality/__next.quality.txt +1 -1
- ui/static/quality/index.html +2 -1
- ui/static/quality/index.txt +7 -7
- ui/static/rules/__next._full.txt +7 -7
- ui/static/rules/__next._head.txt +1 -1
- ui/static/rules/__next._index.txt +6 -6
- ui/static/rules/__next._tree.txt +2 -2
- ui/static/rules/__next.rules.__PAGE__.txt +2 -2
- ui/static/rules/__next.rules.txt +1 -1
- ui/static/rules/index.html +1 -1
- ui/static/rules/index.txt +7 -7
- ui/static/schemas/__next._full.txt +7 -7
- ui/static/schemas/__next._head.txt +1 -1
- ui/static/schemas/__next._index.txt +6 -6
- ui/static/schemas/__next._tree.txt +2 -2
- ui/static/schemas/__next.schemas.__PAGE__.txt +2 -2
- ui/static/schemas/__next.schemas.txt +1 -1
- ui/static/schemas/index.html +1 -1
- ui/static/schemas/index.txt +7 -7
- ui/static/settings/__next._full.txt +7 -7
- ui/static/settings/__next._head.txt +1 -1
- ui/static/settings/__next._index.txt +6 -6
- ui/static/settings/__next._tree.txt +2 -2
- ui/static/settings/__next.settings.__PAGE__.txt +2 -2
- ui/static/settings/__next.settings.txt +1 -1
- ui/static/settings/index.html +1 -1
- ui/static/settings/index.txt +7 -7
- ui/static/validation/__next._full.txt +7 -7
- ui/static/validation/__next._head.txt +1 -1
- ui/static/validation/__next._index.txt +6 -6
- ui/static/validation/__next._tree.txt +2 -2
- ui/static/validation/__next.validation.__PAGE__.txt +2 -2
- ui/static/validation/__next.validation.txt +1 -1
- ui/static/validation/index.html +1 -1
- ui/static/validation/index.txt +7 -7
- pycharter-0.0.18.dist-info/RECORD +0 -298
- ui/static/_next/static/chunks/3b61d985a8b04c3a.css +0 -1
- ui/static/static/.gitkeep +0 -0
- ui/static/static/404/index.html +0 -1
- ui/static/static/404.html +0 -1
- ui/static/static/__next.__PAGE__.txt +0 -10
- ui/static/static/__next._full.txt +0 -30
- ui/static/static/__next._head.txt +0 -7
- ui/static/static/__next._index.txt +0 -9
- ui/static/static/__next._tree.txt +0 -2
- ui/static/static/_next/static/chunks/e6e82fae2fe3ab15.css +0 -1
- ui/static/static/_not-found/__next._full.txt +0 -17
- ui/static/static/_not-found/__next._head.txt +0 -7
- ui/static/static/_not-found/__next._index.txt +0 -9
- ui/static/static/_not-found/__next._not-found.__PAGE__.txt +0 -5
- ui/static/static/_not-found/__next._not-found.txt +0 -4
- ui/static/static/_not-found/__next._tree.txt +0 -2
- ui/static/static/_not-found/index.html +0 -1
- ui/static/static/_not-found/index.txt +0 -17
- ui/static/static/contracts/__next._full.txt +0 -21
- ui/static/static/contracts/__next._head.txt +0 -7
- ui/static/static/contracts/__next._index.txt +0 -9
- ui/static/static/contracts/__next._tree.txt +0 -2
- ui/static/static/contracts/__next.contracts.__PAGE__.txt +0 -9
- ui/static/static/contracts/__next.contracts.txt +0 -4
- ui/static/static/contracts/index.html +0 -1
- ui/static/static/contracts/index.txt +0 -21
- ui/static/static/documentation/__next._full.txt +0 -21
- ui/static/static/documentation/__next._head.txt +0 -7
- ui/static/static/documentation/__next._index.txt +0 -9
- ui/static/static/documentation/__next._tree.txt +0 -2
- ui/static/static/documentation/__next.documentation.__PAGE__.txt +0 -9
- ui/static/static/documentation/__next.documentation.txt +0 -4
- ui/static/static/documentation/index.html +0 -32
- ui/static/static/documentation/index.txt +0 -21
- ui/static/static/index.html +0 -1
- ui/static/static/index.txt +0 -30
- ui/static/static/metadata/__next._full.txt +0 -21
- ui/static/static/metadata/__next._head.txt +0 -7
- ui/static/static/metadata/__next._index.txt +0 -9
- ui/static/static/metadata/__next._tree.txt +0 -2
- ui/static/static/metadata/__next.metadata.__PAGE__.txt +0 -9
- ui/static/static/metadata/__next.metadata.txt +0 -4
- ui/static/static/metadata/index.html +0 -1
- ui/static/static/metadata/index.txt +0 -21
- ui/static/static/quality/__next._full.txt +0 -21
- ui/static/static/quality/__next._head.txt +0 -7
- ui/static/static/quality/__next._index.txt +0 -9
- ui/static/static/quality/__next._tree.txt +0 -2
- ui/static/static/quality/__next.quality.__PAGE__.txt +0 -9
- ui/static/static/quality/__next.quality.txt +0 -4
- ui/static/static/quality/index.html +0 -1
- ui/static/static/quality/index.txt +0 -21
- ui/static/static/rules/__next._full.txt +0 -21
- ui/static/static/rules/__next._head.txt +0 -7
- ui/static/static/rules/__next._index.txt +0 -9
- ui/static/static/rules/__next._tree.txt +0 -2
- ui/static/static/rules/__next.rules.__PAGE__.txt +0 -9
- ui/static/static/rules/__next.rules.txt +0 -4
- ui/static/static/rules/index.html +0 -1
- ui/static/static/rules/index.txt +0 -21
- ui/static/static/schemas/__next._full.txt +0 -21
- ui/static/static/schemas/__next._head.txt +0 -7
- ui/static/static/schemas/__next._index.txt +0 -9
- ui/static/static/schemas/__next._tree.txt +0 -2
- ui/static/static/schemas/__next.schemas.__PAGE__.txt +0 -9
- ui/static/static/schemas/__next.schemas.txt +0 -4
- ui/static/static/schemas/index.html +0 -1
- ui/static/static/schemas/index.txt +0 -21
- ui/static/static/static/404/index.html +0 -1
- ui/static/static/static/404.html +0 -1
- ui/static/static/static/_next/static/css/56fa3c6cf2f66181.css +0 -3
- ui/static/static/static/contracts/index.html +0 -1
- ui/static/static/static/contracts/index.txt +0 -9
- ui/static/static/static/index.html +0 -1
- ui/static/static/static/index.txt +0 -10
- ui/static/static/static/schemas/index.html +0 -1
- ui/static/static/static/schemas/index.txt +0 -11
- ui/static/static/static/validation/index.html +0 -1
- ui/static/static/static/validation/index.txt +0 -9
- ui/static/static/validation/__next._full.txt +0 -21
- ui/static/static/validation/__next._head.txt +0 -7
- ui/static/static/validation/__next._index.txt +0 -9
- ui/static/static/validation/__next._tree.txt +0 -2
- ui/static/static/validation/__next.validation.__PAGE__.txt +0 -9
- ui/static/static/validation/__next.validation.txt +0 -4
- ui/static/static/validation/index.html +0 -1
- ui/static/static/validation/index.txt +0 -21
- {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/WHEEL +0 -0
- {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/entry_points.txt +0 -0
- {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/licenses/LICENSE +0 -0
- {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/top_level.txt +0 -0
api/main.py
CHANGED
|
@@ -147,11 +147,31 @@ def create_application() -> FastAPI:
|
|
|
147
147
|
request: Request, exc: RequestValidationError
|
|
148
148
|
) -> JSONResponse:
|
|
149
149
|
"""Handle request validation errors."""
|
|
150
|
+
# Convert errors to JSON-serializable format
|
|
151
|
+
def serialize_error(error: dict) -> dict:
|
|
152
|
+
"""Recursively serialize error dict, converting exceptions to strings."""
|
|
153
|
+
serialized = {}
|
|
154
|
+
for key, value in error.items():
|
|
155
|
+
if isinstance(value, Exception):
|
|
156
|
+
serialized[key] = str(value)
|
|
157
|
+
elif isinstance(value, dict):
|
|
158
|
+
serialized[key] = serialize_error(value)
|
|
159
|
+
elif isinstance(value, (list, tuple)):
|
|
160
|
+
serialized[key] = [
|
|
161
|
+
serialize_error(item) if isinstance(item, dict) else str(item) if isinstance(item, Exception) else item
|
|
162
|
+
for item in value
|
|
163
|
+
]
|
|
164
|
+
else:
|
|
165
|
+
serialized[key] = value
|
|
166
|
+
return serialized
|
|
167
|
+
|
|
168
|
+
errors = [serialize_error(err) for err in exc.errors()]
|
|
169
|
+
|
|
150
170
|
return JSONResponse(
|
|
151
171
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
152
172
|
content={
|
|
153
173
|
"error": "Validation error",
|
|
154
|
-
"details":
|
|
174
|
+
"details": errors,
|
|
155
175
|
},
|
|
156
176
|
)
|
|
157
177
|
|
api/models/contracts.py
CHANGED
|
@@ -4,7 +4,9 @@ Request/Response models for contract endpoints.
|
|
|
4
4
|
|
|
5
5
|
from typing import Any, Dict, List, Optional
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from pycharter.shared.name_validator import validate_name
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class ContractParseRequest(BaseModel):
|
|
@@ -68,8 +70,14 @@ class ContractParseResponse(BaseModel):
|
|
|
68
70
|
class ContractBuildRequest(BaseModel):
|
|
69
71
|
"""Request model for building a contract from store."""
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
schema_title: str = Field(..., description="Schema title/identifier")
|
|
74
|
+
schema_version: Optional[str] = Field(None, description="Schema version (default: latest)")
|
|
75
|
+
coercion_rules_title: Optional[str] = Field(None, description="Coercion rules title/identifier (default: schema_title)")
|
|
76
|
+
coercion_rules_version: Optional[str] = Field(None, description="Coercion rules version (default: latest or schema_version)")
|
|
77
|
+
validation_rules_title: Optional[str] = Field(None, description="Validation rules title/identifier (default: schema_title)")
|
|
78
|
+
validation_rules_version: Optional[str] = Field(None, description="Validation rules version (default: latest or schema_version)")
|
|
79
|
+
metadata_title: Optional[str] = Field(None, description="Metadata title/identifier (default: schema_title)")
|
|
80
|
+
metadata_version: Optional[str] = Field(None, description="Metadata version (default: latest or schema_version)")
|
|
73
81
|
include_metadata: bool = Field(True, description="Include metadata in contract")
|
|
74
82
|
include_ownership: bool = Field(True, description="Include ownership in contract")
|
|
75
83
|
include_governance: bool = Field(True, description="Include governance rules in contract")
|
|
@@ -77,8 +85,14 @@ class ContractBuildRequest(BaseModel):
|
|
|
77
85
|
class Config:
|
|
78
86
|
json_schema_extra = {
|
|
79
87
|
"example": {
|
|
80
|
-
"
|
|
81
|
-
"
|
|
88
|
+
"schema_title": "user_schema",
|
|
89
|
+
"schema_version": "1.0.0",
|
|
90
|
+
"coercion_rules_title": "user_schema",
|
|
91
|
+
"coercion_rules_version": "1.0.0",
|
|
92
|
+
"validation_rules_title": "user_schema",
|
|
93
|
+
"validation_rules_version": "1.0.0",
|
|
94
|
+
"metadata_title": "user_schema",
|
|
95
|
+
"metadata_version": "1.0.0",
|
|
82
96
|
"include_metadata": True,
|
|
83
97
|
"include_ownership": True,
|
|
84
98
|
"include_governance": True
|
|
@@ -163,6 +177,134 @@ class ContractListResponse(BaseModel):
|
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
|
|
180
|
+
class ContractCreateFromArtifactsRequest(BaseModel):
|
|
181
|
+
"""Request model for creating a contract from existing artifacts."""
|
|
182
|
+
|
|
183
|
+
name: str = Field(..., description="Contract name")
|
|
184
|
+
version: str = Field(..., description="Contract version")
|
|
185
|
+
schema_title: str = Field(..., description="Schema artifact title")
|
|
186
|
+
schema_version: str = Field(..., description="Schema artifact version")
|
|
187
|
+
coercion_rules_title: Optional[str] = Field(None, description="Coercion rules artifact title (optional)")
|
|
188
|
+
coercion_rules_version: Optional[str] = Field(None, description="Coercion rules artifact version (optional)")
|
|
189
|
+
validation_rules_title: Optional[str] = Field(None, description="Validation rules artifact title (optional)")
|
|
190
|
+
validation_rules_version: Optional[str] = Field(None, description="Validation rules artifact version (optional)")
|
|
191
|
+
metadata_title: Optional[str] = Field(None, description="Metadata artifact title (optional)")
|
|
192
|
+
metadata_version: Optional[str] = Field(None, description="Metadata artifact version (optional)")
|
|
193
|
+
status: Optional[str] = Field("active", description="Contract status")
|
|
194
|
+
description: Optional[str] = Field(None, description="Contract description")
|
|
195
|
+
|
|
196
|
+
@field_validator('name')
|
|
197
|
+
@classmethod
|
|
198
|
+
def validate_name(cls, v: str) -> str:
|
|
199
|
+
"""Validate contract name follows naming convention."""
|
|
200
|
+
return validate_name(v, field_name="name")
|
|
201
|
+
|
|
202
|
+
class Config:
|
|
203
|
+
json_schema_extra = {
|
|
204
|
+
"example": {
|
|
205
|
+
"name": "user_contract",
|
|
206
|
+
"version": "2.0.0",
|
|
207
|
+
"schema_title": "user_schema",
|
|
208
|
+
"schema_version": "1.0.0",
|
|
209
|
+
"coercion_rules_title": "user_schema_coercion_rules",
|
|
210
|
+
"coercion_rules_version": "1.0.0",
|
|
211
|
+
"validation_rules_title": "user_schema_validation_rules",
|
|
212
|
+
"validation_rules_version": "1.0.0",
|
|
213
|
+
"metadata_title": "user_metadata",
|
|
214
|
+
"metadata_version": "1.0.0",
|
|
215
|
+
"status": "active",
|
|
216
|
+
"description": "User data contract"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ContractCreateMixedRequest(BaseModel):
|
|
222
|
+
"""Request model for creating a contract with mixed artifacts (some new, some existing)."""
|
|
223
|
+
|
|
224
|
+
name: str = Field(..., description="Contract name")
|
|
225
|
+
version: str = Field(..., description="Contract version")
|
|
226
|
+
|
|
227
|
+
# Schema: either new data or existing title+version
|
|
228
|
+
schema: Optional[Dict[str, Any]] = Field(None, description="New schema definition (if creating new schema)")
|
|
229
|
+
schema_title: Optional[str] = Field(None, description="Existing schema title (if using existing schema)")
|
|
230
|
+
schema_version: Optional[str] = Field(None, description="Existing schema version (if using existing schema)")
|
|
231
|
+
|
|
232
|
+
# Coercion rules: either new data or existing title+version
|
|
233
|
+
coercion_rules: Optional[Dict[str, Any]] = Field(None, description="New coercion rules (if creating new)")
|
|
234
|
+
coercion_rules_title: Optional[str] = Field(None, description="Existing coercion rules title (if using existing)")
|
|
235
|
+
coercion_rules_version: Optional[str] = Field(None, description="Existing coercion rules version (if using existing)")
|
|
236
|
+
|
|
237
|
+
# Validation rules: either new data or existing title+version
|
|
238
|
+
validation_rules: Optional[Dict[str, Any]] = Field(None, description="New validation rules (if creating new)")
|
|
239
|
+
validation_rules_title: Optional[str] = Field(None, description="Existing validation rules title (if using existing)")
|
|
240
|
+
validation_rules_version: Optional[str] = Field(None, description="Existing validation rules version (if using existing)")
|
|
241
|
+
|
|
242
|
+
# Metadata: either new data or existing title+version
|
|
243
|
+
metadata: Optional[Dict[str, Any]] = Field(None, description="New metadata (if creating new)")
|
|
244
|
+
metadata_title: Optional[str] = Field(None, description="Existing metadata title (if using existing)")
|
|
245
|
+
metadata_version: Optional[str] = Field(None, description="Existing metadata version (if using existing)")
|
|
246
|
+
|
|
247
|
+
status: Optional[str] = Field("active", description="Contract status")
|
|
248
|
+
description: Optional[str] = Field(None, description="Contract description")
|
|
249
|
+
|
|
250
|
+
@field_validator('name')
|
|
251
|
+
@classmethod
|
|
252
|
+
def validate_name(cls, v: str) -> str:
|
|
253
|
+
"""Validate contract name follows naming convention."""
|
|
254
|
+
return validate_name(v, field_name="name")
|
|
255
|
+
|
|
256
|
+
@model_validator(mode='after')
|
|
257
|
+
def validate_artifacts(self):
|
|
258
|
+
"""Validate that each artifact is either new or existing, but not both or neither (for schema)."""
|
|
259
|
+
# Schema must be provided (either new or existing)
|
|
260
|
+
has_new_schema = self.schema is not None
|
|
261
|
+
has_existing_schema = self.schema_title is not None and self.schema_version is not None
|
|
262
|
+
|
|
263
|
+
if not has_new_schema and not has_existing_schema:
|
|
264
|
+
raise ValueError("Schema must be provided either as new data (schema) or existing artifact (schema_title + schema_version)")
|
|
265
|
+
if has_new_schema and has_existing_schema:
|
|
266
|
+
raise ValueError("Cannot specify both new schema and existing schema. Choose one.")
|
|
267
|
+
|
|
268
|
+
# Coercion rules: either new or existing, but not both
|
|
269
|
+
has_new_coercion = self.coercion_rules is not None and len(self.coercion_rules) > 0
|
|
270
|
+
has_existing_coercion = self.coercion_rules_title is not None and self.coercion_rules_version is not None
|
|
271
|
+
|
|
272
|
+
if has_new_coercion and has_existing_coercion:
|
|
273
|
+
raise ValueError("Cannot specify both new coercion rules and existing coercion rules. Choose one.")
|
|
274
|
+
|
|
275
|
+
# Validation rules: either new or existing, but not both
|
|
276
|
+
has_new_validation = self.validation_rules is not None and len(self.validation_rules) > 0
|
|
277
|
+
has_existing_validation = self.validation_rules_title is not None and self.validation_rules_version is not None
|
|
278
|
+
|
|
279
|
+
if has_new_validation and has_existing_validation:
|
|
280
|
+
raise ValueError("Cannot specify both new validation rules and existing validation rules. Choose one.")
|
|
281
|
+
|
|
282
|
+
# Metadata: either new or existing, but not both
|
|
283
|
+
has_new_metadata = self.metadata is not None and len(self.metadata) > 0
|
|
284
|
+
has_existing_metadata = self.metadata_title is not None and self.metadata_version is not None
|
|
285
|
+
|
|
286
|
+
if has_new_metadata and has_existing_metadata:
|
|
287
|
+
raise ValueError("Cannot specify both new metadata and existing metadata. Choose one.")
|
|
288
|
+
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
class Config:
|
|
292
|
+
json_schema_extra = {
|
|
293
|
+
"example": {
|
|
294
|
+
"name": "user_contract",
|
|
295
|
+
"version": "1.0.0",
|
|
296
|
+
"schema": {"type": "object", "properties": {}},
|
|
297
|
+
"coercion_rules_title": "existing_coercion_rules",
|
|
298
|
+
"coercion_rules_version": "1.0.0",
|
|
299
|
+
"validation_rules": {"rules": {}},
|
|
300
|
+
"metadata_title": "existing_metadata",
|
|
301
|
+
"metadata_version": "1.0.0",
|
|
302
|
+
"status": "active",
|
|
303
|
+
"description": "User data contract"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
166
308
|
class ContractUpdateRequest(BaseModel):
|
|
167
309
|
"""Request model for updating a contract."""
|
|
168
310
|
|
|
@@ -171,6 +313,14 @@ class ContractUpdateRequest(BaseModel):
|
|
|
171
313
|
status: Optional[str] = Field(None, description="Contract status (e.g., 'active', 'deprecated', 'draft')")
|
|
172
314
|
description: Optional[str] = Field(None, description="Contract description")
|
|
173
315
|
|
|
316
|
+
@field_validator('name')
|
|
317
|
+
@classmethod
|
|
318
|
+
def validate_name(cls, v: Optional[str]) -> Optional[str]:
|
|
319
|
+
"""Validate contract name follows naming convention."""
|
|
320
|
+
if v is not None:
|
|
321
|
+
return validate_name(v, field_name="name")
|
|
322
|
+
return v
|
|
323
|
+
|
|
174
324
|
class Config:
|
|
175
325
|
json_schema_extra = {
|
|
176
326
|
"example": {
|
api/models/metadata.py
CHANGED
|
@@ -4,7 +4,9 @@ Request/Response models for metadata store endpoints.
|
|
|
4
4
|
|
|
5
5
|
from typing import Any, Dict, List, Optional
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import BaseModel, Field, field_validator
|
|
8
|
+
|
|
9
|
+
from pycharter.shared.name_validator import validate_name
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class SchemaStoreRequest(BaseModel):
|
|
@@ -14,6 +16,20 @@ class SchemaStoreRequest(BaseModel):
|
|
|
14
16
|
schema: Dict[str, Any] = Field(..., description="JSON Schema definition")
|
|
15
17
|
version: str = Field(..., description="Schema version")
|
|
16
18
|
|
|
19
|
+
@field_validator('schema_name')
|
|
20
|
+
@classmethod
|
|
21
|
+
def validate_schema_name(cls, v: str) -> str:
|
|
22
|
+
"""Validate schema name follows naming convention."""
|
|
23
|
+
return validate_name(v, field_name="schema_name")
|
|
24
|
+
|
|
25
|
+
@field_validator('schema')
|
|
26
|
+
@classmethod
|
|
27
|
+
def validate_schema_title(cls, v: Dict[str, Any], info) -> Dict[str, Any]:
|
|
28
|
+
"""Validate schema title if present in schema."""
|
|
29
|
+
if isinstance(v, dict) and 'title' in v and v['title']:
|
|
30
|
+
v['title'] = validate_name(str(v['title']), field_name="schema.title")
|
|
31
|
+
return v
|
|
32
|
+
|
|
17
33
|
class Config:
|
|
18
34
|
json_schema_extra = {
|
|
19
35
|
"example": {
|
|
@@ -90,6 +106,14 @@ class MetadataStoreRequest(BaseModel):
|
|
|
90
106
|
metadata: Dict[str, Any] = Field(..., description="Metadata dictionary")
|
|
91
107
|
version: Optional[str] = Field(None, description="Version string (default: uses schema version)")
|
|
92
108
|
|
|
109
|
+
@field_validator('metadata')
|
|
110
|
+
@classmethod
|
|
111
|
+
def validate_metadata_title(cls, v: Dict[str, Any]) -> Dict[str, Any]:
|
|
112
|
+
"""Validate metadata title if present."""
|
|
113
|
+
if isinstance(v, dict) and 'title' in v and v['title']:
|
|
114
|
+
v['title'] = validate_name(str(v['title']), field_name="metadata.title")
|
|
115
|
+
return v
|
|
116
|
+
|
|
93
117
|
class Config:
|
|
94
118
|
json_schema_extra = {
|
|
95
119
|
"example": {
|