ssc_codegen 0.34.0__tar.gz → 0.36.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 (88) hide show
  1. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/PKG-INFO +2 -2
  2. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/pyproject.toml +1 -1
  3. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/rest.py +43 -23
  4. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/runtime.py +14 -0
  5. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/visitor.py +14 -1
  6. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/javascript/rest.py +61 -24
  7. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/rest.py +58 -37
  8. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/.gitignore +0 -0
  9. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/LICENSE +0 -0
  10. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/README.md +0 -0
  11. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/__init__.py +0 -0
  12. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/_logging.py +0 -0
  13. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/__init__.py +0 -0
  14. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/array.py +0 -0
  15. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/base.py +0 -0
  16. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/cast.py +0 -0
  17. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/control.py +0 -0
  18. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/extract.py +0 -0
  19. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/function.py +0 -0
  20. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/helpers.py +0 -0
  21. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/jsondef.py +0 -0
  22. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/module.py +0 -0
  23. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/predicate_containers.py +0 -0
  24. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/predicate_ops.py +0 -0
  25. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/regex.py +0 -0
  26. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/rest.py +0 -0
  27. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/selectors.py +0 -0
  28. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/string.py +0 -0
  29. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/struct.py +0 -0
  30. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/typedef.py +0 -0
  31. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/ast/types.py +0 -0
  32. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/__init__.py +0 -0
  33. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/contexts.py +0 -0
  34. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/expressions.py +0 -0
  35. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/format.py +0 -0
  36. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/linter.py +0 -0
  37. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/module_handler.py +0 -0
  38. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/predicates.py +0 -0
  39. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/reader.py +0 -0
  40. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/rest_artifacts.py +0 -0
  41. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/struct_parser.py +0 -0
  42. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/core/type_checking.py +0 -0
  43. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/exceptions.py +0 -0
  44. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/explore.py +0 -0
  45. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/generation/__init__.py +0 -0
  46. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/generation/builder.py +0 -0
  47. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/generation/runtime.py +0 -0
  48. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/health.py +0 -0
  49. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/main.py +0 -0
  50. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/naming.py +0 -0
  51. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/parsers/__init__.py +0 -0
  52. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/parsers/curl.py +0 -0
  53. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/parsers/http.py +0 -0
  54. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/regex_utils.py +0 -0
  55. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/request_spec.py +0 -0
  56. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/__init__.py +0 -0
  57. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/__init__.py +0 -0
  58. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/http_libs/__init__.py +0 -0
  59. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/http_libs/base.py +0 -0
  60. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/http_libs/nethttp.py +0 -0
  61. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/literals.py +0 -0
  62. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/golang/regex.py +0 -0
  63. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/javascript/__init__.py +0 -0
  64. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/javascript/http_libs/__init__.py +0 -0
  65. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/javascript/http_libs/axios.py +0 -0
  66. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/javascript/http_libs/base.py +0 -0
  67. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/javascript/http_libs/fetch.py +0 -0
  68. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/javascript/visitor.py +0 -0
  69. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/profile.py +0 -0
  70. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/__init__.py +0 -0
  71. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/html_libs/__init__.py +0 -0
  72. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/html_libs/base.py +0 -0
  73. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/html_libs/bs4.py +0 -0
  74. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/html_libs/lxml.py +0 -0
  75. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/html_libs/parsel.py +0 -0
  76. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/html_libs/slax.py +0 -0
  77. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/http_libs/__init__.py +0 -0
  78. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/http_libs/aiohttp.py +0 -0
  79. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/http_libs/base.py +0 -0
  80. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/http_libs/httpx.py +0 -0
  81. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/http_libs/requests.py +0 -0
  82. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/python/visitor.py +0 -0
  83. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/resolver.py +0 -0
  84. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/targets/spec.py +0 -0
  85. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/traversal/__init__.py +0 -0
  86. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/traversal/context.py +0 -0
  87. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/traversal/utils.py +0 -0
  88. {ssc_codegen-0.34.0 → ssc_codegen-0.36.0}/ssc_codegen/traversal/walker.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: ssc_codegen
3
- Version: 0.34.0
3
+ Version: 0.36.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.34.0"
3
+ version = "0.36.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"
@@ -185,9 +185,14 @@ def emit_method_fetch(
185
185
  ctx: WalkContext,
186
186
  http: GoHttpLibStrategy,
187
187
  ) -> list[str]:
