ssc_codegen 0.40.0__tar.gz → 0.42.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 (98) hide show
  1. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/PKG-INFO +1 -1
  2. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/pyproject.toml +124 -124
  3. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/generation/builder.py +9 -2
  4. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/generation/runtime.py +9 -3
  5. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/main.py +39 -1
  6. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/naming.py +17 -0
  7. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/javascript/rest.py +31 -105
  8. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/javascript/visitor.py +32 -58
  9. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/html_libs/base.py +5 -1
  10. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/html_libs/bs4.py +59 -9
  11. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/http_libs/aiohttp.py +81 -59
  12. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/http_libs/base.py +6 -1
  13. ssc_codegen-0.42.0/ssc_codegen/targets/python/http_libs/httpx.py +181 -0
  14. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/http_libs/requests.py +67 -44
  15. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/rest.py +121 -149
  16. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/visitor.py +167 -144
  17. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/resolver.py +282 -227
  18. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/spec.py +67 -61
  19. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/traversal/utils.py +17 -23
  20. ssc_codegen-0.40.0/ssc_codegen/targets/python/http_libs/httpx.py +0 -139
  21. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/.gitignore +0 -0
  22. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/LICENSE +0 -0
  23. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/README.md +0 -0
  24. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/__init__.py +0 -0
  25. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/_logging.py +0 -0
  26. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/__init__.py +0 -0
  27. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/array.py +0 -0
  28. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/base.py +0 -0
  29. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/cast.py +0 -0
  30. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/control.py +0 -0
  31. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/extension.py +0 -0
  32. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/extract.py +0 -0
  33. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/function.py +0 -0
  34. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/jsondef.py +0 -0
  35. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/module.py +0 -0
  36. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/predicate_containers.py +0 -0
  37. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/predicate_ops.py +0 -0
  38. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/regex.py +0 -0
  39. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/rest.py +0 -0
  40. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/selectors.py +0 -0
  41. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/string.py +0 -0
  42. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/struct.py +0 -0
  43. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/typedef.py +0 -0
  44. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/ast/types.py +0 -0
  45. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/__init__.py +0 -0
  46. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/contexts.py +0 -0
  47. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/expressions.py +0 -0
  48. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/extensions.py +0 -0
  49. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/format.py +0 -0
  50. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/imports.py +0 -0
  51. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/linter.py +0 -0
  52. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/module_handler.py +0 -0
  53. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/predicates.py +0 -0
  54. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/reader.py +0 -0
  55. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/rest_artifacts.py +0 -0
  56. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/struct_parser.py +0 -0
  57. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/core/type_checking.py +0 -0
  58. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/exceptions.py +0 -0
  59. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/explore.py +0 -0
  60. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/generation/__init__.py +0 -0
  61. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/health.py +0 -0
  62. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/json_to_kdl.py +0 -0
  63. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/parsers/__init__.py +0 -0
  64. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/parsers/curl.py +0 -0
  65. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/parsers/http.py +0 -0
  66. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/regex_utils.py +0 -0
  67. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/request_spec.py +0 -0
  68. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/symbols.py +0 -0
  69. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/__init__.py +0 -0
  70. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/__init__.py +0 -0
  71. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/http_libs/__init__.py +0 -0
  72. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/http_libs/base.py +0 -0
  73. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/http_libs/nethttp.py +0 -0
  74. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/literals.py +0 -0
  75. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/regex.py +0 -0
  76. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/rest.py +0 -0
  77. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/runtime.py +0 -0
  78. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/golang/visitor.py +0 -0
  79. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/javascript/__init__.py +0 -0
  80. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/javascript/http_libs/__init__.py +0 -0
  81. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/javascript/http_libs/axios.py +0 -0
  82. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/javascript/http_libs/base.py +0 -0
  83. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/javascript/http_libs/fetch.py +0 -0
  84. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/profile.py +0 -0
  85. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/__init__.py +0 -0
  86. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/html_libs/__init__.py +0 -0
  87. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/html_libs/lxml.py +0 -0
  88. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/html_libs/parsel.py +0 -0
  89. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/html_libs/slax.py +0 -0
  90. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/python/http_libs/__init__.py +0 -0
  91. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/rust/__init__.py +0 -0
  92. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/rust/regex.py +0 -0
  93. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/rust/rest.py +0 -0
  94. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/rust/runtime.py +0 -0
  95. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/targets/rust/visitor.py +0 -0
  96. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/traversal/__init__.py +0 -0
  97. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/traversal/context.py +0 -0
  98. {ssc_codegen-0.40.0 → ssc_codegen-0.42.0}/ssc_codegen/traversal/walker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ssc_codegen
