ssc_codegen 0.33.0__tar.gz → 0.35.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.33.0 → ssc_codegen-0.35.0}/PKG-INFO +1 -1
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/pyproject.toml +1 -1
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/__init__.py +4 -0
- ssc_codegen-0.35.0/ssc_codegen/ast/function.py +28 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/expressions.py +4 -2
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/linter.py +65 -1
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/module_handler.py +30 -1
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/reader.py +6 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/struct_parser.py +42 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/rest.py +27 -19
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/runtime.py +14 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/visitor.py +30 -3
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/javascript/rest.py +50 -23
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/javascript/visitor.py +26 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/rest.py +58 -37
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/visitor.py +26 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/traversal/utils.py +9 -1
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/traversal/walker.py +4 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/.gitignore +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/LICENSE +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/README.md +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/_logging.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/array.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/base.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/cast.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/control.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/extract.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/helpers.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/jsondef.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/module.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/predicate_containers.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/predicate_ops.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/regex.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/rest.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/selectors.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/string.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/struct.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/typedef.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/ast/types.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/contexts.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/format.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/predicates.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/rest_artifacts.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/core/type_checking.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/exceptions.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/explore.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/generation/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/generation/builder.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/generation/runtime.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/health.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/main.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/naming.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/parsers/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/parsers/curl.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/parsers/http.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/regex_utils.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/request_spec.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/http_libs/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/http_libs/base.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/http_libs/nethttp.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/literals.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/golang/regex.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/javascript/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/javascript/http_libs/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/javascript/http_libs/axios.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/javascript/http_libs/base.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/javascript/http_libs/fetch.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/profile.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/html_libs/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/html_libs/base.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/html_libs/bs4.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/html_libs/lxml.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/html_libs/parsel.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/html_libs/slax.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/http_libs/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/http_libs/aiohttp.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/http_libs/base.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/http_libs/httpx.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/python/http_libs/requests.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/resolver.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/spec.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/traversal/__init__.py +0 -0
- {ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/traversal/context.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssc_codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.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
|
|
@@ -20,6 +20,8 @@ from .module import (
|
|
|
20
20
|
Utilities,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
from .function import FunctionDef
|
|
24
|
+
|
|
23
25
|
from .typedef import TypeDef, TypeDefField
|
|
24
26
|
|
|
25
27
|
from .jsondef import JsonDef, JsonDefField
|
|
@@ -147,6 +149,8 @@ __all__ = [
|
|
|
147
149
|
"CodeEndHook",
|
|
148
150
|
"Docstring",
|
|
149
151
|
"Utilities",
|
|
152
|
+
# function
|
|
153
|
+
"FunctionDef",
|
|
150
154
|
# typedef
|
|
151
155
|
"TypeDef",
|
|
152
156
|
"TypeDefField",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .base import Node
|
|
6
|
+
from .types import TypeInfo, VariableType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class FunctionDef(Node):
|
|
11
|
+
"""Module-level single-value extraction function.
|
|
12
|
+
|
|
13
|
+
DSL: ``fn <name> { pipeline }`` or ``(raw)fn <name> { pipeline }``.
|
|
14
|
+
|
|
15
|
+
Generates a standalone function instead of a class.
|
|
16
|
+
``is_raw`` selects the document type: HTML (DOCUMENT) or plain text (STRING).
|
|
17
|
+
``ret_type_info`` is resolved after pipeline build (AUTO → concrete type).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str = ""
|
|
21
|
+
is_raw: bool = False
|
|
22
|
+
doc: str = ""
|
|
23
|
+
accept_type_info: TypeInfo = field(
|
|
24
|
+
default_factory=lambda: TypeInfo(base=VariableType.DOCUMENT)
|
|
25
|
+
)
|
|
26
|
+
ret_type_info: TypeInfo = field(
|
|
27
|
+
default_factory=lambda: TypeInfo(base=VariableType.AUTO)
|
|
28
|
+
)
|
|
@@ -19,6 +19,7 @@ from ssc_codegen.ast import (
|
|
|
19
19
|
Field,
|
|
20
20
|
Filter,
|
|
21
21
|
Fmt,
|
|
22
|
+
FunctionDef,
|
|
22
23
|
Index,
|
|
23
24
|
InitField,
|
|
24
25
|
JsonDef,
|
|
@@ -104,6 +105,7 @@ FieldLikeNode: TypeAlias = (
|
|
|
104
105
|
| Value
|
|
105
106
|
| Field
|
|
106
107
|
| InitField
|
|
108
|
+
| FunctionDef
|
|
107
109
|
)
|
|
108
110
|
|
|
109
111
|
|
|
@@ -480,8 +482,8 @@ def parse_expressions(
|
|
|
480
482
|
)
|
|
481
483
|
parent.ret_type_info = last_ti
|
|
482
484
|
|
|
483
|
-
# Populate type_info on Field/Value/InitField
|
|
484
|
-
if isinstance(parent, (Field, Value, InitField)):
|
|
485
|
+
# Populate type_info on Field/Value/InitField/FunctionDef
|
|
486
|
+
if isinstance(parent, (Field, Value, InitField, FunctionDef)):
|
|
485
487
|
last_ret = last_ti.base
|
|
486
488
|
is_arr = (
|
|
487
489
|
parent.body[-2].is_array if len(parent.body) >= 2 else False
|
|
@@ -168,6 +168,7 @@ def lint_module(
|
|
|
168
168
|
_lint_defines(doc, source_path, diags, children_defines)
|
|
169
169
|
_lint_json_defs(doc, source_path, diags, children_defines)
|
|
170
170
|
_lint_structs(doc, source_path, diags, children_defines)
|
|
171
|
+
_lint_fns(doc, source_path, diags)
|
|
171
172
|
_lint_nested_topdown(doc, source_path, diags)
|
|
172
173
|
_lint_json_refs_topdown(doc, source_path, diags, children_defines)
|
|
173
174
|
return diags
|
|
@@ -178,7 +179,7 @@ def _lint_top_level(
|
|
|
178
179
|
source_path: str,
|
|
179
180
|
diags: list[ReadDiagnostic],
|
|
180
181
|
) -> None:
|
|
181
|
-
for node in doc.select(":root:not(@doc, json, struct, define, import)"):
|
|
182
|
+
for node in doc.select(":root:not(@doc, json, struct, fn, define, import)"):
|
|
182
183
|
diags.append(
|
|
183
184
|
_error(
|
|
184
185
|
node,
|
|
@@ -378,6 +379,69 @@ def _lint_structs(
|
|
|
378
379
|
_lint_single_struct(node, source_path, diags, children_defines)
|
|
379
380
|
|
|
380
381
|
|
|
382
|
+
# Directives allowed inside ``fn`` / ``(raw)fn`` body.
|
|
383
|
+
_FN_ALLOWED_RESERVED: frozenset[str] = frozenset({"@doc"})
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _lint_fns(
|
|
387
|
+
doc: KdlDocument,
|
|
388
|
+
source_path: str,
|
|
389
|
+
diags: list[ReadDiagnostic],
|
|
390
|
+
) -> None:
|
|
391
|
+
"""Structural lint for ``fn`` / ``(raw)fn`` directives."""
|
|
392
|
+
for node in doc.select("fn:root"):
|
|
393
|
+
name = _node_arg(node, 0)
|
|
394
|
+
if not name:
|
|
395
|
+
diags.append(
|
|
396
|
+
_error(
|
|
397
|
+
node,
|
|
398
|
+
"'fn' requires a name",
|
|
399
|
+
source_path,
|
|
400
|
+
code="E001",
|
|
401
|
+
hint='example: fn page_title { css "h1" { text } }',
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
annotation = node.type_annotation
|
|
407
|
+
if annotation and annotation != "(raw)":
|
|
408
|
+
diags.append(
|
|
409
|
+
_error(
|
|
410
|
+
node,
|
|
411
|
+
f"unsupported fn annotation '{annotation}' — only '(raw)' is recognized",
|
|
412
|
+
source_path,
|
|
413
|
+
code="E400",
|
|
414
|
+
hint="use 'fn { ... }' (HTML) or '(raw)fn { ... }' (plain text)",
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
ops = list(node.children)
|
|
419
|
+
if not ops:
|
|
420
|
+
diags.append(
|
|
421
|
+
_error(
|
|
422
|
+
node,
|
|
423
|
+
f"fn '{name}' must contain at least one pipeline operation",
|
|
424
|
+
source_path,
|
|
425
|
+
code="E001",
|
|
426
|
+
hint='example: fn title { css "h1" { text } }',
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
for child in ops:
|
|
431
|
+
if (
|
|
432
|
+
child.name.startswith("@")
|
|
433
|
+
and child.name not in _FN_ALLOWED_RESERVED
|
|
434
|
+
):
|
|
435
|
+
diags.append(
|
|
436
|
+
_error(
|
|
437
|
+
child,
|
|
438
|
+
f"'{child.name}' is not allowed inside fn — use a struct for init/check/request/etc.",
|
|
439
|
+
source_path,
|
|
440
|
+
code="E203",
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
381
445
|
def _lint_nested_topdown(
|
|
382
446
|
doc: KdlDocument,
|
|
383
447
|
source_path: str,
|
|
@@ -5,12 +5,15 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from ssc_codegen.ast import (
|
|
8
|
+
FunctionDef,
|
|
8
9
|
JsonDef,
|
|
9
10
|
Module,
|
|
10
11
|
StructBase,
|
|
11
12
|
Struct,
|
|
12
13
|
StructRest,
|
|
13
14
|
StructType,
|
|
15
|
+
VariableType,
|
|
16
|
+
TypeInfo,
|
|
14
17
|
)
|
|
15
18
|
from ssc_codegen.exceptions import BuildTimeError
|
|
16
19
|
from kdlquery import KdlNode, parse as kdl_parse
|
|
@@ -23,7 +26,11 @@ from ssc_codegen.core.contexts import (
|
|
|
23
26
|
ParseContext,
|
|
24
27
|
)
|
|
25
28
|
from ssc_codegen.core.expressions import resolve_define_references
|
|
26
|
-
from ssc_codegen.core.struct_parser import
|
|
29
|
+
from ssc_codegen.core.struct_parser import (
|
|
30
|
+
parse_function,
|
|
31
|
+
parse_json_fields,
|
|
32
|
+
parse_struct,
|
|
33
|
+
)
|
|
27
34
|
|
|
28
35
|
_KDL_TEXT_ENCODING = "utf-8-sig"
|
|
29
36
|
|
|
@@ -65,6 +72,28 @@ def handle_struct(
|
|
|
65
72
|
return struct
|
|
66
73
|
|
|
67
74
|
|
|
75
|
+
def handle_function(
|
|
76
|
+
node: KdlNode, module: Module, ctx: ParseContext, lint: LintContext
|
|
77
|
+
) -> FunctionDef:
|
|
78
|
+
raw_annotation = node.type_annotation
|
|
79
|
+
is_raw = raw_annotation == "(raw)"
|
|
80
|
+
if not node.args:
|
|
81
|
+
raise BuildTimeError("'fn' requires a name")
|
|
82
|
+
name = str(node.args[0].value)
|
|
83
|
+
fn = FunctionDef(
|
|
84
|
+
parent=module,
|
|
85
|
+
name=name,
|
|
86
|
+
is_raw=is_raw,
|
|
87
|
+
accept_type_info=(
|
|
88
|
+
TypeInfo(base=VariableType.STRING)
|
|
89
|
+
if is_raw
|
|
90
|
+
else TypeInfo(base=VariableType.DOCUMENT)
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
parse_function(node.children, fn, ctx, lint)
|
|
94
|
+
return fn
|
|
95
|
+
|
|
96
|
+
|
|
68
97
|
def handle_json(
|
|
69
98
|
node: KdlNode, module: Module, ctx: ParseContext, lint: LintContext
|
|
70
99
|
) -> JsonDef:
|
|
@@ -16,6 +16,7 @@ from ssc_codegen.core.expressions import typedef_from_struct
|
|
|
16
16
|
from ssc_codegen.core.rest_artifacts import rest_artifacts_from_struct
|
|
17
17
|
from ssc_codegen.core.module_handler import (
|
|
18
18
|
handle_define,
|
|
19
|
+
handle_function,
|
|
19
20
|
handle_json,
|
|
20
21
|
handle_struct,
|
|
21
22
|
resolve_imports,
|
|
@@ -68,6 +69,7 @@ def parse_module(
|
|
|
68
69
|
module.source_file = source_path.name if source_path else ""
|
|
69
70
|
structs: list = []
|
|
70
71
|
typedefs: list = []
|
|
72
|
+
functions: list = []
|
|
71
73
|
|
|
72
74
|
for node in top_nodes:
|
|
73
75
|
lint.push(node.name)
|
|
@@ -79,6 +81,9 @@ def parse_module(
|
|
|
79
81
|
struct = handle_struct(node, module, ctx, lint)
|
|
80
82
|
typedefs.append(typedef_from_struct(struct, module))
|
|
81
83
|
structs.append(struct)
|
|
84
|
+
elif node.name == "fn":
|
|
85
|
+
fn = handle_function(node, module, ctx, lint)
|
|
86
|
+
functions.append(fn)
|
|
82
87
|
elif node.name in ("define", "import"):
|
|
83
88
|
pass # already handled
|
|
84
89
|
else:
|
|
@@ -95,6 +100,7 @@ def parse_module(
|
|
|
95
100
|
if isinstance(struct, StructRest):
|
|
96
101
|
module.body.extend(rest_artifacts_from_struct(struct, module))
|
|
97
102
|
module.body.append(struct)
|
|
103
|
+
module.body.extend(functions)
|
|
98
104
|
|
|
99
105
|
# merge lint diagnostics into walker diagnostics
|
|
100
106
|
diagnostics.extend(lint.diagnostics)
|
|
@@ -13,6 +13,7 @@ from ssc_codegen.ast import (
|
|
|
13
13
|
CssSelectAll,
|
|
14
14
|
ErrorResponse,
|
|
15
15
|
Field,
|
|
16
|
+
FunctionDef,
|
|
16
17
|
InitField,
|
|
17
18
|
InitFieldCall,
|
|
18
19
|
JsonDef,
|
|
@@ -313,6 +314,47 @@ def parse_struct(
|
|
|
313
314
|
lint.walk_context = prev_ctx
|
|
314
315
|
|
|
315
316
|
|
|
317
|
+
def parse_function(
|
|
318
|
+
kdl_nodes: Sequence[KdlNode],
|
|
319
|
+
fn: FunctionDef,
|
|
320
|
+
ctx: ParseContext,
|
|
321
|
+
lint: LintContext,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Parse the body of a ``fn`` / ``(raw)fn`` directive.
|
|
324
|
+
|
|
325
|
+
The body is a flat pipeline (same ops as a Field), optionally preceded
|
|
326
|
+
by ``@doc``. No struct-level directives (@init, @check, @split-doc, etc.)
|
|
327
|
+
are accepted — those are reported by the structural linter.
|
|
328
|
+
"""
|
|
329
|
+
prev_ctx = lint.walk_context
|
|
330
|
+
lint.walk_context = WalkCtx.PIPELINE
|
|
331
|
+
|
|
332
|
+
pipeline_nodes: list[KdlNode] = []
|
|
333
|
+
for node in kdl_nodes:
|
|
334
|
+
if node.name == "@doc":
|
|
335
|
+
fn.doc = str(node.args[0].value)
|
|
336
|
+
elif node.name.startswith("@"):
|
|
337
|
+
continue # structural linter already reported E203
|
|
338
|
+
else:
|
|
339
|
+
pipeline_nodes.append(node)
|
|
340
|
+
|
|
341
|
+
ops = pipeline_nodes
|
|
342
|
+
parse_expressions(ops, fn, ctx, lint)
|
|
343
|
+
if ops:
|
|
344
|
+
check_pipeline_types(
|
|
345
|
+
ops,
|
|
346
|
+
ctx,
|
|
347
|
+
lint,
|
|
348
|
+
start_type=VariableType.STRING
|
|
349
|
+
if fn.is_raw
|
|
350
|
+
else VariableType.DOCUMENT,
|
|
351
|
+
)
|
|
352
|
+
if fn.is_raw:
|
|
353
|
+
_lint_raw_forbidden_ops(fn, lint)
|
|
354
|
+
|
|
355
|
+
lint.walk_context = prev_ctx
|
|
356
|
+
|
|
357
|
+
|
|
316
358
|
def parse_json_fields(
|
|
317
359
|
nodes: Sequence[KdlNode], parent: JsonDef, ctx: ParseContext
|
|
318
360
|
) -> None:
|
|
@@ -206,7 +206,7 @@ def emit_method_fetch(
|
|
|
206
206
|
i2 = i1 + ctx.indent_char
|
|
207
207
|
|
|
208
208
|
lines: list[str] = [
|
|
209
|
-
f"{i1}func {method_name}(ctx context.Context, client {http.client_type}{ph_params}) (*{name}, error) {{",
|
|
209
|
+
f"{i1}func {method_name}(ctx context.Context, client {http.client_type}{ph_params}, opts ...sscReqOpt) (*{name}, error) {{",
|
|
210
210
|
f"{i2}req, err := http.NewRequestWithContext(ctx, {_go_str(spec.method)}, {url_expr}, nil)",
|
|
211
211
|
f"{i2}if err != nil {{",
|
|
212
212
|
f"{i2}\treturn nil, err",
|
|
@@ -233,6 +233,15 @@ def emit_method_fetch(
|
|
|
233
233
|
f"{i2}req.Body = io.NopCloser(strings.NewReader({body_expr}))"
|
|
234
234
|
)
|
|
235
235
|
|
|
236
|
+
# Apply per-call opts (user headers are additive).
|
|
237
|
+
lines.append(f"{i2}if len(opts) > 0 {{")
|
|
238
|
+
lines.append(f"{i2}\t_opts := &sscReqOpts{{}}")
|
|
239
|
+
lines.append(f"{i2}\tfor _, o := range opts {{ o(_opts) }}")
|
|
240
|
+
lines.append(f"{i2}\tfor _k, _vs := range _opts.Headers {{")
|
|
241
|
+
lines.append(f"{i2}\t\tfor _, _v := range _vs {{ req.Header.Add(_k, _v) }}")
|
|
242
|
+
lines.append(f"{i2}\t}}")
|
|
243
|
+
lines.append(f"{i2}}}")
|
|
244
|
+
|
|
236
245
|
# Execute.
|
|
237
246
|
lines.extend(
|
|
238
247
|
[
|
|
@@ -325,29 +334,28 @@ def emit_method_rest(
|
|
|
325
334
|
|
|
326
335
|
# Function signature.
|
|
327
336
|
lines.append(
|
|
328
|
-
f"{i1}func {method_name}(client {http.client_type}{ph_params}) ({ret_type}, error) {{"
|
|
337
|
+
f"{i1}func {method_name}(client {http.client_type}{ph_params}, opts ...sscReqOpt) ({ret_type}, error) {{"
|
|
329
338
|
)
|
|
330
339
|
|
|
331
|
-
# Build opts.
|
|
332
|
-
opts_lines = _build_opts(spec, i2)
|
|
333
|
-
|
|
334
|
-
# Call sscRestCall — inline if nil, multi-line if struct literal.
|
|
335
|
-
# For void responses, use `_` since the body is discarded.
|
|
340
|
+
# Build _opts from DSL, apply per-call user opts, then call sscRestCall.
|
|
341
|
+
opts_lines = _build_opts(spec, i2 + "\t")
|
|
336
342
|
body_var = "_" if not node.response_schema else "body"
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
)
|
|
343
|
+
|
|
344
|
+
# Assign DSL opts to _opts variable.
|
|
345
|
+
if len(opts_lines) == 1 and opts_lines[0].strip() == "nil":
|
|
346
|
+
lines.append(f"{i2}_opts := &sscReqOpts{{}}")
|
|
342
347
|
else:
|
|
343
|
-
lines.append(
|
|
344
|
-
|
|
345
|
-
f"{_go_str(spec.method)}, {url_expr},"
|
|
346
|
-
)
|
|
347
|
-
lines.append(f"{opts_lines[0].strip()}")
|
|
348
|
-
for ol in opts_lines[1:-1]:
|
|
348
|
+
lines.append(f"{i2}_opts := {opts_lines[0].strip()}")
|
|
349
|
+
for ol in opts_lines[1:]:
|
|
349
350
|
lines.append(ol)
|
|
350
|
-
|
|
351
|
+
|
|
352
|
+
# Apply per-call opts (functional options).
|
|
353
|
+
lines.append(f"{i2}for _, o := range opts {{ o(_opts) }}")
|
|
354
|
+
|
|
355
|
+
lines.append(
|
|
356
|
+
f"{i2}{body_var}, err := sscRestCall(client, {matchers_var}, "
|
|
357
|
+
f"{_go_str(spec.method)}, {url_expr}, _opts)"
|
|
358
|
+
)
|
|
351
359
|
|
|
352
360
|
# Error propagation.
|
|
353
361
|
lines.append(f"{i2}if err != nil {{")
|
|
@@ -493,6 +493,20 @@ BASE_REST_RUNTIME: list[str] = [
|
|
|
493
493
|
"\tBody string",
|
|
494
494
|
"}",
|
|
495
495
|
"",
|
|
496
|
+
"// sscReqOpt is a functional option for per-call request overrides.",
|
|
497
|
+
"// Applied after DSL-specified opts; user headers are additive (shallow-merge).",
|
|
498
|
+
"type sscReqOpt func(*sscReqOpts)",
|
|
499
|
+
"",
|
|
500
|
+
"// WithHeader adds a per-call header value (additive — does not replace DSL headers).",
|
|
501
|
+
"func WithHeader(key, val string) sscReqOpt {",
|
|
502
|
+
"\treturn func(o *sscReqOpts) {",
|
|
503
|
+
"\t\tif o.Headers == nil {",
|
|
504
|
+
"\t\t\to.Headers = sscHeaders{}",
|
|
505
|
+
"\t\t}",
|
|
506
|
+
"\t\to.Headers[key] = append(o.Headers[key], val)",
|
|
507
|
+
"\t}",
|
|
508
|
+
"}",
|
|
509
|
+
"",
|
|
496
510
|
"type sscErrMatcher struct {",
|
|
497
511
|
"\tStatus int",
|
|
498
512
|
"\tCheck func([]byte) bool",
|
|
@@ -35,6 +35,7 @@ from ssc_codegen.ast import (
|
|
|
35
35
|
Field,
|
|
36
36
|
Filter,
|
|
37
37
|
Fmt,
|
|
38
|
+
FunctionDef,
|
|
38
39
|
Init,
|
|
39
40
|
InitField,
|
|
40
41
|
InitFieldCall,
|
|
@@ -136,8 +137,8 @@ from ssc_codegen.naming import (
|
|
|
136
137
|
)
|
|
137
138
|
from ssc_codegen.traversal.utils import (
|
|
138
139
|
find_predicate_container,
|
|
140
|
+
module_has_html_struct,
|
|
139
141
|
module_has_rest,
|
|
140
|
-
module_is_rest_only,
|
|
141
142
|
module_uses_http,
|
|
142
143
|
)
|
|
143
144
|
from ssc_codegen.generation.builder import ModuleBuilder
|
|
@@ -439,10 +440,9 @@ class GoVisitor(BaseWalker):
|
|
|
439
440
|
lines.append(f"// {doc_line}" if doc_line else "//")
|
|
440
441
|
lines.append("")
|
|
441
442
|
|
|
442
|
-
is_rest_only = module_is_rest_only(node)
|
|
443
443
|
uses_http = module_uses_http(node)
|
|
444
444
|
|
|
445
|
-
if
|
|
445
|
+
if module_has_html_struct(node):
|
|
446
446
|
self._builder.require_import('"github.com/PuerkitoBio/goquery"')
|
|
447
447
|
self._builder.require_import('"strings"')
|
|
448
448
|
if uses_http:
|
|
@@ -842,6 +842,33 @@ class GoVisitor(BaseWalker):
|
|
|
842
842
|
lines.append("")
|
|
843
843
|
return lines
|
|
844
844
|
|
|
845
|
+
def visit_function_def(
|
|
846
|
+
self, node: FunctionDef, ctx: WalkContext
|
|
847
|
+
) -> list[str]:
|
|
848
|
+
name = to_pascal_case(node.name)
|
|
849
|
+
t_ret = self._resolve_type(node.ret_type_info)
|
|
850
|
+
inner = ctx.deeper()
|
|
851
|
+
lines: list[str] = []
|
|
852
|
+
if node.doc:
|
|
853
|
+
for doc_line in node.doc.splitlines():
|
|
854
|
+
lines.append(f"// {doc_line}")
|
|
855
|
+
lines.append(f"func {name}(document string) {t_ret} {{")
|
|
856
|
+
if node.is_raw:
|
|
857
|
+
lines.append(f"{inner.indent}{inner.var_name} := document")
|
|
858
|
+
else:
|
|
859
|
+
self._builder.require_import('"strings"')
|
|
860
|
+
lines.append(
|
|
861
|
+
f"{inner.indent}doc, err := goquery.NewDocumentFromReader(strings.NewReader(document))"
|
|
862
|
+
)
|
|
863
|
+
lines.append(f"{inner.indent}if err != nil {{")
|
|
864
|
+
lines.append(f"{inner.indent}\tpanic(err)")
|
|
865
|
+
lines.append(f"{inner.indent}}}")
|
|
866
|
+
lines.append(f"{inner.indent}{inner.var_name} := doc.Selection")
|
|
867
|
+
lines.extend(self.walk_children(node, ctx))
|
|
868
|
+
lines.append("}")
|
|
869
|
+
lines.append("")
|
|
870
|
+
return lines
|
|
871
|
+
|
|
845
872
|
def visit_split_doc(self, node: SplitDoc, ctx: WalkContext) -> list[str]:
|
|
846
873
|
struct = self._enclosing_struct_name(node)
|
|
847
874
|
rcv = _receiver(struct)
|
|
@@ -390,13 +390,22 @@ def emit_method_rest(
|
|
|
390
390
|
t = f"{t}[]"
|
|
391
391
|
bracket = f"[params.{p.name}]" if p.is_optional else f"params.{p.name}"
|
|
392
392
|
lines.append(f"{i1} * @param {{{t}}} {bracket}")
|
|
393
|
+
lines.append(f"{i1} * @param {{Object}} [opts] per-call request options (headers, etc.)")
|
|
393
394
|
lines.append(f"{i1} * @returns {{Promise<{return_union}>}}")
|
|
394
395
|
lines.append(f"{i1} */")
|
|
395
|
-
lines.append(f"{i1}static async {method_name}(client{ph_param}) {{")
|
|
396
|
+
lines.append(f"{i1}static async {method_name}(client{ph_param}, opts = {{}}) {{")
|
|
396
397
|
lines.extend(pre_lines)
|
|
398
|
+
lines.append(f"{i2}const _kw = {opts_obj};")
|
|
399
|
+
lines.append(f"{i2}for (const [_k, _v] of Object.entries(opts)) {{")
|
|
400
|
+
lines.append(f"{i2} if (typeof _kw[_k] === 'object' && _kw[_k] !== null && typeof _v === 'object' && _v !== null) {{")
|
|
401
|
+
lines.append(f"{i2} _kw[_k] = {{..._kw[_k], ..._v}};")
|
|
402
|
+
lines.append(f"{i2} }} else {{")
|
|
403
|
+
lines.append(f"{i2} _kw[_k] = _v;")
|
|
404
|
+
lines.append(f"{i2} }}")
|
|
405
|
+
lines.append(f"{i2}}}")
|
|
397
406
|
lines.append(
|
|
398
407
|
f"{i2}return {fn_name}(client, {matchers_var}, "
|
|
399
|
-
f"{spec.method!r}, {url_expr}, {value_fn},
|
|
408
|
+
f"{spec.method!r}, {url_expr}, {value_fn}, _kw);"
|
|
400
409
|
)
|
|
401
410
|
lines.append(f"{i1}}}")
|
|
402
411
|
return lines
|
|
@@ -434,9 +443,11 @@ def emit_method_fetch(
|
|
|
434
443
|
rl.append(f"{i2}return new {struct_name}(_body);")
|
|
435
444
|
return rl
|
|
436
445
|
|
|
437
|
-
lines: list[str] = [f"{i1}static async {method_name}(client{ph_param}) {{"]
|
|
446
|
+
lines: list[str] = [f"{i1}static async {method_name}(client{ph_param}, opts = {{}}) {{"]
|
|
438
447
|
lines.extend(pre_lines)
|
|
439
448
|
|
|
449
|
+
# Build _kw from DSL-specified request options (method/url stay out).
|
|
450
|
+
kw_parts: list[str] = []
|
|
440
451
|
if http_client == "fetch":
|
|
441
452
|
if params_expr:
|
|
442
453
|
url_inner = spec.url.map(
|
|
@@ -446,21 +457,30 @@ def emit_method_fetch(
|
|
|
446
457
|
url_expr = f"`{url_inner}?${{new URLSearchParams({params_expr})}}`"
|
|
447
458
|
else:
|
|
448
459
|
url_expr = render_value(spec.url)
|
|
449
|
-
options: list[str] = [f"{i3}method: {spec.method!r},"]
|
|
450
460
|
if headers_expr:
|
|
451
|
-
|
|
461
|
+
kw_parts.append(f"headers: {headers_expr}")
|
|
452
462
|
if spec.cookies:
|
|
453
463
|
cookie_str = "; ".join(
|
|
454
464
|
f"{k}={v.source}" for k, v in spec.cookies.items()
|
|
455
465
|
)
|
|
456
|
-
|
|
457
|
-
f"
|
|
466
|
+
kw_parts.append(
|
|
467
|
+
f"// cookies: {cookie_str!r} /* set via headers or credentials */"
|
|
458
468
|
)
|
|
459
469
|
if body_expr:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
lines.
|
|
463
|
-
lines.append(f"{i2}
|
|
470
|
+
kw_parts.append(f"body: {body_expr}")
|
|
471
|
+
kw_obj = "{" + ", ".join(kw_parts) + "}" if kw_parts else "{}"
|
|
472
|
+
lines.append(f"{i2}const _kw = {kw_obj};")
|
|
473
|
+
lines.append(f"{i2}for (const [_k, _v] of Object.entries(opts)) {{")
|
|
474
|
+
lines.append(f"{i2} if (typeof _kw[_k] === 'object' && _kw[_k] !== null && typeof _v === 'object' && _v !== null) {{")
|
|
475
|
+
lines.append(f"{i2} _kw[_k] = {{..._kw[_k], ..._v}};")
|
|
476
|
+
lines.append(f"{i2} }} else {{")
|
|
477
|
+
lines.append(f"{i2} _kw[_k] = _v;")
|
|
478
|
+
lines.append(f"{i2} }}")
|
|
479
|
+
lines.append(f"{i2}}}")
|
|
480
|
+
lines.append(
|
|
481
|
+
f"{i2}const _resp = await client({url_expr},"
|
|
482
|
+
f" {{ method: {spec.method!r}, ..._kw }});"
|
|
483
|
+
)
|
|
464
484
|
lines.append(
|
|
465
485
|
f"{i2}if (!_resp.ok) throw new Error(`HTTP ${{_resp.status}}`);"
|
|
466
486
|
)
|
|
@@ -469,23 +489,30 @@ def emit_method_fetch(
|
|
|
469
489
|
else:
|
|
470
490
|
lines.extend(_response_lines("await _resp.text()"))
|
|
471
491
|
else:
|
|
472
|
-
|
|
473
|
-
f"{i3}method: {spec.method!r},",
|
|
474
|
-
f"{i3}url: {render_value(spec.url)},",
|
|
475
|
-
]
|
|
492
|
+
url_expr = render_value(spec.url)
|
|
476
493
|
if params_expr:
|
|
477
|
-
|
|
494
|
+
kw_parts.append(f"params: {params_expr}")
|
|
478
495
|
if headers_expr:
|
|
479
|
-
|
|
496
|
+
kw_parts.append(f"headers: {headers_expr}")
|
|
480
497
|
if spec.cookies:
|
|
481
|
-
|
|
482
|
-
f"
|
|
498
|
+
kw_parts.append(
|
|
499
|
+
f"// cookies: {render_obj(spec.cookies)},"
|
|
483
500
|
)
|
|
484
501
|
if body_expr:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
lines.
|
|
488
|
-
lines.append(f"{i2}
|
|
502
|
+
kw_parts.append(f"data: {body_expr}")
|
|
503
|
+
kw_obj = "{" + ", ".join(kw_parts) + "}" if kw_parts else "{}"
|
|
504
|
+
lines.append(f"{i2}const _kw = {kw_obj};")
|
|
505
|
+
lines.append(f"{i2}for (const [_k, _v] of Object.entries(opts)) {{")
|
|
506
|
+
lines.append(f"{i2} if (typeof _kw[_k] === 'object' && _kw[_k] !== null && typeof _v === 'object' && _v !== null) {{")
|
|
507
|
+
lines.append(f"{i2} _kw[_k] = {{..._kw[_k], ..._v}};")
|
|
508
|
+
lines.append(f"{i2} }} else {{")
|
|
509
|
+
lines.append(f"{i2} _kw[_k] = _v;")
|
|
510
|
+
lines.append(f"{i2} }}")
|
|
511
|
+
lines.append(f"{i2}}}")
|
|
512
|
+
lines.append(
|
|
513
|
+
f"{i2}const _resp = await client.request({{"
|
|
514
|
+
f" method: {spec.method!r}, url: {url_expr}, ..._kw }});"
|
|
515
|
+
)
|
|
489
516
|
lines.extend(_response_lines("_resp.data"))
|
|
490
517
|
|
|
491
518
|
lines.append(f"{i1}}}")
|
|
@@ -31,6 +31,7 @@ from ssc_codegen.ast import (
|
|
|
31
31
|
Field,
|
|
32
32
|
Filter,
|
|
33
33
|
Fmt,
|
|
34
|
+
FunctionDef,
|
|
34
35
|
Init,
|
|
35
36
|
InitField,
|
|
36
37
|
InitFieldCall,
|
|
@@ -528,6 +529,31 @@ class JsVisitor(BaseWalker):
|
|
|
528
529
|
lines.append("}")
|
|
529
530
|
return lines
|
|
530
531
|
|
|
532
|
+
def visit_function_def(
|
|
533
|
+
self, node: FunctionDef, ctx: WalkContext
|
|
534
|
+
) -> list[str]:
|
|
535
|
+
name = to_camel_case(node.name)
|
|
536
|
+
inner = ctx.deeper()
|
|
537
|
+
lines: list[str] = []
|
|
538
|
+
if node.doc:
|
|
539
|
+
lines.extend(_js_docblock(node.doc.splitlines()))
|
|
540
|
+
lines.append(f"function {name}(document) {{")
|
|
541
|
+
if node.is_raw:
|
|
542
|
+
lines.append(f"{inner.indent}let {inner.var_name} = document;")
|
|
543
|
+
else:
|
|
544
|
+
lines.append(f"{inner.indent}let {inner.var_name};")
|
|
545
|
+
lines.append(f"{inner.indent}if (typeof document === 'string') {{")
|
|
546
|
+
lines.append(
|
|
547
|
+
f"{inner.deeper().indent}{inner.var_name} = (new DOMParser()).parseFromString(document, 'text/html');"
|
|
548
|
+
)
|
|
549
|
+
lines.append(f"{inner.indent}}} else {{")
|
|
550
|
+
lines.append(f"{inner.deeper().indent}{inner.var_name} = document;")
|
|
551
|
+
lines.append(f"{inner.indent}}}")
|
|
552
|
+
lines.extend(self.walk_children(node, ctx))
|
|
553
|
+
lines.append("}")
|
|
554
|
+
lines.append("")
|
|
555
|
+
return lines
|
|
556
|
+
|
|
531
557
|
def visit_split_doc(self, node: SplitDoc, ctx: WalkContext) -> list[str]:
|
|
532
558
|
lines = [f"{ctx.indent}_splitDoc(v) {{"]
|
|
533
559
|
lines.extend(self.walk_children(node, ctx))
|
|
@@ -296,32 +296,44 @@ def placeholder_args(http: RequestHttp) -> str:
|
|
|
296
296
|
# ===========================================================================
|
|
297
297
|
|
|
298
298
|
|
|
299
|
-
def
|
|
300
|
-
spec: RequestHttp, i2: str, i3: str
|
|
299
|
+
def _build_kw_dict(
|
|
300
|
+
spec: RequestHttp, i2: str, i3: str
|
|
301
301
|
) -> tuple[list[str], list[str]]:
|
|
302
|
-
"""Build (pre_lines,
|
|
302
|
+
"""Build (pre_lines, kw_assign_lines) for ``_kw`` dict construction.
|
|
303
303
|
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
Produces ``_kw["headers"] = {...}`` style assignments so caller kwargs
|
|
305
|
+
can be shallow-merged into the same dict before passing ``**_kw`` to
|
|
306
|
+
``client.request()`` / ``ssc_rest_call()``.
|
|
307
|
+
|
|
308
|
+
pre_lines: builder helper code (complex dicts needing intermediate vars).
|
|
309
|
+
kw_lines: ``_kw["key"] = value`` assignment lines (at i2 indent).
|
|
306
310
|
"""
|
|
307
311
|
pre_lines: list[str] = []
|
|
308
|
-
|
|
309
|
-
if include_method_url:
|
|
310
|
-
kwargs_lines.append(f"{i3}{spec.method!r},")
|
|
311
|
-
kwargs_lines.append(f"{i3}{render_value(spec.url)},")
|
|
312
|
+
kw_lines: list[str] = []
|
|
312
313
|
for attr, varname in _DICT_KWARGS:
|
|
313
314
|
d = getattr(spec, attr)
|
|
314
315
|
if not d:
|
|
315
316
|
continue
|
|
316
317
|
if dict_needs_builder(d):
|
|
317
318
|
pre_lines.extend(emit_dict_builder(varname, d, i2))
|
|
318
|
-
|
|
319
|
+
kw_lines.append(f'{i2}_kw["{attr}"] = {varname}')
|
|
319
320
|
else:
|
|
320
|
-
|
|
321
|
+
kw_lines.append(f'{i2}_kw["{attr}"] = {render_dict(d)}')
|
|
321
322
|
body_result = render_body(spec)
|
|
322
323
|
if body_result:
|
|
323
|
-
|
|
324
|
-
return pre_lines,
|
|
324
|
+
kw_lines.append(f'{i2}_kw["{body_result[0]}"] = {body_result[1]}')
|
|
325
|
+
return pre_lines, kw_lines
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _merge_loop_lines(i2: str) -> list[str]:
|
|
329
|
+
"""Shallow-merge user kwargs into _kw (dict keys merged key-by-key)."""
|
|
330
|
+
return [
|
|
331
|
+
f"{i2}for _k, _v in kwargs.items():",
|
|
332
|
+
f"{i2} if isinstance(_kw.get(_k), dict) and isinstance(_v, dict):",
|
|
333
|
+
f"{i2} _kw[_k] = {{**_kw[_k], **_v}}",
|
|
334
|
+
f"{i2} else:",
|
|
335
|
+
f"{i2} _kw[_k] = _v",
|
|
336
|
+
]
|
|
325
337
|
|
|
326
338
|
|
|
327
339
|
# ===========================================================================
|
|
@@ -343,9 +355,22 @@ def emit_method_fetch(
|
|
|
343
355
|
i2 = i1 + ctx.indent_char
|
|
344
356
|
i3 = i2 + ctx.indent_char
|
|
345
357
|
|
|
346
|
-
pre_lines,
|
|
347
|
-
|
|
348
|
-
|
|
358
|
+
pre_lines, kw_lines = _build_kw_dict(spec, i2, i3)
|
|
359
|
+
|
|
360
|
+
# kwargs_lines for fetch_body_lines: method, url, **_kw
|
|
361
|
+
fetch_kwargs = [
|
|
362
|
+
f"{i3}{spec.method!r},",
|
|
363
|
+
f"{i3}{render_value(spec.url)},",
|
|
364
|
+
f"{i3}**_kw,",
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
def _kw_setup() -> list[str]:
|
|
368
|
+
"""Lines to build _kw dict + merge user kwargs before the request."""
|
|
369
|
+
setup: list[str] = [f"{i2}_kw: Dict[str, Any] = {{}}"]
|
|
370
|
+
setup.extend(pre_lines)
|
|
371
|
+
setup.extend(kw_lines)
|
|
372
|
+
setup.extend(_merge_loop_lines(i2))
|
|
373
|
+
return setup
|
|
349
374
|
|
|
350
375
|
lines: list[str] = []
|
|
351
376
|
|
|
@@ -353,14 +378,14 @@ def emit_method_fetch(
|
|
|
353
378
|
if http.supports_sync_fetch:
|
|
354
379
|
lines.append(f"{i1}@classmethod")
|
|
355
380
|
lines.append(
|
|
356
|
-
f'{i1}def fetch{suffix}(cls, client: {http.sync_client_type}{ph_params}) -> "{struct_name}":'
|
|
381
|
+
f'{i1}def fetch{suffix}(cls, client: {http.sync_client_type}{ph_params}, **kwargs: Any) -> "{struct_name}":'
|
|
357
382
|
)
|
|
358
|
-
lines.extend(
|
|
383
|
+
lines.extend(_kw_setup())
|
|
359
384
|
lines.extend(
|
|
360
385
|
http.fetch_body_lines(
|
|
361
386
|
is_async=False,
|
|
362
387
|
request_call=f"{i2}_resp = client.request(",
|
|
363
|
-
kwargs_lines=
|
|
388
|
+
kwargs_lines=fetch_kwargs,
|
|
364
389
|
response_path=node.response_path,
|
|
365
390
|
response_join=node.response_join,
|
|
366
391
|
i2=i2,
|
|
@@ -372,24 +397,27 @@ def emit_method_fetch(
|
|
|
372
397
|
# --- async_fetch ------------------------------------------------------
|
|
373
398
|
lines.append(f"{i1}@classmethod")
|
|
374
399
|
lines.append(
|
|
375
|
-
f'{i1}async def async_fetch{suffix}(cls, client: {http.async_client_type}{ph_params}) -> "{struct_name}":'
|
|
400
|
+
f'{i1}async def async_fetch{suffix}(cls, client: {http.async_client_type}{ph_params}, **kwargs: Any) -> "{struct_name}":'
|
|
376
401
|
)
|
|
377
402
|
if http.async_fetch_delegates_to_sync:
|
|
378
403
|
# requests: no native async — run the sync fetch in a worker thread
|
|
379
404
|
# via asyncio.to_thread (non-blocking, yields control to the loop).
|
|
380
|
-
|
|
405
|
+
if ph_kwargs:
|
|
406
|
+
to_thread_args = f"client, {ph_kwargs}, **kwargs"
|
|
407
|
+
else:
|
|
408
|
+
to_thread_args = "client, **kwargs"
|
|
381
409
|
lines.append(f"{i2}import asyncio")
|
|
382
410
|
lines.append(
|
|
383
411
|
f"{i2}return await asyncio.to_thread("
|
|
384
412
|
f"cls.fetch{suffix}, {to_thread_args})"
|
|
385
413
|
)
|
|
386
414
|
else:
|
|
387
|
-
lines.extend(
|
|
415
|
+
lines.extend(_kw_setup())
|
|
388
416
|
lines.extend(
|
|
389
417
|
http.fetch_body_lines(
|
|
390
418
|
is_async=True,
|
|
391
419
|
request_call=f"{i2}_resp = await client.request(",
|
|
392
|
-
kwargs_lines=
|
|
420
|
+
kwargs_lines=fetch_kwargs,
|
|
393
421
|
response_path=node.response_path,
|
|
394
422
|
response_join=node.response_join,
|
|
395
423
|
i2=i2,
|
|
@@ -416,9 +444,7 @@ def emit_method_rest(
|
|
|
416
444
|
|
|
417
445
|
doc_line = f'{i2}"""{node.doc}"""' if node.doc else None
|
|
418
446
|
|
|
419
|
-
pre_lines,
|
|
420
|
-
spec, i2, i3, include_method_url=False
|
|
421
|
-
)
|
|
447
|
+
pre_lines, kw_lines = _build_kw_dict(spec, i2, i3)
|
|
422
448
|
|
|
423
449
|
value_kwarg: list[str] = []
|
|
424
450
|
if node.response_path:
|
|
@@ -431,36 +457,31 @@ def emit_method_rest(
|
|
|
431
457
|
body: list[str] = []
|
|
432
458
|
if doc_line:
|
|
433
459
|
body.append(doc_line)
|
|
460
|
+
body.append(f"{i2}_kw: Dict[str, Any] = {{}}")
|
|
434
461
|
body.extend(pre_lines)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
# heterogeneous matchers list will produce at runtime). The
|
|
438
|
-
# parser-declared Result alias narrows to the precise union
|
|
439
|
-
# (Err400 | UnknownErr | TransportErr | ...). cast() documents
|
|
440
|
-
# this intent at the call site without suppressing mypy via
|
|
441
|
-
# ``# type: ignore``; the wrapping method's return type is the
|
|
442
|
-
# stable monad consumers see.
|
|
462
|
+
body.extend(kw_lines)
|
|
463
|
+
body.extend(_merge_loop_lines(i2))
|
|
443
464
|
body.append(f"{i2}return cast({ret_type}, {await_kw}{fn_name}(")
|
|
444
465
|
body.append(
|
|
445
466
|
f"{i3}client, {matchers_var}, {spec.method!r},"
|
|
446
467
|
f" {render_value(spec.url)},"
|
|
447
468
|
)
|
|
448
469
|
body.extend(value_kwarg)
|
|
449
|
-
body.
|
|
470
|
+
body.append(f"{i3}**_kw,")
|
|
450
471
|
body.append(f"{i2}))")
|
|
451
472
|
return body
|
|
452
473
|
|
|
453
474
|
lines: list[str] = []
|
|
454
475
|
lines.append(f"{i1}@classmethod")
|
|
455
476
|
lines.append(
|
|
456
|
-
f"{i1}def {method_name}(cls, client: {http.sync_client_type}{ph_params}) -> {ret_type}:"
|
|
477
|
+
f"{i1}def {method_name}(cls, client: {http.sync_client_type}{ph_params}, **kwargs: Any) -> {ret_type}:"
|
|
457
478
|
)
|
|
458
479
|
lines.extend(_body("ssc_rest_call", ""))
|
|
459
480
|
lines.append("")
|
|
460
481
|
|
|
461
482
|
lines.append(f"{i1}@classmethod")
|
|
462
483
|
lines.append(
|
|
463
|
-
f"{i1}async def async_{method_name}(cls, client: {http.async_client_type}{ph_params}) -> {ret_type}:"
|
|
484
|
+
f"{i1}async def async_{method_name}(cls, client: {http.async_client_type}{ph_params}, **kwargs: Any) -> {ret_type}:"
|
|
464
485
|
)
|
|
465
486
|
lines.extend(_body("ssc_rest_call_async", "await "))
|
|
466
487
|
return lines
|
|
@@ -28,6 +28,7 @@ from ssc_codegen.ast import (
|
|
|
28
28
|
InitFieldCall,
|
|
29
29
|
InitField,
|
|
30
30
|
Field,
|
|
31
|
+
FunctionDef,
|
|
31
32
|
PreValidate,
|
|
32
33
|
CheckMethod,
|
|
33
34
|
SplitDoc,
|
|
@@ -661,6 +662,31 @@ class PythonVisitor(BaseWalker):
|
|
|
661
662
|
lines.extend(self.walk_children(node, ctx))
|
|
662
663
|
return lines
|
|
663
664
|
|
|
665
|
+
def visit_function_def(
|
|
666
|
+
self, node: FunctionDef, ctx: WalkContext
|
|
667
|
+
) -> list[str]:
|
|
668
|
+
name = to_snake_case(node.name)
|
|
669
|
+
t_ret = self._resolve_type(node.ret_type_info)
|
|
670
|
+
inner = ctx.deeper()
|
|
671
|
+
lines = [f"{ctx.indent}def {name}(document: str) -> {t_ret}:"]
|
|
672
|
+
if node.doc:
|
|
673
|
+
lines.append(f'{inner.indent}"""')
|
|
674
|
+
for doc_line in node.doc.splitlines():
|
|
675
|
+
lines.append(inner.indent + doc_line)
|
|
676
|
+
lines.append(f'{inner.indent}"""')
|
|
677
|
+
if node.is_raw:
|
|
678
|
+
lines.append(f"{inner.indent}{inner.var_name} = document")
|
|
679
|
+
else:
|
|
680
|
+
lines.append(f"{inner.indent}if isinstance(document, str):")
|
|
681
|
+
lines.append(
|
|
682
|
+
f"{inner.deeper().indent}{inner.var_name} = {self._dom.init_from_str_expr}"
|
|
683
|
+
)
|
|
684
|
+
lines.append(f"{inner.indent}else:")
|
|
685
|
+
lines.append(f"{inner.deeper().indent}{inner.var_name} = document")
|
|
686
|
+
lines.extend(self.walk_children(node, ctx))
|
|
687
|
+
lines.append("")
|
|
688
|
+
return lines
|
|
689
|
+
|
|
664
690
|
def visit_split_doc(self, node: SplitDoc, ctx: WalkContext) -> list[str]:
|
|
665
691
|
t_arg = self._resolve_type(node.accept_type_info)
|
|
666
692
|
t_ret = self._resolve_type(node.ret_type_info)
|
|
@@ -6,6 +6,7 @@ from ssc_codegen.ast import (
|
|
|
6
6
|
Assert,
|
|
7
7
|
ErrorResponse,
|
|
8
8
|
Filter,
|
|
9
|
+
FunctionDef,
|
|
9
10
|
Match,
|
|
10
11
|
MethodBase,
|
|
11
12
|
Module,
|
|
@@ -49,13 +50,20 @@ def module_is_rest_only(module: Module) -> bool:
|
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def module_has_html_struct(module: Module) -> bool:
|
|
52
|
-
"""True if the module has at least one HTML-parsing struct
|
|
53
|
+
"""True if the module has at least one HTML-parsing struct or function.
|
|
54
|
+
|
|
55
|
+
RAW structs, (raw)fn, and REST structs are excluded — they don't need
|
|
56
|
+
an HTML parser backend (bs4/lxml/goquery/DOMParser).
|
|
57
|
+
"""
|
|
53
58
|
for n in module.body:
|
|
54
59
|
if isinstance(n, StructRest):
|
|
55
60
|
continue
|
|
56
61
|
if isinstance(n, Struct):
|
|
57
62
|
if n.type != StructType.RAW:
|
|
58
63
|
return True
|
|
64
|
+
if isinstance(n, FunctionDef):
|
|
65
|
+
if not n.is_raw:
|
|
66
|
+
return True
|
|
59
67
|
return False
|
|
60
68
|
|
|
61
69
|
|
|
@@ -16,6 +16,7 @@ from ssc_codegen.ast import (
|
|
|
16
16
|
InitFieldCall,
|
|
17
17
|
InitField,
|
|
18
18
|
Field,
|
|
19
|
+
FunctionDef,
|
|
19
20
|
PreValidate,
|
|
20
21
|
CheckMethod,
|
|
21
22
|
SplitDoc,
|
|
@@ -115,6 +116,7 @@ _CONTAINER_NODES = (JsonDef, TypeDef, StructBase, Init)
|
|
|
115
116
|
# Pipeline: index advances after each node (delegated to walk_pipeline).
|
|
116
117
|
_PIPELINE_NODES = (
|
|
117
118
|
Field,
|
|
119
|
+
FunctionDef,
|
|
118
120
|
InitField,
|
|
119
121
|
PreValidate,
|
|
120
122
|
CheckMethod,
|
|
@@ -179,6 +181,8 @@ class BaseWalker:
|
|
|
179
181
|
MethodFetch: "visit_method_fetch",
|
|
180
182
|
MethodRest: "visit_method_rest",
|
|
181
183
|
ErrorResponse: "visit_error_response",
|
|
184
|
+
# function
|
|
185
|
+
FunctionDef: "visit_function_def",
|
|
182
186
|
# REST result artifacts
|
|
183
187
|
ResultVariantDef: "visit_result_variant_def",
|
|
184
188
|
ResultAliasDef: "visit_result_alias_def",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ssc_codegen-0.33.0 → ssc_codegen-0.35.0}/ssc_codegen/targets/javascript/http_libs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|