ssc_codegen 0.21.0__tar.gz → 0.22.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.21.0 → ssc_codegen-0.22.0}/PKG-INFO +1 -1
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/pyproject.toml +1 -1
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/__init__.py +2 -1
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/jsondef.py +11 -5
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/struct.py +20 -5
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/base.py +2 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/go_goquery.py +13 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/js_pure.py +58 -8
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/py_bs4.py +21 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/py_helpers.py +41 -9
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/rules_struct.py +1 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/parser.py +71 -11
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/repl.py +12 -3
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/.gitignore +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/LICENSE +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/README.md +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/__init__.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/_logging.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/array.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/base.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/cast.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/control.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/extract.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/helpers.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/module.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/predicate_containers.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/predicate_ops.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/regex.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/selectors.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/string.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/transform.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/typedef.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/ast/types.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/helpers.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/py_lxml.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/py_parsel.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/converters/py_slax.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/document_utils.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/exceptions.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/health.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/kdl/__init__.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/kdl/parser.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/__init__.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/_kdl_lang.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/base.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/errors.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/format_errors.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/metadata.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/navigation.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/path.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/rule_keywords.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/rules.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/type_rules.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/linter/types.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/main.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/openapi/__init__.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/openapi/converter.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/openapi/emitter.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/openapi/parser.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/parsers/__init__.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/parsers/curl.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/parsers/http.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/parsers/spec.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/pseudo_selectors.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.0}/ssc_codegen/regex_utils.py +0 -0
- {ssc_codegen-0.21.0 → ssc_codegen-0.22.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.22.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
|
|
@@ -25,6 +25,7 @@ from .struct import (
|
|
|
25
25
|
Struct,
|
|
26
26
|
StructDocstring,
|
|
27
27
|
PreValidate,
|
|
28
|
+
CheckMethod,
|
|
28
29
|
Init,
|
|
29
30
|
InitField,
|
|
30
31
|
SplitDoc,
|
|
@@ -134,7 +135,7 @@ __all__ = [
|
|
|
134
135
|
# jsondef
|
|
135
136
|
"JsonDef", "JsonDefField",
|
|
136
137
|
# struct
|
|
137
|
-
"Struct", "StructDocstring", "PreValidate",
|
|
138
|
+
"Struct", "StructDocstring", "PreValidate", "CheckMethod",
|
|
138
139
|
"Init", "InitField", "SplitDoc",
|
|
139
140
|
"Key", "Value",
|
|
140
141
|
"TableConfig", "TableRow", "TableMatchKey",
|
|
@@ -10,11 +10,13 @@ class JsonDefField(Node):
|
|
|
10
10
|
Single field in a JSON mapping definition.
|
|
11
11
|
|
|
12
12
|
type_name — primitive ("str", "int", "float", "bool") or ref name.
|
|
13
|
-
is_optional — True when field declared with ? suffix
|
|
14
|
-
is_array — True when field declared with
|
|
13
|
+
is_optional — True when field declared with ? suffix or @optional arg.
|
|
14
|
+
is_array — True when field declared with (array) prefix.
|
|
15
15
|
ref_name — set when type_name references another JsonDef.
|
|
16
|
-
alias — original JSON key when it differs from name
|
|
17
|
-
|
|
16
|
+
alias — original JSON key when it differs from name.
|
|
17
|
+
skip — field is parsed but excluded from output TypedDict.
|
|
18
|
+
may_miss — field key may be absent from JSON (use .get() instead of []).
|
|
19
|
+
doc — documentation string for the field.
|
|
18
20
|
"""
|
|
19
21
|
|
|
20
22
|
name: str = ""
|
|
@@ -23,15 +25,19 @@ class JsonDefField(Node):
|
|
|
23
25
|
is_array: bool = False
|
|
24
26
|
ref_name: str | None = None
|
|
25
27
|
alias: str = ""
|
|
28
|
+
skip: bool = False
|
|
29
|
+
may_miss: bool = False
|
|
30
|
+
doc: str = ""
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
@dataclass
|
|
29
34
|
class JsonDef(Node):
|
|
30
35
|
"""
|
|
31
36
|
JSON mapping definition.
|
|
32
|
-
DSL: json Name { ... } / json Name array=#true { ... }
|
|
37
|
+
DSL: json Name { ... } / json Name array=#true { ... } / json Name path="a.b" { ... }
|
|
33
38
|
body: list[JsonDefField]
|
|
34
39
|
"""
|
|
35
40
|
|
|
36
41
|
name: str = ""
|
|
37
42
|
is_array: bool = False
|
|
43
|
+
path: str = ""
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import re as _re
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
4
5
|
from typing import cast
|
|
5
6
|
|
|
6
7
|
from .base import Node
|
|
@@ -119,6 +120,20 @@ class PreValidate(Node):
|
|
|
119
120
|
ret: VariableType = field(default=VariableType.DOCUMENT)
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
@dataclass
|
|
124
|
+
class CheckMethod(Node):
|
|
125
|
+
"""
|
|
126
|
+
Boolean check method on the parsed class.
|
|
127
|
+
DSL: @check <name> { pipeline ... }
|
|
128
|
+
Runs a pipeline on the document and returns True on success, False on failure.
|
|
129
|
+
Called manually by the user before parse().
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
name: str = ""
|
|
133
|
+
accept: VariableType = field(default=VariableType.DOCUMENT)
|
|
134
|
+
ret: VariableType = field(default=VariableType.BOOL)
|
|
135
|
+
|
|
136
|
+
|
|
122
137
|
@dataclass
|
|
123
138
|
class Init(Node):
|
|
124
139
|
"""
|
|
@@ -276,18 +291,18 @@ class RequestConfig(Node):
|
|
|
276
291
|
class ErrorResponse(Node):
|
|
277
292
|
"""
|
|
278
293
|
Error response mapping for type=rest struct.
|
|
279
|
-
DSL: @error <status> [field=
|
|
294
|
+
DSL: @error <status> <SchemaName> [field=value ...]
|
|
280
295
|
|
|
281
296
|
status: HTTP status code [100..599].
|
|
282
297
|
schema_name: json schema reference for deserialised error body.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
298
|
+
conditions: field=value pairs checked against the parsed JSON body.
|
|
299
|
+
Keys are dot-paths (e.g. "response.success", "data.0.type").
|
|
300
|
+
When non-empty, the error triggers on matching status + all conditions.
|
|
286
301
|
"""
|
|
287
302
|
|
|
288
303
|
status: int = 0
|
|
289
304
|
schema_name: str = ""
|
|
290
|
-
|
|
305
|
+
conditions: dict[str, Any] = field(default_factory=dict)
|
|
291
306
|
|
|
292
307
|
|
|
293
308
|
@dataclass
|
|
@@ -9,6 +9,7 @@ from ssc_codegen.ast import LogicNot, LogicAnd, LogicOr
|
|
|
9
9
|
from ssc_codegen.ast import JsonDef, TypeDef, Struct, Init
|
|
10
10
|
from ssc_codegen.ast import (
|
|
11
11
|
PreValidate,
|
|
12
|
+
CheckMethod,
|
|
12
13
|
Field,
|
|
13
14
|
InitField,
|
|
14
15
|
SplitDoc,
|
|
@@ -64,6 +65,7 @@ _PIPELINE_NODES = (
|
|
|
64
65
|
Field,
|
|
65
66
|
InitField,
|
|
66
67
|
PreValidate,
|
|
68
|
+
CheckMethod,
|
|
67
69
|
SplitDoc,
|
|
68
70
|
Key,
|
|
69
71
|
Value,
|
|
@@ -25,6 +25,7 @@ from ssc_codegen.ast import (
|
|
|
25
25
|
Init,
|
|
26
26
|
InitField,
|
|
27
27
|
PreValidate,
|
|
28
|
+
CheckMethod,
|
|
28
29
|
SplitDoc,
|
|
29
30
|
TableConfig,
|
|
30
31
|
TableMatchKey,
|
|
@@ -1011,6 +1012,18 @@ def post_pre_validate(node: PreValidate, _):
|
|
|
1011
1012
|
return ["}"]
|
|
1012
1013
|
|
|
1013
1014
|
|
|
1015
|
+
@GO_GOQUERY_CONVERTER(CheckMethod)
|
|
1016
|
+
def pre_check_method(node: CheckMethod, _):
|
|
1017
|
+
struct_name = to_pascal_case(node.parent.name)
|
|
1018
|
+
recv = to_camel_case(struct_name)
|
|
1019
|
+
return [f"func ({recv} *{struct_name}) {to_camel_case(node.name)}() bool {{", "\tdefer func() { recover() }()"]
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
@GO_GOQUERY_CONVERTER.post(CheckMethod)
|
|
1023
|
+
def post_check_method(node: CheckMethod, _):
|
|
1024
|
+
return ["\treturn true", "}"]
|
|
1025
|
+
|
|
1026
|
+
|
|
1014
1027
|
@GO_GOQUERY_CONVERTER(SplitDoc)
|
|
1015
1028
|
def pre_split_doc(node: SplitDoc, _):
|
|
1016
1029
|
struct_name = to_pascal_case(node.parent.name)
|
|
@@ -51,6 +51,7 @@ from ssc_codegen.ast import (
|
|
|
51
51
|
Init,
|
|
52
52
|
InitField,
|
|
53
53
|
PreValidate,
|
|
54
|
+
CheckMethod,
|
|
54
55
|
SplitDoc,
|
|
55
56
|
TableConfig,
|
|
56
57
|
TableMatchKey,
|
|
@@ -464,11 +465,40 @@ def pre_typedef_field(node: TypeDefField, ctx: ConverterContext):
|
|
|
464
465
|
|
|
465
466
|
def _js_err_subclass_name(struct_name: str, err) -> str:
|
|
466
467
|
base = f"{to_pascal_case(struct_name)}Err{err.status}"
|
|
467
|
-
if err.
|
|
468
|
-
|
|
468
|
+
if err.conditions:
|
|
469
|
+
for key in err.conditions:
|
|
470
|
+
base += to_pascal_case(key.replace(".", "_").replace("-", "_"))
|
|
469
471
|
return base
|
|
470
472
|
|
|
471
473
|
|
|
474
|
+
def _js_resolve_path_expr(body_var: str, path: str) -> str:
|
|
475
|
+
"""Generate a JS expression navigating a dot-path into a JSON body."""
|
|
476
|
+
parts = path.split(".")
|
|
477
|
+
expr = body_var
|
|
478
|
+
for seg in parts:
|
|
479
|
+
if seg.isdigit():
|
|
480
|
+
expr += f"[{seg}]"
|
|
481
|
+
else:
|
|
482
|
+
expr += f"[{seg!r}]"
|
|
483
|
+
return expr
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _js_condition_check_expr(err) -> str:
|
|
487
|
+
"""Build a compound JS condition expression for @error field conditions."""
|
|
488
|
+
parts = []
|
|
489
|
+
for path, value in err.conditions.items():
|
|
490
|
+
lhs = _js_resolve_path_expr("_body", path)
|
|
491
|
+
if isinstance(value, bool):
|
|
492
|
+
parts.append(f"{lhs} === {str(value).lower()}")
|
|
493
|
+
elif value is None:
|
|
494
|
+
parts.append(f"{lhs} === null")
|
|
495
|
+
elif isinstance(value, (int, float)):
|
|
496
|
+
parts.append(f"{lhs} === {value}")
|
|
497
|
+
else:
|
|
498
|
+
parts.append(f"{lhs} === {value!r}")
|
|
499
|
+
return " && ".join(parts)
|
|
500
|
+
|
|
501
|
+
|
|
472
502
|
def _js_err_value_type(err, struct) -> str:
|
|
473
503
|
schema = err.schema_name
|
|
474
504
|
if not schema:
|
|
@@ -527,8 +557,8 @@ def _emit_dispatch_err_js(node: Struct, ctx: ConverterContext) -> list[str]:
|
|
|
527
557
|
i3 = i2 + ctx.indent_char # nested (if ...)
|
|
528
558
|
|
|
529
559
|
errors = node.errors
|
|
530
|
-
status_errors = [e for e in errors if e.
|
|
531
|
-
field_errors = [e for e in errors if e.
|
|
560
|
+
status_errors = [e for e in errors if not e.conditions]
|
|
561
|
+
field_errors = [e for e in errors if e.conditions]
|
|
532
562
|
|
|
533
563
|
lines: list[str] = [
|
|
534
564
|
f"{i1}static _dispatchErr(_status, _headers, _body) {{",
|
|
@@ -536,10 +566,10 @@ def _emit_dispatch_err_js(node: Struct, ctx: ConverterContext) -> list[str]:
|
|
|
536
566
|
]
|
|
537
567
|
for err in field_errors:
|
|
538
568
|
if 200 <= err.status < 300:
|
|
539
|
-
|
|
569
|
+
cond = _js_condition_check_expr(err)
|
|
540
570
|
lines.append(
|
|
541
571
|
f"{i3}if (_status === {err.status} && _body "
|
|
542
|
-
f"&& _body
|
|
572
|
+
f"&& _body instanceof Object && {cond}) {{"
|
|
543
573
|
)
|
|
544
574
|
lines.append(
|
|
545
575
|
f"{i3}{ctx.indent_char}return {{ isOk: false, "
|
|
@@ -558,10 +588,10 @@ def _emit_dispatch_err_js(node: Struct, ctx: ConverterContext) -> list[str]:
|
|
|
558
588
|
lines.append(f"{i2}}}")
|
|
559
589
|
for err in field_errors:
|
|
560
590
|
if not (200 <= err.status < 300):
|
|
561
|
-
|
|
591
|
+
cond = _js_condition_check_expr(err)
|
|
562
592
|
lines.append(
|
|
563
593
|
f"{i2}if (_status === {err.status} && _body "
|
|
564
|
-
f"&& _body
|
|
594
|
+
f"&& _body instanceof Object && {cond}) {{"
|
|
565
595
|
)
|
|
566
596
|
lines.append(
|
|
567
597
|
f"{i3}return {{ isOk: false, status: _status, "
|
|
@@ -632,6 +662,26 @@ def pre_struct_pre_validate(node: PreValidate, ctx: ConverterContext):
|
|
|
632
662
|
]
|
|
633
663
|
|
|
634
664
|
|
|
665
|
+
@JS_CONVERTER(CheckMethod)
|
|
666
|
+
def pre_struct_check_method(node: CheckMethod, ctx: ConverterContext):
|
|
667
|
+
method_name = to_camel_case(node.name)
|
|
668
|
+
return [
|
|
669
|
+
f"{ctx.indent}{method_name}() " + "{",
|
|
670
|
+
f"{ctx.indent}{ctx.indent_char}try " + "{",
|
|
671
|
+
]
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@JS_CONVERTER.post(CheckMethod)
|
|
675
|
+
def post_struct_check_method(node: CheckMethod, ctx: ConverterContext):
|
|
676
|
+
return [
|
|
677
|
+
f"{ctx.indent}{ctx.indent_char}{ctx.indent_char}return true;",
|
|
678
|
+
f"{ctx.indent}{ctx.indent_char}" + "} catch (e) {",
|
|
679
|
+
f"{ctx.indent}{ctx.indent_char}{ctx.indent_char}return false;",
|
|
680
|
+
f"{ctx.indent}{ctx.indent_char}" + "}",
|
|
681
|
+
f"{ctx.indent}" + "}",
|
|
682
|
+
]
|
|
683
|
+
|
|
684
|
+
|
|
635
685
|
@JS_CONVERTER(SplitDoc, post_callback=lambda _, ctx: ctx.indent + "}")
|
|
636
686
|
def pre_struct_split_doc(node: SplitDoc, ctx: ConverterContext):
|
|
637
687
|
return [f"{ctx.indent}_splitDoc(v) " + "{"]
|
|
@@ -37,6 +37,7 @@ from ssc_codegen.ast import (
|
|
|
37
37
|
Init,
|
|
38
38
|
InitField,
|
|
39
39
|
PreValidate,
|
|
40
|
+
CheckMethod,
|
|
40
41
|
SplitDoc,
|
|
41
42
|
TableConfig,
|
|
42
43
|
TableMatchKey,
|
|
@@ -537,6 +538,26 @@ def pre_struct_pre_validate(node: PreValidate, ctx: ConverterContext):
|
|
|
537
538
|
]
|
|
538
539
|
|
|
539
540
|
|
|
541
|
+
@PY_BASE_CONVERTER(CheckMethod)
|
|
542
|
+
def pre_struct_check_method(node: CheckMethod, ctx: ConverterContext):
|
|
543
|
+
from ssc_codegen.converters.helpers import to_snake_case
|
|
544
|
+
|
|
545
|
+
method_name = to_snake_case(node.name)
|
|
546
|
+
return [
|
|
547
|
+
f" def {method_name}(self) -> bool:",
|
|
548
|
+
" try:",
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
@PY_BASE_CONVERTER.post(CheckMethod)
|
|
553
|
+
def post_struct_check_method(node: CheckMethod, ctx: ConverterContext):
|
|
554
|
+
return [
|
|
555
|
+
" return True",
|
|
556
|
+
" except Exception:",
|
|
557
|
+
" return False",
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
|
|
540
561
|
@PY_BASE_CONVERTER(SplitDoc)
|
|
541
562
|
def pre_struct_split_doc(node: SplitDoc, ctx: ConverterContext):
|
|
542
563
|
return [
|
|
@@ -99,10 +99,11 @@ def rest_utilities(node) -> list[str]:
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def _err_subclass_name(struct_name: str, err: ErrorResponse) -> str:
|
|
102
|
-
"""Naming: `<Struct>Err<Status>[<
|
|
102
|
+
"""Naming: `<Struct>Err<Status>[<ConditionKeys>]`."""
|
|
103
103
|
base = f"{to_pascal_case(struct_name)}Err{err.status}"
|
|
104
|
-
if err.
|
|
105
|
-
|
|
104
|
+
if err.conditions:
|
|
105
|
+
for key in err.conditions:
|
|
106
|
+
base += to_pascal_case(key.replace(".", "_").replace("-", "_"))
|
|
106
107
|
return base
|
|
107
108
|
|
|
108
109
|
|
|
@@ -132,6 +133,37 @@ def _rest_err_union_type(struct: Struct) -> str:
|
|
|
132
133
|
return "Union[" + ", ".join([*variants, "UnknownErr", "None"]) + "]"
|
|
133
134
|
|
|
134
135
|
|
|
136
|
+
def _resolve_path_expr(body_var: str, path: str) -> str:
|
|
137
|
+
"""Generate a Python expression that navigates a dot-path into a JSON body.
|
|
138
|
+
|
|
139
|
+
Segments: digits → list index, else → dict key via .get().
|
|
140
|
+
"""
|
|
141
|
+
parts = path.split(".")
|
|
142
|
+
expr = body_var
|
|
143
|
+
for seg in parts:
|
|
144
|
+
if seg.isdigit():
|
|
145
|
+
expr += f"[{seg}]"
|
|
146
|
+
else:
|
|
147
|
+
expr += f".get({seg!r})"
|
|
148
|
+
return expr
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _condition_check_expr(err: ErrorResponse) -> str:
|
|
152
|
+
"""Build a compound condition expression for an @error's field conditions."""
|
|
153
|
+
parts = []
|
|
154
|
+
for path, value in err.conditions.items():
|
|
155
|
+
lhs = _resolve_path_expr("_body", path)
|
|
156
|
+
if isinstance(value, bool):
|
|
157
|
+
parts.append(f"{lhs} == {value}")
|
|
158
|
+
elif value is None:
|
|
159
|
+
parts.append(f"{lhs} is None")
|
|
160
|
+
elif isinstance(value, (int, float)):
|
|
161
|
+
parts.append(f"{lhs} == {value}")
|
|
162
|
+
else:
|
|
163
|
+
parts.append(f"{lhs} == {value!r}")
|
|
164
|
+
return " and ".join(parts)
|
|
165
|
+
|
|
166
|
+
|
|
135
167
|
def _emit_dispatch_err_py(node: Struct, ctx: ConverterContext) -> list[str]:
|
|
136
168
|
"""Emit the `_dispatch_err` @staticmethod lines inside a REST class.
|
|
137
169
|
|
|
@@ -143,8 +175,8 @@ def _emit_dispatch_err_py(node: Struct, ctx: ConverterContext) -> list[str]:
|
|
|
143
175
|
i4 = i3 + ctx.indent_char
|
|
144
176
|
|
|
145
177
|
errors = node.errors
|
|
146
|
-
status_errors = [e for e in errors if e.
|
|
147
|
-
field_errors = [e for e in errors if e.
|
|
178
|
+
status_errors = [e for e in errors if not e.conditions]
|
|
179
|
+
field_errors = [e for e in errors if e.conditions]
|
|
148
180
|
union_type = _rest_err_union_type(node)
|
|
149
181
|
|
|
150
182
|
lines: list[str] = [
|
|
@@ -162,9 +194,9 @@ def _emit_dispatch_err_py(node: Struct, ctx: ConverterContext) -> list[str]:
|
|
|
162
194
|
for err in field_errors:
|
|
163
195
|
if 200 <= err.status < 300:
|
|
164
196
|
cls_name = _err_subclass_name(node.name, err)
|
|
197
|
+
cond = _condition_check_expr(err)
|
|
165
198
|
lines.append(
|
|
166
|
-
f"{i4}if _status == {err.status} "
|
|
167
|
-
f"and {err.discriminator_field!r} in _body:"
|
|
199
|
+
f"{i4}if _status == {err.status} and {cond}:"
|
|
168
200
|
)
|
|
169
201
|
lines.append(
|
|
170
202
|
f"{i4}{ctx.indent_char}return {cls_name}("
|
|
@@ -185,10 +217,10 @@ def _emit_dispatch_err_py(node: Struct, ctx: ConverterContext) -> list[str]:
|
|
|
185
217
|
for err in field_errors:
|
|
186
218
|
if not (200 <= err.status < 300):
|
|
187
219
|
cls_name = _err_subclass_name(node.name, err)
|
|
220
|
+
cond = _condition_check_expr(err)
|
|
188
221
|
lines.append(
|
|
189
222
|
f"{i2}if _status == {err.status} "
|
|
190
|
-
f"and isinstance(_body, dict) "
|
|
191
|
-
f"and {err.discriminator_field!r} in _body:"
|
|
223
|
+
f"and isinstance(_body, dict) and {cond}:"
|
|
192
224
|
)
|
|
193
225
|
lines.append(
|
|
194
226
|
f"{i3}return {cls_name}(headers=_headers, value=_body)"
|
|
@@ -45,6 +45,7 @@ _RESERVED_ALLOWED: dict[str, frozenset[str] | None] = {
|
|
|
45
45
|
"@request": None,
|
|
46
46
|
"@doc": None,
|
|
47
47
|
"@pre-validate": frozenset({"item", "list", "dict", "table", "flat"}),
|
|
48
|
+
"@check": frozenset({"item", "list", "dict", "table", "flat"}),
|
|
48
49
|
"@init": frozenset({"item", "list", "dict", "table", "flat"}),
|
|
49
50
|
"@split-doc": frozenset({"list", "dict"}), # dict can also use @split-doc
|
|
50
51
|
"@key": frozenset({"dict"}),
|
|
@@ -56,6 +56,7 @@ from ssc_codegen.ast import (
|
|
|
56
56
|
# stuct layer
|
|
57
57
|
from ssc_codegen.ast import (
|
|
58
58
|
PreValidate,
|
|
59
|
+
CheckMethod,
|
|
59
60
|
SplitDoc,
|
|
60
61
|
TableConfig,
|
|
61
62
|
TableMatchKey,
|
|
@@ -461,6 +462,27 @@ class AstParser:
|
|
|
461
462
|
ret_type = VariableType.JSON
|
|
462
463
|
# second argument is optional alias (original JSON key)
|
|
463
464
|
alias = str(node.args[1]) if len(node.args) > 1 else ""
|
|
465
|
+
# scan remaining args for @modifiers (@optional, @missing, @skip)
|
|
466
|
+
skip = False
|
|
467
|
+
may_miss = False
|
|
468
|
+
for arg in node.args[2:]:
|
|
469
|
+
arg_s = str(arg)
|
|
470
|
+
if arg_s == "@skip":
|
|
471
|
+
skip = True
|
|
472
|
+
elif arg_s == "@missing":
|
|
473
|
+
may_miss = True
|
|
474
|
+
elif arg_s == "@optional":
|
|
475
|
+
is_optional = True
|
|
476
|
+
# if alias was actually an @modifier, correct alias
|
|
477
|
+
if alias.startswith("@"):
|
|
478
|
+
if alias == "@skip":
|
|
479
|
+
skip = True
|
|
480
|
+
elif alias == "@missing":
|
|
481
|
+
may_miss = True
|
|
482
|
+
elif alias == "@optional":
|
|
483
|
+
is_optional = True
|
|
484
|
+
alias = ""
|
|
485
|
+
doc = str(node.properties.get("doc", ""))
|
|
464
486
|
jf = JsonDefField(
|
|
465
487
|
parent=parent,
|
|
466
488
|
ret=ret_type,
|
|
@@ -469,15 +491,20 @@ class AstParser:
|
|
|
469
491
|
is_array=is_array,
|
|
470
492
|
ref_name=ref_name,
|
|
471
493
|
alias=alias,
|
|
494
|
+
skip=skip,
|
|
495
|
+
may_miss=may_miss,
|
|
496
|
+
doc=doc,
|
|
472
497
|
)
|
|
473
498
|
logger.debug(
|
|
474
|
-
" json field %r: ret=%s, optional=%s, array=%s%s%s",
|
|
499
|
+
" json field %r: ret=%s, optional=%s, array=%s%s%s%s%s",
|
|
475
500
|
name,
|
|
476
501
|
ret_type,
|
|
477
502
|
is_optional,
|
|
478
503
|
is_array,
|
|
479
504
|
f", ref={ref_name!r}" if ref_name else "",
|
|
480
505
|
f", alias={alias!r}" if alias else "",
|
|
506
|
+
", skip" if skip else "",
|
|
507
|
+
", miss" if may_miss else "",
|
|
481
508
|
)
|
|
482
509
|
parent.body.append(jf)
|
|
483
510
|
|
|
@@ -777,6 +804,15 @@ class AstParser:
|
|
|
777
804
|
expr = PreValidate(parent=parent)
|
|
778
805
|
self._parse_expressions(node.children, expr)
|
|
779
806
|
parent.body.append(expr)
|
|
807
|
+
elif node.name == "@check":
|
|
808
|
+
if not node.args:
|
|
809
|
+
raise ParseError(
|
|
810
|
+
"@check requires a name: @check <name> { ... }"
|
|
811
|
+
)
|
|
812
|
+
check_name = str(node.args[0])
|
|
813
|
+
expr = CheckMethod(parent=parent, name=check_name)
|
|
814
|
+
self._parse_expressions(node.children, expr)
|
|
815
|
+
parent.body.append(expr)
|
|
780
816
|
elif node.name == "@split-doc":
|
|
781
817
|
expr = SplitDoc(parent=parent)
|
|
782
818
|
self._parse_expressions(node.children, expr)
|
|
@@ -836,7 +872,7 @@ class AstParser:
|
|
|
836
872
|
if not node.args:
|
|
837
873
|
raise ParseError(
|
|
838
874
|
"@error requires status and schema name: "
|
|
839
|
-
|
|
875
|
+
"@error <status> <Schema> [field=value ...]"
|
|
840
876
|
)
|
|
841
877
|
if len(node.args) < 2:
|
|
842
878
|
raise ParseError(
|
|
@@ -852,23 +888,25 @@ class AstParser:
|
|
|
852
888
|
schema_name = str(
|
|
853
889
|
self.ctx.property_defines.get(node.args[1], node.args[1])
|
|
854
890
|
)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
self.ctx.property_defines.get(
|
|
891
|
+
conditions: dict[str, Any] = {}
|
|
892
|
+
for k, v in node.properties.items():
|
|
893
|
+
key = str(
|
|
894
|
+
self.ctx.property_defines.get(k, k)
|
|
859
895
|
)
|
|
896
|
+
val = self.ctx.property_defines.get(v, v)
|
|
897
|
+
conditions[key] = val
|
|
860
898
|
err = ErrorResponse(
|
|
861
899
|
parent=parent,
|
|
862
900
|
status=status_int,
|
|
863
901
|
schema_name=schema_name,
|
|
864
|
-
|
|
902
|
+
conditions=conditions,
|
|
865
903
|
)
|
|
866
904
|
parent.body.append(err)
|
|
867
905
|
logger.debug(
|
|
868
|
-
" @error: status=%d schema=%r
|
|
906
|
+
" @error: status=%d schema=%r conditions=%r",
|
|
869
907
|
status_int,
|
|
870
908
|
schema_name,
|
|
871
|
-
|
|
909
|
+
conditions,
|
|
872
910
|
)
|
|
873
911
|
else:
|
|
874
912
|
if parent.struct_type == StructType.TABLE:
|
|
@@ -1310,6 +1348,24 @@ _FLOAT_RE = _re.compile(
|
|
|
1310
1348
|
)
|
|
1311
1349
|
|
|
1312
1350
|
|
|
1351
|
+
_DEFINE_REF_RE = _re.compile(r"\{\{([A-Za-z][A-Za-z0-9_-]*)\}\}")
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def _resolve_define_references(value: str, ctx: ParseContext) -> str:
|
|
1355
|
+
"""Replace {{NAME}} in a scalar define value with previously-defined values."""
|
|
1356
|
+
|
|
1357
|
+
def _replacer(m: _re.Match) -> str:
|
|
1358
|
+
name = m.group(1)
|
|
1359
|
+
resolved = ctx.property_defines.get(name)
|
|
1360
|
+
if resolved is None:
|
|
1361
|
+
raise ParseError(
|
|
1362
|
+
f"define references undefined name {name!r}"
|
|
1363
|
+
)
|
|
1364
|
+
return str(resolved)
|
|
1365
|
+
|
|
1366
|
+
return _DEFINE_REF_RE.sub(_replacer, value)
|
|
1367
|
+
|
|
1368
|
+
|
|
1313
1369
|
# contexts
|
|
1314
1370
|
@PARSER.register_module_context("define")
|
|
1315
1371
|
def reg_define(node: KdlNode, ctx: ParseContext):
|
|
@@ -1322,7 +1378,10 @@ def reg_define(node: KdlNode, ctx: ParseContext):
|
|
|
1322
1378
|
)
|
|
1323
1379
|
else:
|
|
1324
1380
|
pair = list(node.properties.items())[0]
|
|
1325
|
-
|
|
1381
|
+
value = pair[1]
|
|
1382
|
+
if isinstance(value, str):
|
|
1383
|
+
value = _resolve_define_references(value, ctx)
|
|
1384
|
+
ctx.property_defines[pair[0]] = value
|
|
1326
1385
|
logger.debug("define: property %r = %r", pair[0], pair[1])
|
|
1327
1386
|
|
|
1328
1387
|
|
|
@@ -1428,7 +1487,8 @@ def reg_module_struct(node: KdlNode, parent: Module, _: ParseContext):
|
|
|
1428
1487
|
def reg_module_json(node: KdlNode, parent: Module, _: ParseContext):
|
|
1429
1488
|
name = node.args[0]
|
|
1430
1489
|
is_array = node.properties.get("array", False)
|
|
1431
|
-
|
|
1490
|
+
path = str(node.properties.get("path", ""))
|
|
1491
|
+
return JsonDef(parent=parent, name=name, is_array=is_array, path=path)
|
|
1432
1492
|
|
|
1433
1493
|
|
|
1434
1494
|
# expressions layer
|
|
@@ -39,8 +39,9 @@ def _try_prompt_toolkit():
|
|
|
39
39
|
Completion,
|
|
40
40
|
FuzzyWordCompleter,
|
|
41
41
|
)
|
|
42
|
+
from prompt_toolkit.history import InMemoryHistory
|
|
42
43
|
|
|
43
|
-
return pt_prompt, Completer, Completion, FuzzyWordCompleter
|
|
44
|
+
return pt_prompt, Completer, Completion, FuzzyWordCompleter, InMemoryHistory
|
|
44
45
|
except ImportError:
|
|
45
46
|
return None
|
|
46
47
|
|
|
@@ -159,6 +160,12 @@ def _make_completer(repl: Repl):
|
|
|
159
160
|
|
|
160
161
|
return _SsgCompleter()
|
|
161
162
|
|
|
163
|
+
def _make_history():
|
|
164
|
+
if _PROMPT_TOOLKIT is None:
|
|
165
|
+
return
|
|
166
|
+
from prompt_toolkit.history import InMemoryHistory
|
|
167
|
+
|
|
168
|
+
return InMemoryHistory()
|
|
162
169
|
|
|
163
170
|
def _get_converter(target: str):
|
|
164
171
|
mod_path, attr = _CONVERTERS[target].rsplit(":", 1)
|
|
@@ -179,7 +186,7 @@ def _fetch_html(url: str) -> str:
|
|
|
179
186
|
except ImportError:
|
|
180
187
|
raise RuntimeError(
|
|
181
188
|
"httpx is required for URL fetching. "
|
|
182
|
-
"Install with: pip install ssc-
|
|
189
|
+
"Install with: pip install ssc-codegen[repl]"
|
|
183
190
|
)
|
|
184
191
|
|
|
185
192
|
|
|
@@ -224,14 +231,16 @@ class Repl:
|
|
|
224
231
|
self._generated_code: str = ""
|
|
225
232
|
self._multiline_kdl: list[str] | None = None
|
|
226
233
|
self._completer = _make_completer(self)
|
|
234
|
+
self._history = _make_history()
|
|
227
235
|
|
|
228
236
|
def _read_line(self, prompt_str: str) -> str:
|
|
229
237
|
if _PROMPT_TOOLKIT is not None:
|
|
230
|
-
pt_prompt, _, _, _ = _PROMPT_TOOLKIT
|
|
238
|
+
pt_prompt, _, _, _, _ = _PROMPT_TOOLKIT
|
|
231
239
|
return pt_prompt(
|
|
232
240
|
prompt_str,
|
|
233
241
|
completer=self._completer,
|
|
234
242
|
complete_while_typing=True,
|
|
243
|
+
history=self._history
|
|
235
244
|
)
|
|
236
245
|
return input(prompt_str)
|
|
237
246
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|