forked from akhundMurad/typeid-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathv2.py
More file actions
164 lines (128 loc) · 5.05 KB
/
v2.py
File metadata and controls
164 lines (128 loc) · 5.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
from dataclasses import dataclass
from typing import Any, ClassVar, Generic, Literal, Optional, TypeVar, get_args, get_origin
from pydantic_core import core_schema
from pydantic.json_schema import JsonSchemaValue
from typeid import TypeID
T = TypeVar("T")
def _parse_typeid(value: Any) -> TypeID:
"""
Convert input into a TypeID instance.
Supports:
- TypeID -> TypeID
- str -> parse into TypeID
"""
if isinstance(value, TypeID):
return value
if isinstance(value, str):
return TypeID.from_string(value)
raise TypeError(f"TypeID must be str or TypeID, got {type(value).__name__}")
@dataclass(frozen=True)
class _TypeIDMeta:
expected_prefix: Optional[str] = None
pattern: Optional[str] = None
example: Optional[str] = None
class _TypeIDFieldBase:
"""
Base class implementing Pydantic v2 hooks.
Subclasses specify _typeid_meta.
"""
_typeid_meta: ClassVar[_TypeIDMeta] = _TypeIDMeta()
@classmethod
def _validate(cls, v: Any) -> TypeID:
tid = _parse_typeid(v)
exp = cls._typeid_meta.expected_prefix
if exp is not None:
if tid.prefix != exp:
raise ValueError(f"TypeID prefix mismatch: expected '{exp}', got '{tid.prefix}'")
return tid
@classmethod
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema:
"""
Build a schema that:
- accepts TypeID instances
- accepts strings and validates/parses them
- serializes to string in JSON
"""
# Accept either already-parsed TypeID, or a string (or any -> we validate)
# Using a plain validator keeps it simple and fast.
return core_schema.no_info_plain_validator_function(
cls._validate,
json_schema_input_schema=core_schema.str_schema(),
serialization=core_schema.plain_serializer_function_ser_schema(
lambda v: str(v),
when_used="json",
),
)
@classmethod
def __get_pydantic_json_schema__(cls, core_schema_: core_schema.CoreSchema, handler: Any) -> JsonSchemaValue:
# Pass the json_schema_input_schema to the handler instead of the validator function schema
# This allows Pydantic to generate a proper JSON schema from the string type
schema = handler(core_schema_.get("json_schema_input_schema", core_schema_))
# Ensure JSON schema is "string"
schema.update(
{
"type": "string",
"format": "typeid",
}
)
# Add prefix hint in schema
exp = cls._typeid_meta.expected_prefix
if exp is not None:
schema.setdefault("description", f"TypeID with prefix '{exp}'")
# Optional pattern / example
if cls._typeid_meta.pattern:
schema["pattern"] = cls._typeid_meta.pattern
if cls._typeid_meta.example:
schema.setdefault("examples", [cls._typeid_meta.example])
return schema
class TypeIDField(Generic[T]):
"""
Usage:
from typeid.integrations.pydantic import TypeIDField
class User(BaseModel):
id: TypeIDField["user"]
This returns a specialized *type* that Pydantic will validate into your core TypeID.
"""
def __class_getitem__(cls, item: str | tuple[str]) -> type[TypeID]:
"""
Support:
- TypeIDField["user"]
- TypeIDField[Literal["user"]]
- TypeIDField[("user",)]
"""
if isinstance(item, tuple):
if len(item) != 1:
raise TypeError("TypeIDField[...] expects a single prefix")
item = item[0]
# Literal["user"]
if get_origin(item) is Literal:
args = get_args(item)
if len(args) != 1 or not isinstance(args[0], str):
raise TypeError("TypeIDField[Literal['prefix']] expects a single string literal")
prefix = args[0]
# Plain "user"
elif isinstance(item, str):
prefix = item
else:
raise TypeError("TypeIDField[...] expects a string prefix or Literal['prefix']")
name = f"TypeIDField_{prefix}"
# Optionally add a simple example that looks like TypeID format
# You can improve this to a real example generator if your core has one.
example = f"{prefix}_01hxxxxxxxxxxxxxxxxxxxxxxx"
# Create a new subclass of _TypeIDFieldBase with fixed meta
field_cls = type(
name,
(_TypeIDFieldBase,),
{
"_typeid_meta": _TypeIDMeta(
expected_prefix=prefix,
# If you know your precise regex, put it here:
# pattern=rf"^{prefix}_[0-9a-z]{{26}}$",
pattern=None,
example=example,
)
},
)
# IMPORTANT:
# We return `field_cls` as the annotation type, but the runtime validated value is your core TypeID.
return field_cls # type: ignore[return-value]