188
- """Emit a Go Fetch method for HTML-parser structs with ``@request``.
188
+ """Emit a Go HTTP-entry function for HTML-parser structs with ``@request``.
189
189
 
190
- Signature: ``func Fetch(ctx context.Context, client *http.Client, <phs>) (*Name, error)``
190
+ Signature: ``func New<Struct><Method>(ctx, client, <phs>, opts ...) (*Name, error)``
191
+
192
+ Free function (not a receiver method) — semantically a sibling of
193
+ ``New<Name>(input string)`` that fetches the input from HTTP instead.
194
+ Struct-name prefix guarantees uniqueness across schemas in the same
195
+ package (default ``New<Name>Fetch``; ``@request name=X`` → ``New<Name>X``).
191
196
 
192
197
  Idiomatic Go: cancellation via ``context.Context``, async via ``go f(ctx)``
193
198
  on caller side. Headers/body inlined (no ``sscReqOpts`` dependency).
@@ -198,7 +203,8 @@ def emit_method_fetch(
198
203
  struct = node.parent
199
204
  assert isinstance(struct, StructBase)
200
205
  name = to_pascal_case(struct.name)
201
- method_name = to_pascal_case(node.name) if node.name else "Fetch"
206
+ method_suffix = to_pascal_case(node.name) if node.name else "Fetch"
207
+ func_name = f"New{name}{method_suffix}"
202
208
  ph_params = placeholder_params(spec)
203
209
  url_expr = render_url(spec.url)
204
210
 
@@ -206,7 +212,7 @@ def emit_method_fetch(
206
212
  i2 = i1 + ctx.indent_char
207
213
 
208
214
  lines: list[str] = [
209
- f"{i1}func {method_name}(ctx context.Context, client {http.client_type}{ph_params}) (*{name}, error) {{",
215
+ f"{i1}func {func_name}(ctx context.Context, client {http.client_type}{ph_params}, opts ...sscReqOpt) (*{name}, error) {{",
210
216
  f"{i2}req, err := http.NewRequestWithContext(ctx, {_go_str(spec.method)}, {url_expr}, nil)",
211
217
  f"{i2}if err != nil {{",
212
218
  f"{i2}\treturn nil, err",
@@ -233,6 +239,15 @@ def emit_method_fetch(
233
239
  f"{i2}req.Body = io.NopCloser(strings.NewReader({body_expr}))"
234
240
  )
235
241
 
242
+ # Apply per-call opts (user headers are additive).
243
+ lines.append(f"{i2}if len(opts) > 0 {{")
244
+ lines.append(f"{i2}\t_opts := &sscReqOpts{{}}")
245
+ lines.append(f"{i2}\tfor _, o := range opts {{ o(_opts) }}")
246
+ lines.append(f"{i2}\tfor _k, _vs := range _opts.Headers {{")
247
+ lines.append(f"{i2}\t\tfor _, _v := range _vs {{ req.Header.Add(_k, _v) }}")
248
+ lines.append(f"{i2}\t}}")
249
+ lines.append(f"{i2}}}")
250
+
236
251
  # Execute.
237
252
  lines.extend(
238
253
  [
@@ -284,6 +299,7 @@ def emit_method_rest(
284
299
  node: MethodRest,
285
300
  ctx: WalkContext,
286
301
  http: GoHttpLibStrategy,
302
+ rcv: str,
287
303
  ) -> list[str]:
288
304
  """Emit a Go REST method that calls sscRestCall.
289
305
 
@@ -291,6 +307,10 @@ def emit_method_rest(
291
307
  - typed value → ``(*SchemaJson, error)`` or ``([]SchemaJson, error)``
292
308
  - void → ``(struct{}, error)``
293
309
 
310
+ Receiver method on the (empty) marker struct — namespaced by type so
311
+ multiple REST structs in one package don't collide. Caller writes
312
+ ``NewApiX().Fetch(client, opts...)`` (factory + receiver).
313
+
294
314
  Errors propagate as typed structs: ``@error`` variants via matchers,
295
315
  ``*UnknownErr`` for unmatched HTTP >= 400, ``*TransportErr`` for network.
296
316
  Caller uses ``errors.As`` to discriminate.
@@ -298,6 +318,7 @@ def emit_method_rest(
298
318
  spec = node.http_request.with_renamed_placeholders(to_camel_case)
299
319
  struct = node.parent
300
320
  assert isinstance(struct, StructBase)
321
+ name = to_pascal_case(struct.name)
301
322
  method_name = to_pascal_case(node.name) if node.name else "Fetch"
302
323
  ph_params = placeholder_params(spec)
303
324
  matchers_var = f"{to_snake_case(struct.name)}Matchers"
@@ -323,31 +344,30 @@ def emit_method_rest(
323
344
 
324
345
  lines: list[str] = []
325
346
 
326
- # Function signature.
347
+ # Function signature (receiver method on marker struct).
327
348
  lines.append(
328
- f"{i1}func {method_name}(client {http.client_type}{ph_params}) ({ret_type}, error) {{"
349
+ f"{i1}func ({rcv} {name}) {method_name}(client {http.client_type}{ph_params}, opts ...sscReqOpt) ({ret_type}, error) {{"
329
350
  )
330
351
 
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.
352
+ # Build _opts from DSL, apply per-call user opts, then call sscRestCall.
353
+ opts_lines = _build_opts(spec, i2 + "\t")
336
354
  body_var = "_" if not node.response_schema else "body"
337
- if len(opts_lines) == 1:
338
- lines.append(
339
- f"{i2}{body_var}, err := sscRestCall(client, {matchers_var}, "
340
- f"{_go_str(spec.method)}, {url_expr}, {opts_lines[0].strip()})"
341
- )
355
+
356
+ # Assign DSL opts to _opts variable.
357
+ if len(opts_lines) == 1 and opts_lines[0].strip() == "nil":
358
+ lines.append(f"{i2}_opts := &sscReqOpts{{}}")
342
359
  else:
343
- lines.append(
344
- f"{i2}{body_var}, err := sscRestCall(client, {matchers_var}, "
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]:
360
+ lines.append(f"{i2}_opts := {opts_lines[0].strip()}")
361
+ for ol in opts_lines[1:]:
349
362
  lines.append(ol)
350
- lines.append(f"{opts_lines[-1].strip()})")
363
+
364
+ # Apply per-call opts (functional options).
365
+ lines.append(f"{i2}for _, o := range opts {{ o(_opts) }}")
366
+
367
+ lines.append(
368
+ f"{i2}{body_var}, err := sscRestCall(client, {matchers_var}, "
369
+ f"{_go_str(spec.method)}, {url_expr}, _opts)"
370
+ )
351
371
 
352
372
  # Error propagation.
353
373
  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",
@@ -596,6 +596,12 @@ class GoVisitor(BaseWalker):
596
596
  lines.append(f"type {name} struct {{")
597
597
  lines.append("}")
598
598
  lines.append("")
599
+ # Empty-struct factory: caller writes New<Name>().Method(...) —
600
+ # the struct is a pure namespace marker (no state), receiver
601
+ # methods namespaced by type avoid collisions across schemas in
602
+ # the same package.
603
+ lines.append(f"func New{name}() {name} {{ return {name}{{}} }}")
604
+ lines.append("")
599
605
  for child in node.body:
600
606
  lines.extend(self.walk(child, ctx))
601
607
  return lines
@@ -1885,7 +1891,14 @@ class GoVisitor(BaseWalker):
1885
1891
  if spec.body_kind == "form" and isinstance(spec.body, dict):
1886
1892
  self._builder.require_import('"net/url"')
1887
1893
  self._builder.require_import('"' + self._http.import_path + '"')
1888
- return rest.emit_method_rest(node, ctx, self._http)
1894
+ struct = node.parent
1895
+ name = (
1896
+ to_pascal_case(struct.name)
1897
+ if isinstance(struct, StructBase)
1898
+ else "X"
1899
+ )
1900
+ rcv = _receiver(name)
1901
+ return rest.emit_method_rest(node, ctx, self._http, rcv)
1889
1902
 
1890
1903
  def visit_method_fetch(
1891
1904
  self, node: MethodFetch, ctx: WalkContext
@@ -390,13 +390,28 @@ 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(
394
+ f"{i1} * @param {{Object}} [opts] per-call request options (headers, etc.)"
395
+ )
393
396
  lines.append(f"{i1} * @returns {{Promise<{return_union}>}}")
394
397
  lines.append(f"{i1} */")
395
- lines.append(f"{i1}static async {method_name}(client{ph_param}) {{")
398
+ lines.append(
399
+ f"{i1}static async {method_name}(client{ph_param}, opts = {{}}) {{"
400
+ )
396
401
  lines.extend(pre_lines)
402
+ lines.append(f"{i2}const _kw = {opts_obj};")
403
+ lines.append(f"{i2}for (const [_k, _v] of Object.entries(opts)) {{")
404
+ lines.append(
405
+ f"{i2} if (typeof _kw[_k] === 'object' && _kw[_k] !== null && typeof _v === 'object' && _v !== null) {{"
406
+ )
407
+ lines.append(f"{i2} _kw[_k] = {{..._kw[_k], ..._v}};")
408
+ lines.append(f"{i2} }} else {{")
409
+ lines.append(f"{i2} _kw[_k] = _v;")
410
+ lines.append(f"{i2} }}")
411
+ lines.append(f"{i2}}}")
397
412
  lines.append(
398
413
  f"{i2}return {fn_name}(client, {matchers_var}, "
399
- f"{spec.method!r}, {url_expr}, {value_fn}, {opts_obj});"
414
+ f"{spec.method!r}, {url_expr}, {value_fn}, _kw);"
400
415
  )
401
416
  lines.append(f"{i1}}}")
402
417
  return lines
@@ -434,9 +449,13 @@ def emit_method_fetch(
434
449
  rl.append(f"{i2}return new {struct_name}(_body);")
435
450
  return rl
436
451
 
437
- lines: list[str] = [f"{i1}static async {method_name}(client{ph_param}) {{"]
452
+ lines: list[str] = [
453
+ f"{i1}static async {method_name}(client{ph_param}, opts = {{}}) {{"
454
+ ]
438
455
  lines.extend(pre_lines)
439
456
 
457
+ # Build _kw from DSL-specified request options (method/url stay out).
458
+ kw_parts: list[str] = []
440
459
  if http_client == "fetch":
441
460
  if params_expr:
442
461
  url_inner = spec.url.map(
@@ -446,21 +465,32 @@ def emit_method_fetch(
446
465
  url_expr = f"`{url_inner}?${{new URLSearchParams({params_expr})}}`"
447
466
  else:
448
467
  url_expr = render_value(spec.url)
449
- options: list[str] = [f"{i3}method: {spec.method!r},"]
450
468
  if headers_expr:
451
- options.append(f"{i3}headers: {headers_expr},")
469
+ kw_parts.append(f"headers: {headers_expr}")
452
470
  if spec.cookies:
453
471
  cookie_str = "; ".join(
454
472
  f"{k}={v.source}" for k, v in spec.cookies.items()
455
473
  )
456
- options.append(
457
- f"{i3}// cookies: {cookie_str!r} /* set via headers or credentials */"
474
+ kw_parts.append(
475
+ f"// cookies: {cookie_str!r} /* set via headers or credentials */"
458
476
  )
459
477
  if body_expr:
460
- options.append(f"{i3}body: {body_expr},")
461
- lines.append(f"{i2}const _resp = await client({url_expr}, {{")
462
- lines.extend(options)
463
- lines.append(f"{i2}}});")
478
+ kw_parts.append(f"body: {body_expr}")
479
+ kw_obj = "{" + ", ".join(kw_parts) + "}" if kw_parts else "{}"
480
+ lines.append(f"{i2}const _kw = {kw_obj};")
481
+ lines.append(f"{i2}for (const [_k, _v] of Object.entries(opts)) {{")
482
+ lines.append(
483
+ f"{i2} if (typeof _kw[_k] === 'object' && _kw[_k] !== null && typeof _v === 'object' && _v !== null) {{"
484
+ )
485
+ lines.append(f"{i2} _kw[_k] = {{..._kw[_k], ..._v}};")
486
+ lines.append(f"{i2} }} else {{")
487
+ lines.append(f"{i2} _kw[_k] = _v;")
488
+ lines.append(f"{i2} }}")
489
+ lines.append(f"{i2}}}")
490
+ lines.append(
491
+ f"{i2}const _resp = await client({url_expr},"
492
+ f" {{ method: {spec.method!r}, ..._kw }});"
493
+ )
464
494
  lines.append(
465
495
  f"{i2}if (!_resp.ok) throw new Error(`HTTP ${{_resp.status}}`);"
466
496
  )
@@ -469,23 +499,30 @@ def emit_method_fetch(
469
499
  else:
470
500
  lines.extend(_response_lines("await _resp.text()"))
471
501
  else:
472
- req_props: list[str] = [
473
- f"{i3}method: {spec.method!r},",
474
- f"{i3}url: {render_value(spec.url)},",
475
- ]
502
+ url_expr = render_value(spec.url)
476
503
  if params_expr:
477
- req_props.append(f"{i3}params: {params_expr},")
504
+ kw_parts.append(f"params: {params_expr}")
478
505
  if headers_expr:
479
- req_props.append(f"{i3}headers: {headers_expr},")
506
+ kw_parts.append(f"headers: {headers_expr}")
480
507
  if spec.cookies:
481
- req_props.append(
482
- f"{i3}// cookies: {render_obj(spec.cookies)},",
483
- )
508
+ kw_parts.append(f"// cookies: {render_obj(spec.cookies)},")
484
509
  if body_expr:
485
- req_props.append(f"{i3}data: {body_expr},")
486
- lines.append(f"{i2}const _resp = await client.request({{")
487
- lines.extend(req_props)
488
- lines.append(f"{i2}}});")
510
+ kw_parts.append(f"data: {body_expr}")
511
+ kw_obj = "{" + ", ".join(kw_parts) + "}" if kw_parts else "{}"
512
+ lines.append(f"{i2}const _kw = {kw_obj};")
513
+ lines.append(f"{i2}for (const [_k, _v] of Object.entries(opts)) {{")
514
+ lines.append(
515
+ f"{i2} if (typeof _kw[_k] === 'object' && _kw[_k] !== null && typeof _v === 'object' && _v !== null) {{"
516
+ )
517
+ lines.append(f"{i2} _kw[_k] = {{..._kw[_k], ..._v}};")
518
+ lines.append(f"{i2} }} else {{")
519
+ lines.append(f"{i2} _kw[_k] = _v;")
520
+ lines.append(f"{i2} }}")
521
+ lines.append(f"{i2}}}")
522
+ lines.append(
523
+ f"{i2}const _resp = await client.request({{"
524
+ f" method: {spec.method!r}, url: {url_expr}, ..._kw }});"
525
+ )
489
526
  lines.extend(_response_lines("_resp.data"))
490
527
 
491
528
  lines.append(f"{i1}}}")
@@ -296,32 +296,44 @@ def placeholder_args(http: RequestHttp) -> str:
296
296
  # ===========================================================================
297
297
 
298
298
 
299
- def _build_kwargs(
300
- spec: RequestHttp, i2: str, i3: str, include_method_url: bool
299
+ def _build_kw_dict(
300
+ spec: RequestHttp, i2: str, i3: str
301
301
  ) -> tuple[list[str], list[str]]:
302
- """Build (pre_lines, kwargs_lines) for request call.
302
+ """Build (pre_lines, kw_assign_lines) for ``_kw`` dict construction.
303
303
 
304
- include_method_url=True for fetch (client.request(method, url, ...)).
305
- include_method_url=False for rest (ssc_rest_call(client, matchers, method, url, ...)).
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
- kwargs_lines: list[str] = []
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
- kwargs_lines.append(f"{i3}{attr}={varname},")
319
+ kw_lines.append(f'{i2}_kw["{attr}"] = {varname}')
319
320
  else:
320
- kwargs_lines.append(f"{i3}{attr}={render_dict(d)},")
321
+ kw_lines.append(f'{i2}_kw["{attr}"] = {render_dict(d)}')
321
322
  body_result = render_body(spec)
322
323
  if body_result:
323
- kwargs_lines.append(f"{i3}{body_result[0]}={body_result[1]},")
324
- return pre_lines, kwargs_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, kwargs_lines = _build_kwargs(
347
- spec, i2, i3, include_method_url=True
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(pre_lines)
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=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
- to_thread_args = f"client, {ph_kwargs}" if ph_kwargs else "client"
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(pre_lines)
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=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, kwargs_lines = _build_kwargs(
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
- # Wrap in cast(): ssc_rest_call returns Union[Ok[_T], Err] (Err
436
- # base — it cannot know the specific Err subclasses that the
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.extend(kwargs_lines)
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
File without changes
File without changes
File without changes