-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpatch_http_validation_error.py
More file actions
145 lines (118 loc) · 5.17 KB
/
Copy pathpatch_http_validation_error.py
File metadata and controls
145 lines (118 loc) · 5.17 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
#!/usr/bin/env python3
"""
Patch the generated HTTPValidationError model so it correctly handles both
list-of-ValidationError and plain-string ``detail`` payloads from the API.
Some API error paths (e.g. invalid model alias) return a 422 whose ``detail``
is a plain string instead of the standard list-of-ValidationError objects.
The auto-generated ``from_dict`` iterates ``detail`` unconditionally, which
causes it to crash when ``detail`` is a string (each character is passed to
``ValidationError.from_dict``). This script fixes that by replacing the
generated for-loop with an isinstance-guarded block, and changes the field
initialisation from an empty list to UNSET so missing/absent detail is
distinguishable from an empty error list.
Workflow
--------
Run this script as a post-generation hook after
``scripts/auto-generate-api-client.sh``::
python scripts/patch_http_validation_error.py \\
src/galileo/resources/models/http_validation_error.py
Exit codes
----------
0 patch applied successfully
1 I/O or syntax error
2 expected pattern not found in target (template drift — update this script)
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
# ---------------------------------------------------------------------------
# Patterns to find in the auto-generated file
# ---------------------------------------------------------------------------
# Inside from_dict — 0.29.0 generator output:
#
# _detail = d.pop("detail", UNSET)
# detail: list[ValidationError] | Unset = UNSET
# if _detail is not UNSET:
# detail = []
# for detail_item_data in _detail:
# detail_item = ValidationError.from_dict(detail_item_data)
# <- blank line
# detail.append(detail_item)
#
# Changes from 0.26.x: _detail = d.pop(...) now comes BEFORE the type-annotated
# init; the loop body is wrapped in `if _detail is not UNSET:`; the `or []` is
# gone; type annotation uses `list[...] | Unset` instead of `Union[Unset, list[...]]`.
_LOOP_RE = re.compile(
r"(?P<indent>[ \t]+)_detail = d\.pop\(\"detail\", UNSET\)\n"
r"(?P=indent)detail: list\[ValidationError\] \| Unset = UNSET\n"
r"(?P=indent)if _detail is not UNSET:\n"
r"(?P=indent) detail = \[\]\n"
r"(?P=indent) for detail_item_data in _detail:\n"
r"(?P=indent) detail_item = ValidationError\.from_dict\(detail_item_data\)\n"
r"[ \t]*\n"
r"(?P=indent) detail\.append\(detail_item\)",
re.MULTILINE,
)
def _loop_replacement(indent: str) -> str:
i = indent
i4 = indent + " "
return "\n".join(
[
f'{i}_detail = d.pop("detail", UNSET)',
f"{i}detail: list[ValidationError] | Unset = UNSET",
f"{i}if isinstance(_detail, list):",
f"{i4}detail = [ValidationError.from_dict(item) for item in _detail]",
f"{i}elif isinstance(_detail, str) and _detail:",
f"{i4}# Some backend 422s return a bare string instead of the standard",
f"{i4}# list-of-ValidationError shape (e.g. invalid model alias lookups",
f'{i4}# returning {{"detail": "Model alias \'...\' not found"}}).',
f"{i4}# Wrap it so callers always receive a consistent ValidationError list.",
f'{i4}detail = [ValidationError(loc=[], msg=_detail, type_="server_error")]',
]
)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
_PATCHED_SENTINEL = "isinstance(_detail, list)"
def patch(text: str) -> tuple[str, bool]:
"""Rewrite the from_dict loop; return (patched_text, success)."""
patched, count = _LOOP_RE.subn(lambda m: _loop_replacement(m.group("indent")), text)
# Treat the file as already-patched if no replacements were made but the
# patched sentinel is present — ensures idempotent re-runs exit 0 instead
# of falsely reporting template drift (exit 2).
already_patched = count == 0 and _PATCHED_SENTINEL in text
return patched, bool(count) or already_patched
def main() -> int:
if len(sys.argv) != 2:
print(f"Usage: {Path(sys.argv[0]).name} <file_path>", file=sys.stderr)
return 1
target = Path(sys.argv[1])
try:
original = target.read_text(encoding="utf-8")
except FileNotFoundError:
print(f"{target} not found", file=sys.stderr)
return 1
except OSError as e:
print(f"Error reading {target}: {e}", file=sys.stderr)
return 1
patched, success = patch(original)
if not success:
print(
f"Expected patterns not found in {target} — template may have changed. "
"Update this script to match the new generated code.",
file=sys.stderr,
)
return 2
if patched == original:
print(f"{target} already patched — nothing to do.")
return 0
try:
target.write_text(patched, encoding="utf-8")
except OSError as e:
print(f"Error writing {target}: {e}", file=sys.stderr)
return 1
print(f"Patched {target}")
return 0
if __name__ == "__main__":
sys.exit(main())