Coverage for postrfp / shared / serial / refmodels.py: 100%
124 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 01:35 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 01:35 +0000
1from typing import Any, Optional, Literal, Annotated
3from pydantic import BaseModel, Field, ConfigDict, StringConstraints
5from postrfp.shared.serial.common import Pagination, TimestampedId
7from .constants import (
8 DESC_DEFAULT_VALUE,
9 DESC_FIELD_SCHEMA,
10 DESC_FIELD_TYPE,
11 DESC_JSON_POINTER_PATH,
12 DESC_JSON_POINTER_PATH_DELETE,
13 DESC_JSON_POINTER_PATH_EXISTING,
14 DESC_JSON_POINTER_PATH_RENAMED,
15 DESC_JSON_POINTER_PATH_FROM,
16 DESC_JSON_POINTER_PATH_TO,
17)
19# COMMON TYPES
21AUTH_POLICY_TYPE = Annotated[
22 str,
23 Field(description="Authorization policy - CEL expression"),
24 StringConstraints(max_length=4000),
25]
27JSON_TYPES = str | int | float | bool | list | dict
30# UNIFIED MODELS
33class ContentSchema(TimestampedId):
34 """Unified content schema model - works for create, update, and response"""
36 # Required/core fields
37 name: str
38 description: Optional[str] = None
39 version: int | None = None
40 spec_doc: dict[str, Any]
41 is_draft: bool | None = None
42 auth_policy: Optional[AUTH_POLICY_TYPE] = None
43 is_validated: bool | None = None
44 tags: list[str] = Field(default_factory=list)
46 # Optional for input, present in responses
47 org_id: Optional[str] = None
49 model_config = ConfigDict(from_attributes=True)
52class TagSummary(BaseModel):
53 """Tag reference - can be minimal for lists or full for details"""
55 id: int
56 name: Optional[str] = None
57 description: Optional[str] = None
59 model_config = ConfigDict(from_attributes=True)
62class SubjectSummary(BaseModel):
63 """Subject reference - can be minimal for lists or full for details"""
65 id: int
66 parent_id: Optional[int] = None
67 name: Optional[str] = None
68 code: Optional[str] = None
69 description: Optional[str] = None
70 subject_type: Optional[str] = None
72 model_config = ConfigDict(from_attributes=True)
75class ContentDocument(TimestampedId):
76 """Unified content model - adapts based on usage context"""
78 # Required/core fields
79 title: str
80 schema_id: int
81 is_validated: bool | None = None
83 # Optional fields
84 content_doc: Optional[dict[str, Any]] = None # Full content only in detail views
85 primary_subject_id: Optional[int] = None
86 auth_policy: Optional[AUTH_POLICY_TYPE] = None
88 # Flexible references - can be IDs (input) or objects (output)
89 tags: list[TagSummary] = Field(default_factory=list)
90 subjects: list[SubjectSummary] = Field(default_factory=list)
92 # Response-only fields
93 author_org_id: Optional[str] = None
94 last_updated_by_id: Optional[str] = None
96 model_config = ConfigDict(from_attributes=True)
99# PERMISSION MODELS
102class PolicyRequest(BaseModel):
103 """Request to update/delete a policy"""
105 entity_type: Literal["Content", "ContentSpec", "Subject"]
106 entity_id: int
107 policy: Optional[str] = None # None for delete operations
110class PolicyResponse(PolicyRequest):
111 """Response from policy operations"""
113 success: bool
114 entity_name: Optional[str] = None
115 error: Optional[str] = None
116 operation_performed: Optional[str] = None
119class PermissionUpdated(BaseModel):
120 """Response for individual permission update operations"""
122 operation_performed: str
123 permission_name: str
124 entity_type: str
125 entity_name: str
126 target_org: str
128 model_config = ConfigDict(from_attributes=True)
131class SubjectDocument(TimestampedId):
132 """Unified subject model - works for create, update, and response"""
134 # Required/core fields
135 name: str
136 subject_type: str # Using string to handle enum serialization
138 # Optional fields
139 code: Optional[str] = None
140 description: Optional[str] = None
141 auth_policy: Optional[AUTH_POLICY_TYPE] = None
142 parent_id: Optional[int] = None
143 managing_org_id: Optional[str] = None
144 subject_metadata: Optional[dict[str, Any]] = None
146 model_config = ConfigDict(from_attributes=True)
149class ContentQElementPairDocument(BaseModel):
150 """Mapping between a question element and a content field via JSON Pointer"""
152 # Optional for input, required for responses
153 id: Optional[int] = None
155 # Required fields
156 question_element_id: int
157 content_reference: str = Field(
158 description="JSON Pointer expression to locate field in content document (e.g., '$.sla.uptime')"
159 )
161 # Response-only field (parent relationship)
162 content_map_id: Optional[int] = None
164 model_config = ConfigDict(from_attributes=True)
167class ContentSpecMapDocument(BaseModel):
168 """Mapping between question elements and content fields"""
170 # Optional for input, required for responses
171 id: Optional[int] = Field(
172 None, description="Unique ID of the content spec map, assigned by the system"
173 )
175 # Required fields
176 name: str
177 description: str
178 content_spec_id: int
180 content_spec_name: Optional[str] = Field(
181 None, description="Name of the associated content spec, not required for input"
182 )
184 # Nested pairs - can be provided on create/update
185 pairs: list[ContentQElementPairDocument] = Field(default_factory=list)
187 model_config = ConfigDict(from_attributes=True)
190# GENERIC RESPONSE TYPES
193class ListResponse(BaseModel):
194 """Generic list response that works with any item type"""
196 items: (
197 list[ContentDocument]
198 | list[SubjectSummary]
199 | list[ContentSchema]
200 | list[ContentSpecMapDocument]
201 )
202 pagination: Pagination
204 model_config = ConfigDict(from_attributes=True)
207class DeletionResponse(BaseModel):
208 """Standard deletion response"""
210 success: bool
211 message: str
213 model_config = ConfigDict(from_attributes=True)
216class SchemaPointersResponse(BaseModel):
217 """Response containing valid JSON Pointer paths for a schema"""
219 content_spec_id: int
220 content_spec_name: str
221 pointers: list[str] = Field(
222 description="List of valid JSON Pointer paths that can be used in ContentQElementPair mappings"
223 )
225 model_config = ConfigDict(from_attributes=True)
228class JsonPatchOp(BaseModel):
229 """JSON Patch operation model"""
231 op: Literal["add", "remove", "replace", "move", "copy", "test"]
232 path: str
233 value: JSON_TYPES | None = None
234 from_: str | None = Field(
235 default=None, alias="from", exclude_if=lambda v: v is None
236 )
238 model_config = {"populate_by_name": True, "serialize_by_alias": True}
241class ContentPatch(BaseModel):
242 """List of JSON Patch operations"""
244 patches: list[JsonPatchOp]
245 comment: str
248# Request models for each migration operation
251class AddOptionalFieldRequest(BaseModel):
252 """Request to add an optional field to a JSON schema"""
254 path: str = Field(description=DESC_JSON_POINTER_PATH)
255 field_type: str = Field(description=DESC_FIELD_TYPE)
256 field_schema: dict | None = Field(
257 default=None,
258 description=DESC_FIELD_SCHEMA,
259 )
262class AddRequiredFieldRequest(BaseModel):
263 """Request to add a required field to a JSON schema and update existing documents"""
265 path: str = Field(description=DESC_JSON_POINTER_PATH)
266 field_type: str = Field(description=DESC_FIELD_TYPE)
267 default_value: JSON_TYPES = Field(description=DESC_DEFAULT_VALUE)
268 field_schema: dict | None = Field(
269 default=None,
270 description=DESC_FIELD_SCHEMA,
271 )
274class DeleteFieldRequest(BaseModel):
275 """Request to delete a field from a JSON schema and existing documents"""
277 path: str = Field(description=DESC_JSON_POINTER_PATH_DELETE)
280class RenameFieldRequest(BaseModel):
281 """Request to rename a field in a JSON schema and existing documents"""
283 old_path: str = Field(description=DESC_JSON_POINTER_PATH_EXISTING)
284 new_path: str = Field(description=DESC_JSON_POINTER_PATH_RENAMED)
287class MoveFieldRequest(BaseModel):
288 """Request to move a field to a different location in the schema"""
290 from_path: str = Field(description=DESC_JSON_POINTER_PATH_FROM)
291 to_path: str = Field(description=DESC_JSON_POINTER_PATH_TO)
294class SchemaUpdate(BaseModel):
295 """Schema update with optional migration operation"""
297 request_type: Literal[
298 "add_optional_field",
299 "add_required_field",
300 "delete_field",
301 "rename_field",
302 "move_field",
303 ]
304 request: (
305 AddOptionalFieldRequest
306 | AddRequiredFieldRequest
307 | DeleteFieldRequest
308 | RenameFieldRequest
309 | MoveFieldRequest
310 )