ssc_codegen 0.19.4__tar.gz → 0.20.0__tar.gz
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.
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/PKG-INFO +2 -1
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/pyproject.toml +2 -1
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/converters/py_bs4.py +19 -336
- ssc_codegen-0.20.0/ssc_codegen/converters/py_helpers.py +361 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/converters/py_lxml.py +4 -3
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/converters/py_parsel.py +3 -2
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/converters/py_slax.py +3 -2
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/main.py +77 -0
- ssc_codegen-0.20.0/ssc_codegen/openapi/__init__.py +87 -0
- ssc_codegen-0.20.0/ssc_codegen/openapi/converter.py +684 -0
- ssc_codegen-0.20.0/ssc_codegen/openapi/emitter.py +128 -0
- ssc_codegen-0.20.0/ssc_codegen/openapi/parser.py +143 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/parsers/spec.py +3 -1
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/.gitignore +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/LICENSE +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/README.md +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/__init__.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/_logging.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/__init__.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/array.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/base.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/cast.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/control.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/extract.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/helpers.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/jsondef.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/module.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/predicate_containers.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/predicate_ops.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/regex.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/selectors.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/string.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/struct.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/transform.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/typedef.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/ast/types.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/converters/base.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/converters/go_goquery.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/converters/helpers.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/converters/js_pure.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/document_utils.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/exceptions.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/health.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/kdl/__init__.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/kdl/parser.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/__init__.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/_kdl_lang.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/base.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/errors.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/format_errors.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/metadata.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/navigation.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/path.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/rule_keywords.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/rules.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/rules_struct.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/type_rules.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/linter/types.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/parser.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/parsers/__init__.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/parsers/curl.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/parsers/http.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/pseudo_selectors.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/regex_utils.py +0 -0
- {ssc_codegen-0.19.4 → ssc_codegen-0.20.0}/ssc_codegen/selector_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssc_codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Python-dsl code converter to html parser for web scraping
|
|
5
5
|
Project-URL: Documentation, https://github.com/vypivshiy/selector_schema_codegen#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/vypivshiy/selector_schema_codegen/issues
|
|
@@ -24,6 +24,7 @@ Requires-Dist: click<8.2.0
|
|
|
24
24
|
Requires-Dist: colorama>=0.4.6; sys_platform == 'win32'
|
|
25
25
|
Requires-Dist: cssselect>=1.2.0
|
|
26
26
|
Requires-Dist: lxml>=5.3.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
28
|
Requires-Dist: soupsieve>=2.6
|
|
28
29
|
Requires-Dist: typer>=0.15.1
|
|
29
30
|
Requires-Dist: typing-extensions; python_version < '3.11'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ssc_codegen"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.20.0"
|
|
4
4
|
description = "Python-dsl code converter to html parser for web scraping "
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
# https://github.com/fastapi/typer/discussions/1215
|
|
16
16
|
# https://github.com/fastapi/typer/pull/1145
|
|
17
17
|
"click<8.2.0",
|
|
18
|
+
"pyyaml>=6.0",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
classifiers = [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from ssc_codegen.converters.base import ConverterContext, BaseConverter
|
|
2
|
+
from ssc_codegen.converters import py_helpers
|
|
2
3
|
|
|
3
4
|
# types
|
|
4
5
|
from ssc_codegen.ast import VariableType, StructType
|
|
@@ -22,14 +23,12 @@ from ssc_codegen.parsers import (
|
|
|
22
23
|
from ssc_codegen.ast import (
|
|
23
24
|
Docstring,
|
|
24
25
|
Imports,
|
|
25
|
-
Module,
|
|
26
26
|
Utilities,
|
|
27
27
|
JsonDef,
|
|
28
28
|
JsonDefField,
|
|
29
29
|
TypeDef,
|
|
30
30
|
TypeDefField,
|
|
31
31
|
Struct,
|
|
32
|
-
PlaceholderSpec,
|
|
33
32
|
)
|
|
34
33
|
|
|
35
34
|
# struct layer
|
|
@@ -177,28 +176,6 @@ def pre_docstring(node: Docstring, _: ConverterContext):
|
|
|
177
176
|
]
|
|
178
177
|
|
|
179
178
|
|
|
180
|
-
def _module_has_rest(node) -> bool:
|
|
181
|
-
"""True if the given node's Module has any `struct type=rest`."""
|
|
182
|
-
module = node
|
|
183
|
-
while module is not None and not isinstance(module, Module):
|
|
184
|
-
module = getattr(module, "parent", None)
|
|
185
|
-
if module is None:
|
|
186
|
-
return False
|
|
187
|
-
return any(
|
|
188
|
-
isinstance(n, Struct) and n.is_rest for n in getattr(module, "body", [])
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def rest_imports(node) -> list[str]:
|
|
193
|
-
"""Extra imports required when the module has any `struct type=rest`."""
|
|
194
|
-
if not _module_has_rest(node):
|
|
195
|
-
return []
|
|
196
|
-
return [
|
|
197
|
-
"from dataclasses import dataclass, field",
|
|
198
|
-
"from typing import Generic, Literal, Mapping, TypeVar",
|
|
199
|
-
]
|
|
200
|
-
|
|
201
|
-
|
|
202
179
|
@PY_BASE_CONVERTER(Imports)
|
|
203
180
|
def pre_imports(node: Imports, _: ConverterContext):
|
|
204
181
|
base_imports = [
|
|
@@ -208,7 +185,7 @@ def pre_imports(node: Imports, _: ConverterContext):
|
|
|
208
185
|
"from typing import TypedDict, Optional, Any, List, Dict, Union",
|
|
209
186
|
"from html import unescape as _html_unescape",
|
|
210
187
|
]
|
|
211
|
-
base_imports.extend(rest_imports(node))
|
|
188
|
+
base_imports.extend(py_helpers.rest_imports(node))
|
|
212
189
|
|
|
213
190
|
# Get transform imports for Python (already collected during parsing)
|
|
214
191
|
transform_imports = sorted(node.transform_imports.get("py", set()))
|
|
@@ -216,16 +193,6 @@ def pre_imports(node: Imports, _: ConverterContext):
|
|
|
216
193
|
return base_imports + transform_imports
|
|
217
194
|
|
|
218
195
|
|
|
219
|
-
def http_client_import(ctx: ConverterContext) -> list[str]:
|
|
220
|
-
"""Return the import line(s) for the chosen HTTP client, or [] if none."""
|
|
221
|
-
client = ctx.meta.get("http_client", "")
|
|
222
|
-
if client == "requests":
|
|
223
|
-
return ["import requests"]
|
|
224
|
-
if client == "httpx":
|
|
225
|
-
return ["import httpx"]
|
|
226
|
-
return []
|
|
227
|
-
|
|
228
|
-
|
|
229
196
|
# hook for add extra dependencies
|
|
230
197
|
@PY_BASE_CONVERTER.post(Imports)
|
|
231
198
|
def post_imports(node: Imports, ctx: ConverterContext):
|
|
@@ -233,7 +200,7 @@ def post_imports(node: Imports, ctx: ConverterContext):
|
|
|
233
200
|
"from bs4 import BeautifulSoup, ResultSet, Tag",
|
|
234
201
|
"BS4_FEATURES = 'lxml'",
|
|
235
202
|
]
|
|
236
|
-
lines.extend(http_client_import(ctx))
|
|
203
|
+
lines.extend(py_helpers.http_client_import(ctx))
|
|
237
204
|
return lines
|
|
238
205
|
|
|
239
206
|
|
|
@@ -285,55 +252,10 @@ def pre_utilities(node: Utilities, _: ConverterContext):
|
|
|
285
252
|
"UNMATCHED_TABLE_ROW = _UnmatchedTableRow()",
|
|
286
253
|
"\n\n",
|
|
287
254
|
]
|
|
288
|
-
lines.extend(rest_utilities(node))
|
|
255
|
+
lines.extend(py_helpers.rest_utilities(node))
|
|
289
256
|
return lines
|
|
290
257
|
|
|
291
258
|
|
|
292
|
-
def rest_utilities(node) -> list[str]:
|
|
293
|
-
"""Ok/Err/UnknownErr/TransportErr + _parse_response block; empty if no REST."""
|
|
294
|
-
if not _module_has_rest(node):
|
|
295
|
-
return []
|
|
296
|
-
return [
|
|
297
|
-
"_T = TypeVar('_T')",
|
|
298
|
-
"_E = TypeVar('_E')",
|
|
299
|
-
"\n",
|
|
300
|
-
"@dataclass(frozen=True)",
|
|
301
|
-
"class Ok(Generic[_T]):",
|
|
302
|
-
" status: int = 0",
|
|
303
|
-
" headers: Mapping[str, str] = field(default_factory=dict)",
|
|
304
|
-
" value: _T = None # type: ignore[assignment]",
|
|
305
|
-
" is_ok: Literal[True] = True",
|
|
306
|
-
"\n",
|
|
307
|
-
"@dataclass(frozen=True)",
|
|
308
|
-
"class Err(Generic[_E]):",
|
|
309
|
-
" status: int = 0",
|
|
310
|
-
" headers: Mapping[str, str] = field(default_factory=dict)",
|
|
311
|
-
" value: _E = None # type: ignore[assignment]",
|
|
312
|
-
" is_ok: Literal[False] = False",
|
|
313
|
-
"\n",
|
|
314
|
-
"@dataclass(frozen=True)",
|
|
315
|
-
"class UnknownErr(Err[Any]):",
|
|
316
|
-
" pass",
|
|
317
|
-
"\n",
|
|
318
|
-
"@dataclass(frozen=True)",
|
|
319
|
-
"class TransportErr(Err[None]):",
|
|
320
|
-
" status: Literal[0] = 0",
|
|
321
|
-
" cause: str = ''",
|
|
322
|
-
" value: None = None",
|
|
323
|
-
" headers: Mapping[str, str] = field(default_factory=dict)",
|
|
324
|
-
"\n\n",
|
|
325
|
-
"def _parse_response(_resp):",
|
|
326
|
-
" _status = _resp.status_code",
|
|
327
|
-
" _headers = {k.lower(): v for k, v in _resp.headers.items()}",
|
|
328
|
-
" try:",
|
|
329
|
-
" _body = _resp.json()",
|
|
330
|
-
" except Exception:",
|
|
331
|
-
" _body = None",
|
|
332
|
-
" return _status, _headers, _body",
|
|
333
|
-
"\n\n",
|
|
334
|
-
]
|
|
335
|
-
|
|
336
|
-
|
|
337
259
|
@PY_BASE_CONVERTER(JsonDef, post_callback="})")
|
|
338
260
|
def pre_json_struct(node: JsonDef, _: ConverterContext):
|
|
339
261
|
name = to_pascal_case(node.name)
|
|
@@ -394,108 +316,6 @@ def pre_typedef_field(node: TypeDefField, ctx: ConverterContext):
|
|
|
394
316
|
return [f"{ctx.indent}{name}: {type_}"]
|
|
395
317
|
|
|
396
318
|
|
|
397
|
-
def _err_subclass_name(struct_name: str, err: ErrorResponse) -> str:
|
|
398
|
-
"""Naming: `<Struct>Err<Status>[<FieldPascal>]`."""
|
|
399
|
-
base = f"{to_pascal_case(struct_name)}Err{err.status}"
|
|
400
|
-
if err.discriminator_field:
|
|
401
|
-
base += to_pascal_case(err.discriminator_field)
|
|
402
|
-
return base
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def _err_value_type(err: ErrorResponse, struct: Struct) -> str:
|
|
406
|
-
"""Return the typed value annotation for an Err subclass."""
|
|
407
|
-
schema = err.schema_name
|
|
408
|
-
if not schema:
|
|
409
|
-
return "Any"
|
|
410
|
-
type_name = f"{to_pascal_case(schema)}Json"
|
|
411
|
-
module = struct.parent
|
|
412
|
-
if module is not None:
|
|
413
|
-
for n in module.body:
|
|
414
|
-
if isinstance(n, JsonDef) and n.name == schema and n.is_array:
|
|
415
|
-
return f"List[{type_name}]"
|
|
416
|
-
return type_name
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def _rest_err_union_type(struct: Struct) -> str:
|
|
420
|
-
"""Return the typed union of all Err variants for _dispatch_err sig."""
|
|
421
|
-
variants: list[str] = []
|
|
422
|
-
seen: set[str] = set()
|
|
423
|
-
for err in struct.errors:
|
|
424
|
-
cls_name = _err_subclass_name(struct.name, err)
|
|
425
|
-
if cls_name not in seen:
|
|
426
|
-
seen.add(cls_name)
|
|
427
|
-
variants.append(cls_name)
|
|
428
|
-
return "Union[" + ", ".join([*variants, "UnknownErr", "None"]) + "]"
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def _emit_dispatch_err_py(node: Struct, ctx: ConverterContext) -> list[str]:
|
|
432
|
-
"""Emit the `_dispatch_err` @staticmethod lines inside a REST class.
|
|
433
|
-
|
|
434
|
-
ctx arrives from StructDocstring — already at class-body depth.
|
|
435
|
-
"""
|
|
436
|
-
i1 = ctx.indent # class-body level
|
|
437
|
-
i2 = i1 + ctx.indent_char # method body
|
|
438
|
-
i3 = i2 + ctx.indent_char # nested (if ...:)
|
|
439
|
-
i4 = i3 + ctx.indent_char
|
|
440
|
-
|
|
441
|
-
errors = node.errors
|
|
442
|
-
status_errors = [e for e in errors if e.discriminator_field is None]
|
|
443
|
-
field_errors = [e for e in errors if e.discriminator_field is not None]
|
|
444
|
-
union_type = _rest_err_union_type(node)
|
|
445
|
-
|
|
446
|
-
lines: list[str] = [
|
|
447
|
-
f"{i1}@staticmethod",
|
|
448
|
-
f"{i1}def _dispatch_err("
|
|
449
|
-
f"_status: int, _headers: Mapping[str, str], _body: Any"
|
|
450
|
-
f") -> {union_type}:",
|
|
451
|
-
f"{i2}if 200 <= _status < 300:",
|
|
452
|
-
]
|
|
453
|
-
|
|
454
|
-
# 2xx field discriminators check first, inside the 2xx branch
|
|
455
|
-
if field_errors:
|
|
456
|
-
lines.append(f"{i3}if isinstance(_body, dict):")
|
|
457
|
-
emitted_field_branch = False
|
|
458
|
-
for err in field_errors:
|
|
459
|
-
if 200 <= err.status < 300:
|
|
460
|
-
cls_name = _err_subclass_name(node.name, err)
|
|
461
|
-
lines.append(
|
|
462
|
-
f"{i4}if _status == {err.status} "
|
|
463
|
-
f"and {err.discriminator_field!r} in _body:"
|
|
464
|
-
)
|
|
465
|
-
lines.append(
|
|
466
|
-
f"{i4}{ctx.indent_char}return {cls_name}("
|
|
467
|
-
f"headers=_headers, value=_body)"
|
|
468
|
-
)
|
|
469
|
-
emitted_field_branch = True
|
|
470
|
-
if not emitted_field_branch:
|
|
471
|
-
# no 2xx-field errors → drop the isinstance guard
|
|
472
|
-
lines.pop()
|
|
473
|
-
lines.append(f"{i3}return None")
|
|
474
|
-
|
|
475
|
-
# non-2xx status routing
|
|
476
|
-
for err in status_errors:
|
|
477
|
-
cls_name = _err_subclass_name(node.name, err)
|
|
478
|
-
lines.append(f"{i2}if _status == {err.status}:")
|
|
479
|
-
lines.append(f"{i3}return {cls_name}(headers=_headers, value=_body)")
|
|
480
|
-
# non-2xx field discriminators (unusual but supported)
|
|
481
|
-
for err in field_errors:
|
|
482
|
-
if not (200 <= err.status < 300):
|
|
483
|
-
cls_name = _err_subclass_name(node.name, err)
|
|
484
|
-
lines.append(
|
|
485
|
-
f"{i2}if _status == {err.status} "
|
|
486
|
-
f"and isinstance(_body, dict) "
|
|
487
|
-
f"and {err.discriminator_field!r} in _body:"
|
|
488
|
-
)
|
|
489
|
-
lines.append(
|
|
490
|
-
f"{i3}return {cls_name}(headers=_headers, value=_body)"
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
lines.append(
|
|
494
|
-
f"{i2}return UnknownErr(status=_status, headers=_headers, value=_body)"
|
|
495
|
-
)
|
|
496
|
-
return lines
|
|
497
|
-
|
|
498
|
-
|
|
499
319
|
@PY_BASE_CONVERTER(Struct)
|
|
500
320
|
def pre_struct(node: Struct, ctx: ConverterContext):
|
|
501
321
|
name = to_pascal_case(node.name)
|
|
@@ -503,15 +323,19 @@ def pre_struct(node: Struct, ctx: ConverterContext):
|
|
|
503
323
|
if node.is_rest:
|
|
504
324
|
seen: set[str] = set()
|
|
505
325
|
for err in node.errors:
|
|
506
|
-
cls_name = _err_subclass_name(node.name, err)
|
|
326
|
+
cls_name = py_helpers._err_subclass_name(node.name, err)
|
|
507
327
|
if cls_name in seen:
|
|
508
328
|
continue
|
|
509
329
|
seen.add(cls_name)
|
|
510
|
-
value_type = _err_value_type(err, node)
|
|
330
|
+
value_type = py_helpers._err_value_type(err, node)
|
|
511
331
|
lines.append("@dataclass(frozen=True)")
|
|
512
332
|
lines.append(f"class {cls_name}(Err[{value_type}]):")
|
|
513
333
|
lines.append(f" status: Literal[{err.status}] = {err.status}")
|
|
514
334
|
lines.append("")
|
|
335
|
+
alias_lines = py_helpers._emit_result_aliases(node)
|
|
336
|
+
if alias_lines:
|
|
337
|
+
lines.extend(alias_lines)
|
|
338
|
+
lines.append("")
|
|
515
339
|
lines.append(f"class {name}:")
|
|
516
340
|
return lines
|
|
517
341
|
|
|
@@ -523,7 +347,7 @@ def post_struct_docstring(node: StructDocstring, ctx: ConverterContext):
|
|
|
523
347
|
parent = node.parent
|
|
524
348
|
if not isinstance(parent, Struct) or not parent.is_rest:
|
|
525
349
|
return None
|
|
526
|
-
return _emit_dispatch_err_py(parent, ctx)
|
|
350
|
+
return py_helpers._emit_dispatch_err_py(parent, ctx)
|
|
527
351
|
|
|
528
352
|
|
|
529
353
|
@PY_BASE_CONVERTER(StructDocstring)
|
|
@@ -1614,147 +1438,6 @@ def pre_expr_pred_re_any(node: PredReAny, ctx: ConverterContext):
|
|
|
1614
1438
|
return ctx.indent + f"and {cond}"
|
|
1615
1439
|
|
|
1616
1440
|
|
|
1617
|
-
def _build_request_method(
|
|
1618
|
-
*,
|
|
1619
|
-
name: str,
|
|
1620
|
-
is_async: bool,
|
|
1621
|
-
client_type: str,
|
|
1622
|
-
struct_name: str,
|
|
1623
|
-
call_args: list[str],
|
|
1624
|
-
ph_params: str,
|
|
1625
|
-
pre_lines: list[str],
|
|
1626
|
-
response_path: str,
|
|
1627
|
-
response_join: str,
|
|
1628
|
-
i1: str,
|
|
1629
|
-
i2: str,
|
|
1630
|
-
i3: str,
|
|
1631
|
-
) -> list[str]:
|
|
1632
|
-
async_kw = "async " if is_async else ""
|
|
1633
|
-
await_kw = "await " if is_async else ""
|
|
1634
|
-
lines: list[str] = [
|
|
1635
|
-
f"{i1}@classmethod",
|
|
1636
|
-
f'{i1}{async_kw}def {name}(cls, client: {client_type}{ph_params}) -> "{struct_name}":',
|
|
1637
|
-
]
|
|
1638
|
-
lines.extend(pre_lines)
|
|
1639
|
-
lines.append(f"{i2}_resp = {await_kw}client.request(")
|
|
1640
|
-
lines.extend(f"{i3}{a}" for a in call_args)
|
|
1641
|
-
lines.append(f"{i2})")
|
|
1642
|
-
lines.append(f"{i2}_resp.raise_for_status()")
|
|
1643
|
-
if response_path:
|
|
1644
|
-
accessor = "".join(f"[{p!r}]" for p in response_path.split("."))
|
|
1645
|
-
lines.append(f"{i2}_data = _resp.json()")
|
|
1646
|
-
if response_join:
|
|
1647
|
-
lines.append(f"{i2}_body = {response_join!r}.join(_data{accessor})")
|
|
1648
|
-
else:
|
|
1649
|
-
lines.append(f"{i2}_body = _data{accessor}")
|
|
1650
|
-
else:
|
|
1651
|
-
lines.append(f"{i2}_body = _resp.text")
|
|
1652
|
-
lines.append(f"{i2}return cls(_body)")
|
|
1653
|
-
return lines
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
_PY_PRIM_ANNO = {"str": "str", "int": "int", "float": "float", "bool": "bool"}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
def _ph_to_py_annotation(ph: PlaceholderSpec) -> tuple[str, str]:
|
|
1660
|
-
"""Return (annotation, default_suffix). Suffix is '' or ' = None'."""
|
|
1661
|
-
anno = _PY_PRIM_ANNO[ph.type_name]
|
|
1662
|
-
if ph.is_array:
|
|
1663
|
-
anno = f"List[{anno}]"
|
|
1664
|
-
if ph.is_optional:
|
|
1665
|
-
return f"Optional[{anno}]", " = None"
|
|
1666
|
-
return anno, ""
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
def _render_signature_params(placeholders: list[PlaceholderSpec]) -> str:
|
|
1670
|
-
"""Build keyword-only parameters clause: ', *, a: int, b: Optional[str] = None'."""
|
|
1671
|
-
if not placeholders:
|
|
1672
|
-
return ""
|
|
1673
|
-
required = [p for p in placeholders if not p.is_optional]
|
|
1674
|
-
optional = [p for p in placeholders if p.is_optional]
|
|
1675
|
-
parts: list[str] = []
|
|
1676
|
-
for ph in required + optional:
|
|
1677
|
-
anno, default = _ph_to_py_annotation(ph)
|
|
1678
|
-
parts.append(f"{ph.name}: {anno}{default}")
|
|
1679
|
-
return ", *, " + ", ".join(parts)
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
def _resolve_ok_payload_type(node: RequestConfig) -> str:
|
|
1683
|
-
"""Return the Python type annotation for the successful payload."""
|
|
1684
|
-
if not node.response_schema:
|
|
1685
|
-
return "None"
|
|
1686
|
-
struct = node.parent
|
|
1687
|
-
module = struct.parent if struct is not None else None
|
|
1688
|
-
schema_type = f"{to_pascal_case(node.response_schema)}Json"
|
|
1689
|
-
if module is not None:
|
|
1690
|
-
for n in module.body:
|
|
1691
|
-
if isinstance(n, JsonDef) and n.name == node.response_schema:
|
|
1692
|
-
if n.is_array:
|
|
1693
|
-
return f"List[{schema_type}]"
|
|
1694
|
-
break
|
|
1695
|
-
return schema_type
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
def _resolve_response_type(node: RequestConfig) -> str:
|
|
1699
|
-
"""Return the full Result-union annotation for a REST method."""
|
|
1700
|
-
payload = _resolve_ok_payload_type(node)
|
|
1701
|
-
struct = node.parent
|
|
1702
|
-
err_variants: list[str] = []
|
|
1703
|
-
seen: set[str] = set()
|
|
1704
|
-
if isinstance(struct, Struct):
|
|
1705
|
-
for err in struct.errors:
|
|
1706
|
-
cls_name = _err_subclass_name(struct.name, err)
|
|
1707
|
-
if cls_name not in seen:
|
|
1708
|
-
seen.add(cls_name)
|
|
1709
|
-
err_variants.append(cls_name)
|
|
1710
|
-
parts = [f"Ok[{payload}]", *err_variants, "UnknownErr", "TransportErr"]
|
|
1711
|
-
return "Union[" + ", ".join(parts) + "]"
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
def _build_rest_method(
|
|
1715
|
-
*,
|
|
1716
|
-
node: RequestConfig,
|
|
1717
|
-
name: str,
|
|
1718
|
-
is_async: bool,
|
|
1719
|
-
client_type: str,
|
|
1720
|
-
call_args: list[str],
|
|
1721
|
-
ph_params: str,
|
|
1722
|
-
pre_lines: list[str],
|
|
1723
|
-
ret_type: str,
|
|
1724
|
-
transport_exc: str,
|
|
1725
|
-
i1: str,
|
|
1726
|
-
i2: str,
|
|
1727
|
-
i3: str,
|
|
1728
|
-
i4: str,
|
|
1729
|
-
) -> list[str]:
|
|
1730
|
-
async_kw = "async " if is_async else ""
|
|
1731
|
-
await_kw = "await " if is_async else ""
|
|
1732
|
-
|
|
1733
|
-
lines: list[str] = [
|
|
1734
|
-
f"{i1}@classmethod",
|
|
1735
|
-
f"{i1}{async_kw}def {name}(cls, client: {client_type}"
|
|
1736
|
-
f"{ph_params}) -> {ret_type}:",
|
|
1737
|
-
]
|
|
1738
|
-
if node.doc:
|
|
1739
|
-
lines.append(f'{i2}"""{node.doc}"""')
|
|
1740
|
-
lines.extend(pre_lines)
|
|
1741
|
-
lines.append(f"{i2}try:")
|
|
1742
|
-
lines.append(f"{i3}_resp = {await_kw}client.request(")
|
|
1743
|
-
lines.extend(f"{i4}{a}" for a in call_args)
|
|
1744
|
-
lines.append(f"{i3})")
|
|
1745
|
-
lines.append(f"{i2}except {transport_exc} as _exc:")
|
|
1746
|
-
lines.append(f"{i3}return TransportErr(cause=repr(_exc))")
|
|
1747
|
-
lines.append(f"{i2}_status, _headers, _body = _parse_response(_resp)")
|
|
1748
|
-
lines.append(f"{i2}_err = cls._dispatch_err(_status, _headers, _body)")
|
|
1749
|
-
lines.append(f"{i2}if _err is not None:")
|
|
1750
|
-
lines.append(f"{i3}return _err")
|
|
1751
|
-
ok_value = "_body" if node.response_schema else "None"
|
|
1752
|
-
lines.append(
|
|
1753
|
-
f"{i2}return Ok(status=_status, headers=_headers, value={ok_value})"
|
|
1754
|
-
)
|
|
1755
|
-
return lines
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
1441
|
@PY_BASE_CONVERTER(RequestConfig)
|
|
1759
1442
|
def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
1760
1443
|
spec = normalize_placeholder_names(
|
|
@@ -1770,7 +1453,7 @@ def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
|
1770
1453
|
|
|
1771
1454
|
is_rest = isinstance(node.parent, Struct) and node.parent.is_rest
|
|
1772
1455
|
struct_name = to_pascal_case(node.parent.name)
|
|
1773
|
-
ph_params = _render_signature_params(spec.placeholders)
|
|
1456
|
+
ph_params = py_helpers._render_signature_params(spec.placeholders)
|
|
1774
1457
|
|
|
1775
1458
|
# Pre-lines: emitted inside the method body before `client.request(...)`
|
|
1776
1459
|
# to build local _headers/_cookies/_params dicts when dict_needs_builder().
|
|
@@ -1798,7 +1481,7 @@ def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
|
1798
1481
|
# REST method naming: kebab-case name → snake_case, "" → "fetch"
|
|
1799
1482
|
method_name = to_snake_case(node.name) if node.name else "fetch"
|
|
1800
1483
|
async_method_name = f"async_{method_name}"
|
|
1801
|
-
ret_type = _resolve_response_type(node)
|
|
1484
|
+
ret_type = py_helpers._resolve_response_type(node)
|
|
1802
1485
|
|
|
1803
1486
|
if http_client == "httpx":
|
|
1804
1487
|
transport_exc = "httpx.HTTPError"
|
|
@@ -1819,7 +1502,7 @@ def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
|
1819
1502
|
)
|
|
1820
1503
|
|
|
1821
1504
|
if http_client == "httpx":
|
|
1822
|
-
lines = _build_rest_method(
|
|
1505
|
+
lines = py_helpers._build_rest_method(
|
|
1823
1506
|
name=method_name,
|
|
1824
1507
|
is_async=False,
|
|
1825
1508
|
client_type="httpx.Client",
|
|
@@ -1827,7 +1510,7 @@ def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
|
1827
1510
|
)
|
|
1828
1511
|
lines.append("")
|
|
1829
1512
|
lines.extend(
|
|
1830
|
-
_build_rest_method(
|
|
1513
|
+
py_helpers._build_rest_method(
|
|
1831
1514
|
name=async_method_name,
|
|
1832
1515
|
is_async=True,
|
|
1833
1516
|
client_type="httpx.AsyncClient",
|
|
@@ -1836,7 +1519,7 @@ def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
|
1836
1519
|
)
|
|
1837
1520
|
return lines
|
|
1838
1521
|
|
|
1839
|
-
return _build_rest_method(
|
|
1522
|
+
return py_helpers._build_rest_method(
|
|
1840
1523
|
name=method_name,
|
|
1841
1524
|
is_async=False,
|
|
1842
1525
|
client_type="requests.Session",
|
|
@@ -1861,7 +1544,7 @@ def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
|
1861
1544
|
)
|
|
1862
1545
|
|
|
1863
1546
|
if http_client == "httpx":
|
|
1864
|
-
lines = _build_request_method(
|
|
1547
|
+
lines = py_helpers._build_request_method(
|
|
1865
1548
|
name=fetch_name,
|
|
1866
1549
|
is_async=False,
|
|
1867
1550
|
client_type="httpx.Client",
|
|
@@ -1869,7 +1552,7 @@ def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
|
1869
1552
|
)
|
|
1870
1553
|
lines.append("")
|
|
1871
1554
|
lines.extend(
|
|
1872
|
-
_build_request_method(
|
|
1555
|
+
py_helpers._build_request_method(
|
|
1873
1556
|
name=async_fetch_name,
|
|
1874
1557
|
is_async=True,
|
|
1875
1558
|
client_type="httpx.AsyncClient",
|
|
@@ -1879,7 +1562,7 @@ def pre_request_config(node: RequestConfig, ctx: ConverterContext):
|
|
|
1879
1562
|
return lines
|
|
1880
1563
|
|
|
1881
1564
|
# requests (default)
|
|
1882
|
-
return _build_request_method(
|
|
1565
|
+
return py_helpers._build_request_method(
|
|
1883
1566
|
name=fetch_name,
|
|
1884
1567
|
is_async=False,
|
|
1885
1568
|
client_type="requests.Session",
|