3
- Version: 0.40.0
3
+ Version: 0.42.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,124 +1,124 @@
1
- [project]
2
- name = "ssc_codegen"
3
- version = "0.40.0"
4
- description = "Python-dsl code converter to html parser for web scraping "
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- dependencies = [
8
- "bs4>=0.0.2",
9
- "colorama>=0.4.6 ; sys_platform == 'win32'",
10
- "cssselect>=1.2.0",
11
- "lxml>=5.3.0",
12
- "soupsieve>=2.6",
13
- "typer>0.26",
14
- "typing_extensions;python_version<'3.11'",
15
- "kdlquery>=1.3.0",
16
- "httpx>=0.28.1",
17
- ]
18
-
19
- classifiers = [
20
- "Development Status :: 3 - Alpha",
21
- "Environment :: Console",
22
- "Intended Audience :: Developers",
23
- "License :: OSI Approved :: MIT License",
24
- "Topic :: Software Development :: Code Generators",
25
- "Topic :: Text Processing :: Markup :: HTML",
26
- "Topic :: Utilities",
27
- "Programming Language :: Python :: 3.10",
28
- "Programming Language :: Python :: 3.11",
29
- "Programming Language :: Python :: 3.12",
30
- "Programming Language :: Python :: 3.13",
31
- ]
32
-
33
- [project.urls]
34
- Documentation = "https://github.com/vypivshiy/selector_schema_codegen#readme"
35
- Issues = "https://github.com/vypivshiy/selector_schema_codegen/issues"
36
- Source = "https://github.com/vypivshiy/selector_schema_codegen"
37
- Examples = "https://github.com/vypivshiy/selector_schema_codegen/examples"
38
-
39
-
40
- [project.scripts]
41
- ssc-gen = 'ssc_codegen.main:main'
42
-
43
- [build-system]
44
- requires = ["hatchling"]
45
- build-backend = "hatchling.build"
46
-
47
- [tool.hatch.build.targets.sdist]
48
- include = [
49
- "ssc_codegen/**",
50
- ]
51
-
52
- [tool.hatch.build.targets.wheel]
53
- packages = ["ssc_codegen"]
54
-
55
- [dependency-groups]
56
- dev = [
57
- "aiohttp>=3.9,<3.12", # aioresponses 0.7.x breaks on 3.12+ (stream_writer)
58
- "aioresponses>=0.7",
59
- "bs4>=0.0.2",
60
- "coverage>=7.6.12",
61
- "httpx>=0.28.1",
62
- "hypothesis>=6.151.9",
63
- "lxml>=5.3.0",
64
- "mypy>=1.14.1",
65
- "parsel>=1.9.1",
66
- "pytest>=8.3.4",
67
- "pytest-xdist>=3.6.0",
68
- "respx>=0.23.1",
69
- "responses>=0.25",
70
- "ruff>=0.9.3",
71
- "selectolax>=0.3.27",
72
- ]
73
- docs = [
74
- "mkdocs>=1.6.0",
75
- "mkdocs-material>=9.5.0",
76
- "mkdocstrings[python]>=0.27.0",
77
- "mkdocs-click>=0.8.1",
78
- ]
79
-
80
- [tool.ruff]
81
- target-version = "py310"
82
- line-length = 80
83
- exclude = ["__init__.py"]
84
-
85
- [tool.mypy]
86
- python_version = "3.10"
87
- show_error_codes = true
88
- [[tool.mypy.overrides]]
89
- module="tests.*"
90
- disallow_untyped_defs = false
91
-
92
-
93
- [[tool.mypy.overrides]]
94
- module = ["colorama", "lxml", "lxml.*"]
95
- ignore_missing_imports = true
96
-
97
- [tool.coverage.report]
98
- exclude_also = [
99
- 'def __repr__',
100
- 'if self.debug:',
101
- 'if settings.DEBUG',
102
- 'raise AssertionError',
103
- 'raise NotImplementedError',
104
- 'if 0:',
105
- 'if __name__ == .__main__.:',
106
- 'if TYPE_CHECKING:',
107
- 'class .*\bProtocol\):',
108
- '@(abc\.)?abstractmethod',
109
- ]
110
-
111
- [tool.coverage.run]
112
- source = ["ssc_codegen"]
113
- omit = [
114
- "*/.venv/*",
115
- "*/scripts/*",
116
- "*/tests/*"
117
- ]
118
-
119
- [tool.pytest.ini_options]
120
- testpaths = ["tests"]
121
- markers = [
122
- "toolchain: Tests requiring external toolchains (Rust cargo, Go compiler, Node.js)",
123
- ]
124
-
1
+ [project]
2
+ name = "ssc_codegen"
3
+ version = "0.42.0"
4
+ description = "Python-dsl code converter to html parser for web scraping "
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "bs4>=0.0.2",
9
+ "colorama>=0.4.6 ; sys_platform == 'win32'",
10
+ "cssselect>=1.2.0",
11
+ "lxml>=5.3.0",
12
+ "soupsieve>=2.6",
13
+ "typer>0.26",
14
+ "typing_extensions;python_version<'3.11'",
15
+ "kdlquery>=1.3.0",
16
+ "httpx>=0.28.1",
17
+ ]
18
+
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Environment :: Console",
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Topic :: Software Development :: Code Generators",
25
+ "Topic :: Text Processing :: Markup :: HTML",
26
+ "Topic :: Utilities",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ ]
32
+
33
+ [project.urls]
34
+ Documentation = "https://github.com/vypivshiy/selector_schema_codegen#readme"
35
+ Issues = "https://github.com/vypivshiy/selector_schema_codegen/issues"
36
+ Source = "https://github.com/vypivshiy/selector_schema_codegen"
37
+ Examples = "https://github.com/vypivshiy/selector_schema_codegen/examples"
38
+
39
+
40
+ [project.scripts]
41
+ ssc-gen = 'ssc_codegen.main:main'
42
+
43
+ [build-system]
44
+ requires = ["hatchling"]
45
+ build-backend = "hatchling.build"
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = [
49
+ "ssc_codegen/**",
50
+ ]
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["ssc_codegen"]
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "aiohttp>=3.9,<3.12", # aioresponses 0.7.x breaks on 3.12+ (stream_writer)
58
+ "aioresponses>=0.7",
59
+ "bs4>=0.0.2",
60
+ "coverage>=7.6.12",
61
+ "httpx>=0.28.1",
62
+ "hypothesis>=6.151.9",
63
+ "lxml>=5.3.0",
64
+ "mypy>=1.14.1",
65
+ "parsel>=1.9.1",
66
+ "pytest>=8.3.4",
67
+ "pytest-xdist>=3.6.0",
68
+ "respx>=0.23.1",
69
+ "responses>=0.25",
70
+ "ruff>=0.9.3",
71
+ "selectolax>=0.3.27",
72
+ ]
73
+ docs = [
74
+ "mkdocs>=1.6.0",
75
+ "mkdocs-material>=9.5.0",
76
+ "mkdocstrings[python]>=0.27.0",
77
+ "mkdocs-click>=0.8.1",
78
+ ]
79
+
80
+ [tool.ruff]
81
+ target-version = "py310"
82
+ line-length = 80
83
+ exclude = ["__init__.py"]
84
+
85
+ [tool.mypy]
86
+ python_version = "3.10"
87
+ show_error_codes = true
88
+ [[tool.mypy.overrides]]
89
+ module="tests.*"
90
+ disallow_untyped_defs = false
91
+
92
+
93
+ [[tool.mypy.overrides]]
94
+ module = ["colorama", "lxml", "lxml.*"]
95
+ ignore_missing_imports = true
96
+
97
+ [tool.coverage.report]
98
+ exclude_also = [
99
+ 'def __repr__',
100
+ 'if self.debug:',
101
+ 'if settings.DEBUG',
102
+ 'raise AssertionError',
103
+ 'raise NotImplementedError',
104
+ 'if 0:',
105
+ 'if __name__ == .__main__.:',
106
+ 'if TYPE_CHECKING:',
107
+ 'class .*\bProtocol\):',
108
+ '@(abc\.)?abstractmethod',
109
+ ]
110
+
111
+ [tool.coverage.run]
112
+ source = ["ssc_codegen"]
113
+ omit = [
114
+ "*/.venv/*",
115
+ "*/scripts/*",
116
+ "*/tests/*"
117
+ ]
118
+
119
+ [tool.pytest.ini_options]
120
+ testpaths = ["tests"]
121
+ markers = [
122
+ "toolchain: Tests requiring external toolchains (Rust cargo, Go compiler, Node.js)",
123
+ ]
124
+
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import inspect
6
+
5
7
 
