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.
Files changed (216) hide show
  1. api/main.py +21 -1
  2. api/models/contracts.py +155 -5
  3. api/models/metadata.py +25 -1
  4. api/routes/v1/contracts.py +443 -112
  5. api/routes/v1/metadata.py +67 -1
  6. api/routes/v1/templates.py +142 -3
  7. api/routes/v1/validation.py +54 -88
  8. pycharter/contract_builder/builder.py +51 -12
  9. pycharter/contract_parser/parser.py +1 -2
  10. pycharter/data/__init__.py +1 -0
  11. pycharter/data/templates/template_coercion_rules.yaml +15 -0
  12. pycharter/data/templates/template_contract.yaml +587 -0
  13. pycharter/data/templates/template_metadata.yaml +38 -0
  14. pycharter/data/templates/template_schema.yaml +22 -0
  15. pycharter/data/templates/template_validation_rules.yaml +29 -0
  16. pycharter/db/migrations/versions/20250115120000_add_tier2_tier3_entities.py +12 -0
  17. pycharter/db/migrations/versions/20260120000000_add_name_title_validation_constraints.py +107 -0
  18. pycharter/db/migrations/versions/20260121000000_remove_artifact_versions_from_data_contracts.py +65 -0
  19. pycharter/db/migrations/versions/20260122000000_change_artifact_unique_constraints_to_title_version.py +158 -0
  20. pycharter/db/models/coercion_rule.py +1 -1
  21. pycharter/db/models/data_contract.py +2 -6
  22. pycharter/db/models/dlq.py +1 -1
  23. pycharter/db/models/metadata_record.py +1 -1
  24. pycharter/db/models/schema.py +1 -1
  25. pycharter/db/models/validation_rule.py +1 -1
  26. pycharter/etl_generator/database.py +46 -28
  27. pycharter/metadata_store/client.py +64 -15
  28. pycharter/metadata_store/in_memory.py +9 -12
  29. pycharter/metadata_store/mongodb.py +184 -43
  30. pycharter/metadata_store/postgres.py +341 -43
  31. pycharter/metadata_store/redis.py +12 -15
  32. pycharter/metadata_store/sqlite.py +174 -34
  33. pycharter/quality/check.py +2 -9
  34. pycharter/quality/violations.py +1 -1
  35. pycharter/runtime_validator/__init__.py +2 -2
  36. pycharter/runtime_validator/wrappers.py +4 -5
  37. pycharter/shared/__init__.py +9 -0
  38. pycharter/shared/name_validator.py +144 -0
  39. pycharter/utils/value_injector.py +1 -2
  40. {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/METADATA +1 -1
  41. pycharter-0.0.19.dist-info/RECORD +222 -0
  42. ui/server.py +3 -1
  43. ui/static/404/index.html +1 -1
  44. ui/static/404.html +1 -1
  45. ui/static/__next.__PAGE__.txt +2 -2
  46. ui/static/__next._full.txt +7 -7
  47. ui/static/__next._head.txt +1 -1
  48. ui/static/__next._index.txt +6 -6
  49. ui/static/__next._tree.txt +2 -2
  50. ui/static/_next/static/chunks/4e310fe5005770a3.css +1 -0
  51. ui/static/_not-found/__next._full.txt +12 -12
  52. ui/static/_not-found/__next._head.txt +3 -3
  53. ui/static/_not-found/__next._index.txt +8 -8
  54. ui/static/_not-found/__next._not-found.__PAGE__.txt +2 -2
  55. ui/static/_not-found/__next._not-found.txt +3 -3
  56. ui/static/_not-found/__next._tree.txt +2 -2
  57. ui/static/_not-found/index.html +1 -1
  58. ui/static/_not-found/index.txt +12 -12
  59. ui/static/contracts/__next._full.txt +7 -7
  60. ui/static/contracts/__next._head.txt +1 -1
  61. ui/static/contracts/__next._index.txt +6 -6
  62. ui/static/contracts/__next._tree.txt +2 -2
  63. ui/static/contracts/__next.contracts.__PAGE__.txt +2 -2
  64. ui/static/contracts/__next.contracts.txt +1 -1
  65. ui/static/contracts/index.html +1 -1
  66. ui/static/contracts/index.txt +7 -7
  67. ui/static/documentation/__next._full.txt +7 -7
  68. ui/static/documentation/__next._head.txt +1 -1
  69. ui/static/documentation/__next._index.txt +6 -6
  70. ui/static/documentation/__next._tree.txt +2 -2
  71. ui/static/documentation/__next.documentation.__PAGE__.txt +2 -2
  72. ui/static/documentation/__next.documentation.txt +1 -1
  73. ui/static/documentation/index.html +70 -9
  74. ui/static/documentation/index.txt +7 -7
  75. ui/static/index.html +1 -1
  76. ui/static/index.txt +7 -7
  77. ui/static/metadata/__next._full.txt +7 -7
  78. ui/static/metadata/__next._head.txt +1 -1
  79. ui/static/metadata/__next._index.txt +6 -6
  80. ui/static/metadata/__next._tree.txt +2 -2
  81. ui/static/metadata/__next.metadata.__PAGE__.txt +2 -2
  82. ui/static/metadata/__next.metadata.txt +1 -1
  83. ui/static/metadata/index.html +1 -1
  84. ui/static/metadata/index.txt +7 -7
  85. ui/static/quality/__next._full.txt +7 -7
  86. ui/static/quality/__next._head.txt +1 -1
  87. ui/static/quality/__next._index.txt +6 -6
  88. ui/static/quality/__next._tree.txt +2 -2
  89. ui/static/quality/__next.quality.__PAGE__.txt +2 -2
  90. ui/static/quality/__next.quality.txt +1 -1
  91. ui/static/quality/index.html +2 -1
  92. ui/static/quality/index.txt +7 -7
  93. ui/static/rules/__next._full.txt +7 -7
  94. ui/static/rules/__next._head.txt +1 -1
  95. ui/static/rules/__next._index.txt +6 -6
  96. ui/static/rules/__next._tree.txt +2 -2
  97. ui/static/rules/__next.rules.__PAGE__.txt +2 -2
  98. ui/static/rules/__next.rules.txt +1 -1
  99. ui/static/rules/index.html +1 -1
  100. ui/static/rules/index.txt +7 -7
  101. ui/static/schemas/__next._full.txt +7 -7
  102. ui/static/schemas/__next._head.txt +1 -1
  103. ui/static/schemas/__next._index.txt +6 -6
  104. ui/static/schemas/__next._tree.txt +2 -2
  105. ui/static/schemas/__next.schemas.__PAGE__.txt +2 -2
  106. ui/static/schemas/__next.schemas.txt +1 -1
  107. ui/static/schemas/index.html +1 -1
  108. ui/static/schemas/index.txt +7 -7
  109. ui/static/settings/__next._full.txt +7 -7
  110. ui/static/settings/__next._head.txt +1 -1
  111. ui/static/settings/__next._index.txt +6 -6
  112. ui/static/settings/__next._tree.txt +2 -2
  113. ui/static/settings/__next.settings.__PAGE__.txt +2 -2
  114. ui/static/settings/__next.settings.txt +1 -1
  115. ui/static/settings/index.html +1 -1
  116. ui/static/settings/index.txt +7 -7
  117. ui/static/validation/__next._full.txt +7 -7
  118. ui/static/validation/__next._head.txt +1 -1
  119. ui/static/validation/__next._index.txt +6 -6
  120. ui/static/validation/__next._tree.txt +2 -2
  121. ui/static/validation/__next.validation.__PAGE__.txt +2 -2
  122. ui/static/validation/__next.validation.txt +1 -1
  123. ui/static/validation/index.html +1 -1
  124. ui/static/validation/index.txt +7 -7
  125. pycharter-0.0.18.dist-info/RECORD +0 -298
  126. ui/static/_next/static/chunks/3b61d985a8b04c3a.css +0 -1
  127. ui/static/static/.gitkeep +0 -0
  128. ui/static/static/404/index.html +0 -1
  129. ui/static/static/404.html +0 -1
  130. ui/static/static/__next.__PAGE__.txt +0 -10
  131. ui/static/static/__next._full.txt +0 -30
  132. ui/static/static/__next._head.txt +0 -7
  133. ui/static/static/__next._index.txt +0 -9
  134. ui/static/static/__next._tree.txt +0 -2
  135. ui/static/static/_next/static/chunks/e6e82fae2fe3ab15.css +0 -1
  136. ui/static/static/_not-found/__next._full.txt +0 -17
  137. ui/static/static/_not-found/__next._head.txt +0 -7
  138. ui/static/static/_not-found/__next._index.txt +0 -9
  139. ui/static/static/_not-found/__next._not-found.__PAGE__.txt +0 -5
  140. ui/static/static/_not-found/__next._not-found.txt +0 -4
  141. ui/static/static/_not-found/__next._tree.txt +0 -2
  142. ui/static/static/_not-found/index.html +0 -1
  143. ui/static/static/_not-found/index.txt +0 -17
  144. ui/static/static/contracts/__next._full.txt +0 -21
  145. ui/static/static/contracts/__next._head.txt +0 -7
  146. ui/static/static/contracts/__next._index.txt +0 -9
  147. ui/static/static/contracts/__next._tree.txt +0 -2
  148. ui/static/static/contracts/__next.contracts.__PAGE__.txt +0 -9
  149. ui/static/static/contracts/__next.contracts.txt +0 -4
  150. ui/static/static/contracts/index.html +0 -1
  151. ui/static/static/contracts/index.txt +0 -21
  152. ui/static/static/documentation/__next._full.txt +0 -21
  153. ui/static/static/documentation/__next._head.txt +0 -7
  154. ui/static/static/documentation/__next._index.txt +0 -9
  155. ui/static/static/documentation/__next._tree.txt +0 -2
  156. ui/static/static/documentation/__next.documentation.__PAGE__.txt +0 -9
  157. ui/static/static/documentation/__next.documentation.txt +0 -4
  158. ui/static/static/documentation/index.html +0 -32
  159. ui/static/static/documentation/index.txt +0 -21
  160. ui/static/static/index.html +0 -1
  161. ui/static/static/index.txt +0 -30
  162. ui/static/static/metadata/__next._full.txt +0 -21
  163. ui/static/static/metadata/__next._head.txt +0 -7
  164. ui/static/static/metadata/__next._index.txt +0 -9
  165. ui/static/static/metadata/__next._tree.txt +0 -2
  166. ui/static/static/metadata/__next.metadata.__PAGE__.txt +0 -9
  167. ui/static/static/metadata/__next.metadata.txt +0 -4
  168. ui/static/static/metadata/index.html +0 -1
  169. ui/static/static/metadata/index.txt +0 -21
  170. ui/static/static/quality/__next._full.txt +0 -21
  171. ui/static/static/quality/__next._head.txt +0 -7
  172. ui/static/static/quality/__next._index.txt +0 -9
  173. ui/static/static/quality/__next._tree.txt +0 -2
  174. ui/static/static/quality/__next.quality.__PAGE__.txt +0 -9
  175. ui/static/static/quality/__next.quality.txt +0 -4
  176. ui/static/static/quality/index.html +0 -1
  177. ui/static/static/quality/index.txt +0 -21
  178. ui/static/static/rules/__next._full.txt +0 -21
  179. ui/static/static/rules/__next._head.txt +0 -7
  180. ui/static/static/rules/__next._index.txt +0 -9
  181. ui/static/static/rules/__next._tree.txt +0 -2
  182. ui/static/static/rules/__next.rules.__PAGE__.txt +0 -9
  183. ui/static/static/rules/__next.rules.txt +0 -4
  184. ui/static/static/rules/index.html +0 -1
  185. ui/static/static/rules/index.txt +0 -21
  186. ui/static/static/schemas/__next._full.txt +0 -21
  187. ui/static/static/schemas/__next._head.txt +0 -7
  188. ui/static/static/schemas/__next._index.txt +0 -9
  189. ui/static/static/schemas/__next._tree.txt +0 -2
  190. ui/static/static/schemas/__next.schemas.__PAGE__.txt +0 -9
  191. ui/static/static/schemas/__next.schemas.txt +0 -4
  192. ui/static/static/schemas/index.html +0 -1
  193. ui/static/static/schemas/index.txt +0 -21
  194. ui/static/static/static/404/index.html +0 -1
  195. ui/static/static/static/404.html +0 -1
  196. ui/static/static/static/_next/static/css/56fa3c6cf2f66181.css +0 -3
  197. ui/static/static/static/contracts/index.html +0 -1
  198. ui/static/static/static/contracts/index.txt +0 -9
  199. ui/static/static/static/index.html +0 -1
  200. ui/static/static/static/index.txt +0 -10
  201. ui/static/static/static/schemas/index.html +0 -1
  202. ui/static/static/static/schemas/index.txt +0 -11
  203. ui/static/static/static/validation/index.html +0 -1
  204. ui/static/static/static/validation/index.txt +0 -9
  205. ui/static/static/validation/__next._full.txt +0 -21
  206. ui/static/static/validation/__next._head.txt +0 -7
  207. ui/static/static/validation/__next._index.txt +0 -9
  208. ui/static/static/validation/__next._tree.txt +0 -2
  209. ui/static/static/validation/__next.validation.__PAGE__.txt +0 -9
  210. ui/static/static/validation/__next.validation.txt +0 -4
  211. ui/static/static/validation/index.html +0 -1
  212. ui/static/static/validation/index.txt +0 -21
  213. {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/WHEEL +0 -0
  214. {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/entry_points.txt +0 -0
  215. {pycharter-0.0.18.dist-info → pycharter-0.0.19.dist-info}/licenses/LICENSE +0 -0
  216. {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": exc.errors(),
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
- schema_id: str = Field(..., description="Schema identifier")
72
- version: Optional[str] = Field(None, description="Schema version (default: latest)")
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
- "schema_id": "user_schema",
81
- "version": "1.0.0",
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": {