ssc_codegen 0.23.0__tar.gz → 0.25.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.
Files changed (60) hide show
  1. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/PKG-INFO +1 -1
  2. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/pyproject.toml +1 -1
  3. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/jsondef.py +1 -1
  4. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/struct.py +4 -1
  5. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/js_pure.py +8 -2
  6. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/py_helpers.py +8 -2
  7. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/linting.py +41 -10
  8. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/module_handler.py +2 -4
  9. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/struct_parser.py +16 -1
  10. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/.gitignore +0 -0
  11. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/LICENSE +0 -0
  12. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/README.md +0 -0
  13. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/__init__.py +0 -0
  14. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/_logging.py +0 -0
  15. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/__init__.py +0 -0
  16. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/array.py +0 -0
  17. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/base.py +0 -0
  18. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/cast.py +0 -0
  19. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/control.py +0 -0
  20. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/extract.py +0 -0
  21. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/helpers.py +0 -0
  22. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/module.py +0 -0
  23. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/predicate_containers.py +0 -0
  24. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/predicate_ops.py +0 -0
  25. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/regex.py +0 -0
  26. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/selectors.py +0 -0
  27. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/string.py +0 -0
  28. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/transform.py +0 -0
  29. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/typedef.py +0 -0
  30. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/ast/types.py +0 -0
  31. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/base.py +0 -0
  32. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/helpers.py +0 -0
  33. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/py_bs4.py +0 -0
  34. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/py_lxml.py +0 -0
  35. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/py_parsel.py +0 -0
  36. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/py_render.py +0 -0
  37. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/py_slax.py +0 -0
  38. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/converters/request_spec.py +0 -0
  39. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/__init__.py +0 -0
  40. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/adapter.py +0 -0
  41. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/contexts.py +0 -0
  42. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/expressions.py +0 -0
  43. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/format.py +0 -0
  44. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/predicates.py +0 -0
  45. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/reader.py +0 -0
  46. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/core/type_checking.py +0 -0
  47. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/document_utils.py +0 -0
  48. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/exceptions.py +0 -0
  49. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/health.py +0 -0
  50. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/kdl/__init__.py +0 -0
  51. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/kdl/dict_reader.py +0 -0
  52. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/kdl/parser.py +0 -0
  53. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/kdl/reader.py +0 -0
  54. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/main.py +0 -0
  55. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/parsers/__init__.py +0 -0
  56. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/parsers/curl.py +0 -0
  57. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/parsers/http.py +0 -0
  58. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/pseudo_selectors.py +0 -0
  59. {ssc_codegen-0.23.0 → ssc_codegen-0.25.0}/ssc_codegen/regex_utils.py +0 -0
  60. {ssc_codegen-0.23.0 → ssc_codegen-0.25.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.23.0
3
+ Version: 0.25.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssc_codegen"
3
- version = "0.23.0"
3
+ version = "0.25.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"
@@ -34,7 +34,7 @@ class JsonDefField(Node):
34
34
  class JsonDef(Node):
35
35
  """
36
36
  JSON mapping definition.
37
- DSL: json Name { ... } / json Name array=#true { ... } / json Name path="a.b" { ... }
37
+ DSL: json Name { ... } / (array)json Name { ... } / json Name path="a.b" { ... }
38
38
  body: list[JsonDefField]
39
39
  """
40
40
 
@@ -291,10 +291,12 @@ class RequestConfig(Node):
291
291
  class ErrorResponse(Node):
292
292
  """
293
293
  Error response mapping for type=rest struct.
294
- DSL: @error <status> <SchemaName> [field=value ...]
294
+ DSL: @error <status> <SchemaName> [keys...] [field=value ...]
295
295
 
296
296
  status: HTTP status code [100..599].
297
297
  schema_name: json schema reference for deserialised error body.
298
+ required_keys: key names that must exist in the JSON body (positional args).
299
+ Error triggers on matching status + all keys present.
298
300
  conditions: field=value pairs checked against the parsed JSON body.
299
301
  Keys are dot-paths (e.g. "response.success", "data.0.type").
300
302
  When non-empty, the error triggers on matching status + all conditions.
@@ -302,6 +304,7 @@ class ErrorResponse(Node):
302
304
 
303
305
  status: int = 0
304
306
  schema_name: str = ""
307
+ required_keys: list[str] = field(default_factory=list)
305
308
  conditions: dict[str, Any] = field(default_factory=dict)
306
309
 
307
310
 
@@ -348,6 +348,8 @@ def pre_typedef_field(node: a.TypeDefField, ctx: ConverterContext):
348
348
 
349
349
  def _js_err_subclass_name(struct_name: str, err) -> str:
350
350
  base = f"{to_pascal_case(struct_name)}Err{err.status}"
351
+ for key in err.required_keys:
352
+ base += to_pascal_case(key.replace(".", "_").replace("-", "_"))
351
353
  if err.conditions:
352
354
  for key in err.conditions:
353
355
  base += to_pascal_case(key.replace(".", "_").replace("-", "_"))
@@ -369,6 +371,8 @@ def _js_resolve_path_expr(body_var: str, path: str) -> str:
369
371
  def _js_condition_check_expr(err) -> str:
370
372
  """Build a compound JS condition expression for @error field conditions."""
371
373
  parts = []
374
+ for key in err.required_keys:
375
+ parts.append(f"{key!r} in _body")
372
376
  for path, value in err.conditions.items():
373
377
  lhs = _js_resolve_path_expr("_body", path)
374
378
  if isinstance(value, bool):
@@ -440,8 +444,10 @@ def _emit_dispatch_err_js(node: a.Struct, ctx: ConverterContext) -> list[str]:
440
444
  i3 = i2 + ctx.indent_char # nested (if ...)
441
445
 
442
446
  errors = node.errors
443
- status_errors = [e for e in errors if not e.conditions]
444
- field_errors = [e for e in errors if e.conditions]
447
+ status_errors = [
448
+ e for e in errors if not e.conditions and not e.required_keys
449
+ ]
450
+ field_errors = [e for e in errors if e.conditions or e.required_keys]
445
451
 
446
452
  lines: list[str] = [
447
453
  f"{i1}static _dispatchErr(_status, _headers, _body) {{",
@@ -257,6 +257,8 @@ def rest_utilities(node: a.Node) -> list[str]:
257
257
  def err_subclass_name(struct_name: str, err: a.ErrorResponse) -> str:
258
258
  """Naming: ``<Struct>Err<Status>[<ConditionKeys>]``."""
259
259
  base = f"{to_pascal_case(struct_name)}Err{err.status}"
260
+ for key in err.required_keys:
261
+ base += to_pascal_case(key.replace(".", "_").replace("-", "_"))
260
262
  if err.conditions:
261
263
  for key in err.conditions:
262
264
  base += to_pascal_case(key.replace(".", "_").replace("-", "_"))
@@ -304,6 +306,8 @@ def _resolve_path_expr(body_var: str, path: str) -> str:
304
306
  def _condition_check_expr(err: a.ErrorResponse) -> str:
305
307
  """Build a compound condition expression for an @error's field conditions."""
306
308
  parts: list[str] = []
309
+ for key in err.required_keys:
310
+ parts.append(f"{key!r} in _body")
307
311
  for path, value in err.conditions.items():
308
312
  lhs = _resolve_path_expr("_body", path)
309
313
  if isinstance(value, bool):
@@ -325,8 +329,10 @@ def emit_dispatch_err_py(node: a.Struct, ctx: ConverterContext) -> list[str]:
325
329
  i4 = i3 + ctx.indent_char
326
330
 
327
331
  errors = node.errors
328
- status_errors = [e for e in errors if not e.conditions]
329
- field_errors = [e for e in errors if e.conditions]
332
+ status_errors = [
333
+ e for e in errors if not e.conditions and not e.required_keys
334
+ ]
335
+ field_errors = [e for e in errors if e.conditions or e.required_keys]
330
336
  union_type = _rest_err_union_type(node)
331
337
 
332
338
  lines: list[str] = [
@@ -776,6 +776,26 @@ def lint_reserved_field(
776
776
  message=f"@check {check_name or ''}must contain 'to-bool' to guarantee BOOL return type",
777
777
  code="E100",
778
778
  )
779
+ elif field_name == "@error":
780
+ err_args = lint.get_args(node)
781
+ if len(err_args) < 2:
782
+ lint.error(
783
+ node,
784
+ message="@error requires both status and schema name",
785
+ code="E001",
786
+ hint="example: @error 404 ApiError",
787
+ )
788
+ return
789
+ positional_keys = set(err_args[2:])
790
+ property_keys = set(node.properties.keys())
791
+ duplicates = positional_keys & property_keys
792
+ if duplicates:
793
+ lint.error(
794
+ node,
795
+ message=f"@error has duplicate keys: {', '.join(sorted(duplicates))}",
796
+ code="E400",
797
+ hint="each key must be either a positional arg (presence check) or a property (value check)",
798
+ )
779
799
 
780
800
 
781
801
  def lint_regular_field(
@@ -932,15 +952,6 @@ def lint_json_node(node: KdlNode, lint: LintContext, ctx: ParseContext) -> None:
932
952
  hint=f"rename or remove one of the 'json {name}' definitions",
933
953
  )
934
954
 
935
- array_prop = node.properties.get("array")
936
- if array_prop is not None and not isinstance(array_prop.value, bool):
937
- lint.error(
938
- node,
939
- message=f"'array' property must be boolean (#true/#false), got {array_prop.value!r}",
940
- code="E002",
941
- hint="example: json MySchema array=#true { ... }",
942
- )
943
-
944
955
  path_prop = node.properties.get("path")
945
956
  if path_prop is not None:
946
957
  path_val = str(path_prop.value)
@@ -955,9 +966,29 @@ def lint_json_node(node: KdlNode, lint: LintContext, ctx: ParseContext) -> None:
955
966
  # ── field-level checks ───────────────────────────────────────────────────
956
967
 
957
968
  seen_fields: set[str] = set()
958
- for field_node in lint.get_children_nodes(node):
969
+ _lint_json_children(lint.get_children_nodes(node), lint, ctx, seen_fields)
970
+
971
+
972
+ def _lint_json_children(
973
+ children: list[KdlNode],
974
+ lint: LintContext,
975
+ ctx: ParseContext,
976
+ seen_fields: set[str],
977
+ ) -> None:
978
+ """Lint json field children, expanding block define references."""
979
+ for field_node in children:
959
980
  field_name = lint.node_name(field_node)
960
981
  args = lint.get_args(field_node)
982
+
983
+ # Block define expansion
984
+ if not args and field_name in ctx.children_defines:
985
+ lint.push(field_name)
986
+ _lint_json_children(
987
+ ctx.children_defines[field_name], lint, ctx, seen_fields
988
+ )
989
+ lint.pop()
990
+ continue
991
+
961
992
  has_type = False
962
993
  has_skip = False
963
994
  for arg in args:
@@ -83,9 +83,7 @@ def handle_json(
83
83
  node: KdlNode, module: Module, ctx: ParseContext, lint: LintContext
84
84
  ) -> JsonDef:
85
85
  name = str(node.args[0].value) if node.args else ""
86
- is_array = node.properties.get(
87
- "array", KdlArg(value=False, span=node.span, is_identifier=False)
88
- ).value
86
+ is_array = node.type_annotation == "(array)"
89
87
  path = str(
90
88
  node.properties.get(
91
89
  "path", KdlArg(value="", span=node.span, is_identifier=False)
@@ -93,7 +91,7 @@ def handle_json(
93
91
  )
94
92
  json_def = JsonDef(parent=module, name=name, is_array=is_array, path=path)
95
93
  lint_json_node(node, lint, ctx)
96
- parse_json_fields(node.children, json_def)
94
+ parse_json_fields(node.children, json_def, ctx)
97
95
  ctx.json_defs[json_def.name] = json_def
98
96
  if name:
99
97
  lint.json_kdl_nodes[name] = node
@@ -146,6 +146,14 @@ def parse_struct(
146
146
  schema_name = str(
147
147
  ctx.property_defines.get(node.args[1].value, node.args[1].value)
148
148
  )
149
+ required_keys: list[str] = []
150
+ for i in range(2, len(node.args)):
151
+ key = str(
152
+ ctx.property_defines.get(
153
+ node.args[i].value, node.args[i].value
154
+ )
155
+ )
156
+ required_keys.append(key)
149
157
  conditions: dict[str, Any] = {}
150
158
  for k, v in node.properties.items():
151
159
  key = str(ctx.property_defines.get(k, k))
@@ -155,6 +163,7 @@ def parse_struct(
155
163
  parent=parent,
156
164
  status=status_int,
157
165
  schema_name=schema_name,
166
+ required_keys=required_keys,
158
167
  conditions=conditions,
159
168
  )
160
169
  parent.body.append(err)
@@ -175,8 +184,14 @@ def parse_struct(
175
184
  lint.walk_context = prev_ctx
176
185
 
177
186
 
178
- def parse_json_fields(nodes: Sequence[KdlNode], parent: JsonDef) -> None:
187
+ def parse_json_fields(
188
+ nodes: Sequence[KdlNode], parent: JsonDef, ctx: ParseContext
189
+ ) -> None:
179
190
  for node in nodes:
191
+ # Block define expansion in json context
192
+ if not node.args and node.name in ctx.children_defines:
193
+ parse_json_fields(ctx.children_defines[node.name], parent, ctx)
194
+ continue
180
195
  name = node.name
181
196
  modifiers: list[str] = []
182
197
  type_ = ""
File without changes
File without changes
File without changes