6
8
  class ModuleBuilder:
7
9
  """Language-agnostic accumulator for imports and helper definitions.
@@ -50,12 +52,17 @@ class ModuleBuilder:
50
52
  def require_import(self, line: str) -> None:
51
53
  """Register a top-level import line.
52
54
 
53
- Deduplicated and order-preserving. The line string is stored as-is.
55
+ Deduplicated and order-preserving. Multi-line blocks are normalized
56
+ with `inspect.cleandoc` to remove uniform indentation.
54
57
 
55
58
  Args:
56
59
  line: The verbatim import statement line for the target language.
57
60
  """
58
- self._imports.setdefault(line, None)
61
+ if "\n" in line:
62
+ cleaned = inspect.cleandoc(line)
63
+ else:
64
+ cleaned = line
65
+ self._imports.setdefault(cleaned, None)
59
66
 
60
67
  def require_std(
61
68
  self,
@@ -6,7 +6,6 @@ from collections.abc import Callable
6
6
  from typing import Any
7
7
 
8
8
  import ssc_codegen.ast as a
9
-
10
9
  from ssc_codegen.targets.python.http_libs.base import HttpLibStrategy
11
10
  from ssc_codegen.targets.python.http_libs.httpx import HttpxStrategy
12
11
  from ssc_codegen.traversal.utils import module_has_rest
@@ -439,7 +438,7 @@ def runtime_module_content(
439
438
  lines.extend(_BASE_UTILITY_LINES)
440
439
  lines.append("")
441
440
  if has_rest:
442
- lines.extend(strategy.rest_runtime_lines())
441
+ lines.extend(strategy.rest_runtime_lines(http_io="both"))
443
442
  if extension_defs:
444
443
  for _imports, code in extension_defs.values():
445
444
  lines.append("")
@@ -485,10 +484,17 @@ def register_runtime_file(
485
484
 
486
485
  @converter.file(f"{runtime_name}.py")
487
486
  def _runtime_provider(module_ast: a.Module, meta):
487
+ strat = http_strategy
488
+ if (
489
+ meta
490
+ and meta.get("http_client")
491
+ and hasattr(converter, "http_strategy_for")
492
+ ):
493
+ strat = converter.http_strategy_for(meta.get("http_client"))
488
494
  return _apply_fallback(
489
495
  runtime_module_content(
490
496
  module_ast,
491
- http_strategy=http_strategy,
497
+ http_strategy=strat,
492
498
  extension_defs=_extension_runtime_defs([module_ast]),
493
499
  )
494
500
  )
@@ -152,6 +152,8 @@ def _run_generate(
152
152
  skip_lint: bool = False,
153
153
  verbose: bool = False,
154
154
  fmt: FmtType = FmtType.TEXT,
155
+ bs4_parser: Optional[str] = None,
156
+ http_io: Optional[str] = None,
155
157
  ) -> None:
156
158
  """Execute the shared compilation pipeline for all code generator subcommands.
157
159
 
@@ -216,6 +218,10 @@ def _run_generate(
216
218
  meta: dict = {"package": package or default_package}
217
219
  if http_client:
218
220
  meta["http_client"] = http_client
221
+ if bs4_parser:
222
+ meta["bs4_parser"] = bs4_parser
223
+ if http_io:
224
+ meta["http_io"] = http_io
219
225
 
220
226
  if separate_runtime:
221
227
  from ssc_codegen.generation.runtime import register_runtime_file
@@ -385,6 +391,16 @@ def generate_python(
385
391
  help="HTML parsing library. Default: bs4.",
386
392
  ),
387
393
  ] = None,
394
+ bs4_parser: Annotated[
395
+ Optional[str],
396
+ typer.Option(
397
+ "--bs4-parser",
398
+ help=(
399
+ "Underlying HTML parser engine for bs4: "
400
+ "lxml (default) | html.parser | html5lib."
401
+ ),
402
+ ),
403
+ ] = None,
388
404
  verbose: Annotated[
389
405
  bool,
390
406
  typer.Option(
@@ -413,10 +429,17 @@ def generate_python(
413
429
  "--http-client",
414
430
  help=(
415
431
  "HTTP client for @request codegen: "
416
- "httpx (default) | aiohttp | requests."
432
+ "httpx (default) | httpx2 | aiohttp | requests."
417
433
  ),
418
434
  ),
419
435
  ] = None,
436
+ http_io: Annotated[
437
+ Optional[str],
438
+ typer.Option(
439
+ "--http-io",
440
+ help="HTTP I/O mode for generated methods: both (default) | sync | async.",
441
+ ),
442
+ ] = None,
420
443
  separate_runtime: Annotated[
421
444
  bool,
422
445
  typer.Option(
@@ -452,6 +475,8 @@ def generate_python(
452
475
  lib=lib.value if lib else None,
453
476
  http_client=http_client,
454
477
  separate_runtime=separate_runtime,
478
+ bs4_parser=bs4_parser,
479
+ http_io=http_io,
455
480
  )
456
481
  )
457
482
  except ResolutionError as exc:
@@ -474,6 +499,8 @@ def generate_python(
474
499
  skip_lint=skip_lint,
475
500
  verbose=verbose,
476
501
  fmt=fmt,
502
+ bs4_parser=bs4_parser,
503
+ http_io=http_io,
477
504
  )
478
505
 
479
506
 
@@ -886,6 +913,16 @@ def run(
886
913
  help="HTML parsing library. Default: bs4.",
887
914
  ),
888
915
  ] = None,
916
+ bs4_parser: Annotated[
917
+ Optional[str],
918
+ typer.Option(
919
+ "--bs4-parser",
920
+ help=(
921
+ "Underlying HTML parser engine for bs4: "
922
+ "lxml (default) | html.parser | html5lib."
923
+ ),
924
+ ),
925
+ ] = None,
889
926
  input_file: Annotated[
890
927
  Path | None,
891
928
  typer.Option(
@@ -984,6 +1021,7 @@ def run(
984
1021
  TargetSpec(
985
1022
  lang="python",
986
1023
  lib=lib.value if lib else None,
1024
+ bs4_parser=bs4_parser,
987
1025
  )
988
1026
  )
989
1027
  except ResolutionError as exc:
@@ -22,6 +22,23 @@ def to_snake_case(s: str) -> str:
22
22
  return s.lower()
23
23
 
24
24
 
25
+ def json_descriptor_var_name(name: str) -> str:
26
+ """Format canonical module-level JSON descriptor constant identifier.
27
+
28
+ Args:
29
+ name: Struct, type, or response schema name to format.
30
+
31
+ Returns:
32
+ Canonical uppercase identifier prefixed with ``JSON_DESCRIPTOR_``.
33
+
34
+ Examples:
35
+ 'Episode' -> 'JSON_DESCRIPTOR_EPISODE'
36
+ 'CatalogReleasesResponse' -> 'JSON_DESCRIPTOR_CATALOG_RELEASES_RESPONSE'
37
+ """
38
+ clean_name = to_snake_case(name).lstrip("_").upper()
39
+ return f"JSON_DESCRIPTOR_{clean_name}"
40
+
41
+
25
42
  def to_pascal_case(s: str) -> str:
26
43
  """
27
44
  'my_field_name' -> 'MyFieldName'
@@ -8,14 +8,12 @@ Called by JsVisitor via thin delegate methods. The HTTP library strategy
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
- from typing import Mapping
12
11
 
13
12
  from ssc_codegen.ast import (
14
13
  JsonDef,
15
14
  MatcherListDef,
16
15
  MethodFetch,
17
16
  MethodRest,
18
- Module,
19
17
  PlaceholderSpec,
20
18
  PlaceholderTemplate,
21
19
  ResultAliasDef,
@@ -23,18 +21,21 @@ from ssc_codegen.ast import (
23
21
  StructBase,
24
22
  )
25
23
  from ssc_codegen.ast.struct import RequestHttp
26
- from ssc_codegen.naming import to_camel_case, to_pascal_case, to_snake_case
24
+ from ssc_codegen.naming import (
25
+ json_descriptor_var_name,
26
+ to_camel_case,
27
+ to_pascal_case,
28
+ to_snake_case,
29
+ )
27
30
  from ssc_codegen.request_spec import parse_json_template
28
31
  from ssc_codegen.targets.javascript.http_libs.base import JsHttpLibStrategy
29
32
  from ssc_codegen.traversal.context import WalkContext
30
33
  from ssc_codegen.traversal.utils import (
31
34
  dict_needs_builder,
32
35
  err_subclass_name,
33
- json_def_descriptors,
34
- json_def_mapping,
36
+ find_enclosing_module,
35
37
  )
36
38
 
37
-
38
39
  # ===========================================================================
39
40
  # REST shared runtime source (Ok/Err/sscDispatchErr/etc.)
40
41
  # ===========================================================================
@@ -287,8 +288,7 @@ def js_name(name: str) -> str:
287
288
  def ok_payload_type(node: MethodRest) -> str:
288
289
  if not node.response_schema:
289
290
  return "null"
290
- struct = node.parent
291
- module = struct.parent if struct is not None else None
291
+ module = find_enclosing_module(node)
292
292
  schema_type = f"{to_pascal_case(node.response_schema)}Json"
293
293
  if module is not None:
294
294
  for n in module.body:
@@ -376,21 +376,28 @@ def emit_method_rest(
376
376
  )
377
377
 
378
378
  fn_name = http.fn_name
379
- response_desc = _response_descriptors(node)
379
+ module = find_enclosing_module(node)
380
+ has_response_schema = False
381
+ if node.response_schema and module is not None:
382
+ has_response_schema = any(
383
+ isinstance(n, JsonDef) and n.name == node.response_schema
384
+ for n in module.body
385
+ )
386
+
380
387
  if node.response_path:
381
388
  accessor = "".join(
382
389
  f"[{json.dumps(p)}]" for p in node.response_path.split(".")
383
390
  )
384
- if response_desc is None:
385
- value_fn = f"(_b) => _b{accessor}"
391
+ if has_response_schema:
392
+ desc_var = json_descriptor_var_name(node.response_schema)
393
+ value_fn = f"(_b) => sscJsonProject(_b{accessor}, {desc_var})"
386
394
  else:
387
- value_fn = f"(_b) => sscJsonProject(_b{accessor}, {_render_descriptors(response_desc)})"
395
+ value_fn = f"(_b) => _b{accessor}"
388
396
  elif not node.response_schema:
389
397
  value_fn = "(_b) => null"
390
- elif response_desc is not None:
391
- value_fn = (
392
- f"(_b) => sscJsonProject(_b, {_render_descriptors(response_desc)})"
393
- )
398
+ elif has_response_schema:
399
+ desc_var = json_descriptor_var_name(node.response_schema)
400
+ value_fn = f"(_b) => sscJsonProject(_b, {desc_var})"
394
401
  else:
395
402
  value_fn = "null"
396
403
 
@@ -466,82 +473,6 @@ def emit_method_rest(
466
473
  return lines
467
474
 
468
475
 
469
- def _response_descriptors(
470
- node: MethodRest,
471
- ) -> dict[str, tuple[str, bool, bool, object]] | None:
472
- if not node.response_schema:
473
- return None
474
- module = node.parent.parent if node.parent is not None else None
475
- if not isinstance(module, Module):
476
- return None
477
- definitions = {n.name: n for n in module.body if isinstance(n, JsonDef)}
478
- definition = definitions.get(node.response_schema)
479
- if definition is None:
480
- return None
481
- return json_def_descriptors(definition, definitions)
482
-
483
-
484
- def _render_descriptors(descriptors: Mapping[str, object]) -> str:
485
- def render(value: object) -> str:
486
- if value is None:
487
- return "null"
488
- if isinstance(value, bool):
489
- return "true" if value else "false"
490
- if isinstance(value, str):
491
- return json.dumps(value)
492
- if isinstance(value, tuple):
493
- return "[" + ", ".join(render(x) for x in value) + "]"
494
- if isinstance(value, list):
495
- return "[" + ", ".join(render(item) for item in value) + "]"
496
- if isinstance(value, dict):
497
- return (
498
- "{"
499
- + ", ".join(
500
- f"{json.dumps(key)}: {render(item)}"
501
- for key, item in value.items()
502
- )
503
- + "}"
504
- )
505
- raise TypeError(f"unsupported JSON descriptor value: {value!r}")
506
-
507
- return render(descriptors)
508
-
509
-
510
- def _response_mapping(node: MethodRest) -> dict[str, object] | None:
511
- if not node.response_schema:
512
- return None
513
- module = node.parent.parent if node.parent is not None else None
514
- if not isinstance(module, Module):
515
- return None
516
- definitions = {n.name: n for n in module.body if isinstance(n, JsonDef)}
517
- definition = definitions.get(node.response_schema)
518
- if definition is None or not definition.has_alias_key:
519
- return None
520
- return json_def_mapping(definition, definitions)
521
-
522
-
523
- def _render_mapping(mapping: dict[str, object]) -> str:
524
- def render(value: object) -> str:
525
- if isinstance(value, str):
526
- return json.dumps(value)
527
- if isinstance(value, tuple):
528
- return "[" + json.dumps(value[0]) + ", " + render(value[1]) + "]"
529
- if isinstance(value, list):
530
- return "[" + ", ".join(render(item) for item in value) + "]"
531
- if isinstance(value, dict):
532
- return (
533
- "{"
534
- + ", ".join(
535
- f"{json.dumps(key)}: {render(item)}"
536
- for key, item in value.items()
537
- )
538
- + "}"
539
- )
540
- raise TypeError(f"unsupported JSON mapping value: {value!r}")
541
-
542
- return render(mapping)
543
-
544
-
545
476
  def emit_method_fetch(
546
477
  node: MethodFetch, ctx: WalkContext, http: JsHttpLibStrategy
547
478
  ) -> list[str]:
@@ -679,26 +610,21 @@ def emit_result_alias_def(node: ResultAliasDef) -> list[str]:
679
610
  def emit_matcher_list_def(node: MatcherListDef) -> list[str]:
680
611
  var = f"_{to_snake_case(node.struct_name)}Matchers"
681
612
  lines = [f"const {var} = ["]
682
- module = (
683
- node.parent
684
- if isinstance(node.parent, Module)
685
- else (node.parent.parent if node.parent is not None else None)
686
- )
613
+ module = find_enclosing_module(node)
687
614
  definitions = (
688
615
  {n.name: n for n in module.body if isinstance(n, JsonDef)}
689
- if isinstance(module, Module)
616
+ if module is not None
690
617
  else {}
691
618
  )
692
619
  for e in node.entries:
693
620
  check = render_js_condition_check(e.required_keys, e.conditions)
694
621
  check_arg = check if check else "null"
695
- desc = None
696
- definition = definitions.get(e.error_schema)
697
- if definition is not None:
698
- desc = _render_descriptors(
699
- json_def_descriptors(definition, definitions)
700
- )
701
- value_expr = f"sscJsonProject(_b, {desc})" if desc is not None else "_b"
622
+ desc_var = None
623
+ if e.error_schema and e.error_schema in definitions:
624
+ desc_var = json_descriptor_var_name(e.error_schema)
625
+ value_expr = (
626
+ f"sscJsonProject(_b, {desc_var})" if desc_var is not None else "_b"
627
+ )
702
628
  lines.append(
703
629
  f" {{ status: {e.status}, check: {check_arg}, "
704
630
  f"factory: (_s, _h, _b) => ({{ isOk: false, status: _s, "