ssc_codegen 0.37.1__tar.gz → 0.38.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.37.1 → ssc_codegen-0.38.0}/PKG-INFO +10 -4
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/README.md +12 -6
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/pyproject.toml +1 -11
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/__init__.py +18 -2
- ssc_codegen-0.38.0/ssc_codegen/ast/extension.py +80 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/module.py +5 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/struct.py +21 -15
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/typedef.py +2 -1
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/contexts.py +35 -23
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/expressions.py +84 -1
- ssc_codegen-0.38.0/ssc_codegen/core/extensions.py +144 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/format.py +29 -5
- ssc_codegen-0.38.0/ssc_codegen/core/imports.py +544 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/linter.py +804 -24
- ssc_codegen-0.38.0/ssc_codegen/core/module_handler.py +147 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/reader.py +54 -17
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/struct_parser.py +20 -1
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/type_checking.py +17 -1
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/generation/builder.py +37 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/generation/runtime.py +44 -1
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/main.py +135 -12
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/parsers/curl.py +20 -9
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/parsers/http.py +17 -6
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/request_spec.py +84 -3
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/rest.py +106 -54
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/runtime.py +11 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/visitor.py +109 -9
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/rest.py +48 -14
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/visitor.py +52 -7
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/rest.py +14 -49
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/visitor.py +65 -21
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/traversal/utils.py +11 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/traversal/walker.py +2 -0
- ssc_codegen-0.37.1/ssc_codegen/core/module_handler.py +0 -240
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/.gitignore +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/LICENSE +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/_logging.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/array.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/base.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/cast.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/control.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/extract.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/function.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/helpers.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/jsondef.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/predicate_containers.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/predicate_ops.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/regex.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/rest.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/selectors.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/string.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/ast/types.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/predicates.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/core/rest_artifacts.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/exceptions.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/explore.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/generation/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/health.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/naming.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/parsers/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/regex_utils.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/http_libs/base.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/http_libs/nethttp.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/literals.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/regex.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/http_libs/axios.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/http_libs/base.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/http_libs/fetch.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/profile.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/base.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/bs4.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/lxml.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/parsel.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/slax.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/aiohttp.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/base.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/httpx.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/requests.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/resolver.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/targets/spec.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/traversal/__init__.py +0 -0
- {ssc_codegen-0.37.1 → ssc_codegen-0.38.0}/ssc_codegen/traversal/context.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ssc_codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.38.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
|
|
@@ -42,7 +42,8 @@ Code generator for web scraping parsers. Describe HTML extraction rules in a dec
|
|
|
42
42
|
|
|
43
43
|
- Declarative DSL based on KDL 2.0 syntax
|
|
44
44
|
- Static type checking and linting before code generation
|
|
45
|
-
- Multiple output targets: Python (bs4, lxml, parsel, selectolax), JavaScript (DOM API)
|
|
45
|
+
- Multiple output targets: Python (bs4, lxml, parsel, selectolax), JavaScript (DOM API), Go (goquery)
|
|
46
|
+
- Typed user extensions with lazy target imports and runtime helpers
|
|
46
47
|
- Struct types: `item`, `list`, `dict`, `table`, `flat`
|
|
47
48
|
- LLM-friendly: system prompt + linter loop for AI-assisted schema generation
|
|
48
49
|
|
|
@@ -86,6 +87,9 @@ ssc-gen generate js examples/ -o ./output
|
|
|
86
87
|
# Go (goquery + net/http)
|
|
87
88
|
ssc-gen generate go schema.kdl -o ./output
|
|
88
89
|
|
|
90
|
+
# Go package defaults to main; override explicitly when generating a library
|
|
91
|
+
ssc-gen generate go schema.kdl -o ./output --package scraper
|
|
92
|
+
|
|
89
93
|
# with custom package name
|
|
90
94
|
ssc-gen generate python schema.kdl -L bs4 -o ./parsers --package scraper
|
|
91
95
|
|
|
@@ -116,12 +120,14 @@ ssc-gen check examples/
|
|
|
116
120
|
|
|
117
121
|
```bash
|
|
118
122
|
# from file
|
|
119
|
-
ssc-gen run examples/booksToScrape.kdl:MainCatalogue -
|
|
123
|
+
ssc-gen run examples/booksToScrape.kdl:MainCatalogue -L bs4 -i page.html
|
|
120
124
|
|
|
121
125
|
# from stdin
|
|
122
|
-
curl https://books.toscrape.com/ | ssc-gen run examples/booksToScrape.kdl:MainCatalogue -
|
|
126
|
+
curl https://books.toscrape.com/ | ssc-gen run examples/booksToScrape.kdl:MainCatalogue -L bs4
|
|
123
127
|
```
|
|
124
128
|
|
|
129
|
+
`ssc-gen run` executes generated Python in-process. Run only trusted schema files.
|
|
130
|
+
|
|
125
131
|
### Health check (verify selectors match elements)
|
|
126
132
|
|
|
127
133
|
```bash
|
|
@@ -10,7 +10,8 @@ Code generator for web scraping parsers. Describe HTML extraction rules in a dec
|
|
|
10
10
|
|
|
11
11
|
- Declarative DSL based on KDL 2.0 syntax
|
|
12
12
|
- Static type checking and linting before code generation
|
|
13
|
-
- Multiple output targets: Python (bs4, lxml, parsel, selectolax), JavaScript (DOM API)
|
|
13
|
+
- Multiple output targets: Python (bs4, lxml, parsel, selectolax), JavaScript (DOM API), Go (goquery)
|
|
14
|
+
- Typed user extensions with lazy target imports and runtime helpers
|
|
14
15
|
- Struct types: `item`, `list`, `dict`, `table`, `flat`
|
|
15
16
|
- LLM-friendly: system prompt + linter loop for AI-assisted schema generation
|
|
16
17
|
|
|
@@ -51,8 +52,11 @@ ssc-gen generate python schema.kdl -L bs4 -o ./output
|
|
|
51
52
|
# all .kdl files in a directory (JavaScript)
|
|
52
53
|
ssc-gen generate js examples/ -o ./output
|
|
53
54
|
|
|
54
|
-
# Go (goquery + net/http)
|
|
55
|
-
ssc-gen generate go schema.kdl -o ./output
|
|
55
|
+
# Go (goquery + net/http)
|
|
56
|
+
ssc-gen generate go schema.kdl -o ./output
|
|
57
|
+
|
|
58
|
+
# Go package defaults to main; override explicitly when generating a library
|
|
59
|
+
ssc-gen generate go schema.kdl -o ./output --package scraper
|
|
56
60
|
|
|
57
61
|
# with custom package name
|
|
58
62
|
ssc-gen generate python schema.kdl -L bs4 -o ./parsers --package scraper
|
|
@@ -84,11 +88,13 @@ ssc-gen check examples/
|
|
|
84
88
|
|
|
85
89
|
```bash
|
|
86
90
|
# from file
|
|
87
|
-
ssc-gen run examples/booksToScrape.kdl:MainCatalogue -
|
|
91
|
+
ssc-gen run examples/booksToScrape.kdl:MainCatalogue -L bs4 -i page.html
|
|
88
92
|
|
|
89
93
|
# from stdin
|
|
90
|
-
curl https://books.toscrape.com/ | ssc-gen run examples/booksToScrape.kdl:MainCatalogue -
|
|
91
|
-
```
|
|
94
|
+
curl https://books.toscrape.com/ | ssc-gen run examples/booksToScrape.kdl:MainCatalogue -L bs4
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`ssc-gen run` executes generated Python in-process. Run only trusted schema files.
|
|
92
98
|
|
|
93
99
|
### Health check (verify selectors match elements)
|
|
94
100
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ssc_codegen"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.38.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"
|
|
@@ -78,10 +78,6 @@ exclude = ["__init__.py"]
|
|
|
78
78
|
[tool.mypy]
|
|
79
79
|
python_version = "3.10"
|
|
80
80
|
show_error_codes = true
|
|
81
|
-
exclude = [
|
|
82
|
-
"ssc_codegen/ast" # ignore built-in ast and project ast
|
|
83
|
-
|
|
84
|
-
]
|
|
85
81
|
[[tool.mypy.overrides]]
|
|
86
82
|
module="tests.*"
|
|
87
83
|
disallow_untyped_defs = false
|
|
@@ -91,12 +87,6 @@ disallow_untyped_defs = false
|
|
|
91
87
|
module = ["colorama", "lxml", "lxml.*"]
|
|
92
88
|
ignore_missing_imports = true
|
|
93
89
|
|
|
94
|
-
[[tool.mypy.overrides]]
|
|
95
|
-
# Large visitor classes with match statements may need relaxed return checks
|
|
96
|
-
module = ["ssc_codegen.targets.python.visitor", "ssc_codegen.targets.javascript.visitor"]
|
|
97
|
-
disable_error_code = ["return", "return-value"]
|
|
98
|
-
|
|
99
|
-
|
|
100
90
|
[tool.coverage.report]
|
|
101
91
|
exclude_also = [
|
|
102
92
|
'def __repr__',
|
|
@@ -20,7 +20,16 @@ from .module import (
|
|
|
20
20
|
Utilities,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
-
from .function import FunctionDef
|
|
23
|
+
from .function import FunctionDef
|
|
24
|
+
|
|
25
|
+
from .extension import (
|
|
26
|
+
ExtensionCall,
|
|
27
|
+
ExtensionDef,
|
|
28
|
+
ExtensionHelper,
|
|
29
|
+
ExtensionImport,
|
|
30
|
+
ExtensionTarget,
|
|
31
|
+
ExtensionType,
|
|
32
|
+
)
|
|
24
33
|
|
|
25
34
|
from .typedef import TypeDef, TypeDefField
|
|
26
35
|
|
|
@@ -150,7 +159,14 @@ __all__ = [
|
|
|
150
159
|
"Docstring",
|
|
151
160
|
"Utilities",
|
|
152
161
|
# function
|
|
153
|
-
"FunctionDef",
|
|
162
|
+
"FunctionDef",
|
|
163
|
+
# extensions
|
|
164
|
+
"ExtensionCall",
|
|
165
|
+
"ExtensionDef",
|
|
166
|
+
"ExtensionHelper",
|
|
167
|
+
"ExtensionImport",
|
|
168
|
+
"ExtensionTarget",
|
|
169
|
+
"ExtensionType",
|
|
154
170
|
# typedef
|
|
155
171
|
"TypeDef",
|
|
156
172
|
"TypeDefField",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .base import Node
|
|
6
|
+
from .types import TypeInfo, VariableType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ExtensionType:
|
|
11
|
+
"""Type pattern used by an extension operation signature."""
|
|
12
|
+
|
|
13
|
+
base: VariableType | None = None
|
|
14
|
+
generic: str | None = None
|
|
15
|
+
is_array: bool = False
|
|
16
|
+
is_optional: bool = False
|
|
17
|
+
|
|
18
|
+
def resolve(self, binding: TypeInfo | None = None) -> TypeInfo:
|
|
19
|
+
if self.generic:
|
|
20
|
+
if binding is None:
|
|
21
|
+
return TypeInfo(base=VariableType.AUTO)
|
|
22
|
+
return binding
|
|
23
|
+
return TypeInfo(
|
|
24
|
+
base=self.base or VariableType.AUTO,
|
|
25
|
+
is_array=self.is_array,
|
|
26
|
+
is_optional=self.is_optional,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def accepts(self, value: TypeInfo) -> bool:
|
|
30
|
+
if self.generic:
|
|
31
|
+
return True
|
|
32
|
+
return (
|
|
33
|
+
value.base in (self.base, VariableType.AUTO)
|
|
34
|
+
and value.is_array == self.is_array
|
|
35
|
+
and value.is_optional == self.is_optional
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ExtensionImport:
|
|
41
|
+
value: str
|
|
42
|
+
alias: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ExtensionHelper:
|
|
47
|
+
name: str
|
|
48
|
+
source: str
|
|
49
|
+
imports: tuple[ExtensionImport, ...] = ()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class ExtensionTarget:
|
|
54
|
+
language: str
|
|
55
|
+
emit: str
|
|
56
|
+
imports: tuple[ExtensionImport, ...] = ()
|
|
57
|
+
helpers: tuple[ExtensionHelper, ...] = ()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ExtensionDef:
|
|
62
|
+
namespace: str
|
|
63
|
+
name: str
|
|
64
|
+
accept: ExtensionType
|
|
65
|
+
ret: ExtensionType
|
|
66
|
+
targets: dict[str, ExtensionTarget] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def qualified_name(self) -> str:
|
|
70
|
+
return f"{self.namespace}.{self.name}"
|
|
71
|
+
|
|
72
|
+
def resolve_return(self, input_type: TypeInfo) -> TypeInfo:
|
|
73
|
+
binding = input_type if self.accept.generic else None
|
|
74
|
+
return self.ret.resolve(binding)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ExtensionCall(Node):
|
|
79
|
+
qualified_name: str = ""
|
|
80
|
+
definition: ExtensionDef | None = field(default=None, repr=False)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import warnings
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import field
|
|
4
5
|
|
|
5
6
|
from .base import Node
|
|
6
7
|
|
|
@@ -28,6 +29,7 @@ class Module(Node):
|
|
|
28
29
|
|
|
29
30
|
doc: str = ""
|
|
30
31
|
source_file: str = ""
|
|
32
|
+
extensions: dict[str, "ExtensionDef"] = field(default_factory=dict)
|
|
31
33
|
|
|
32
34
|
def __post_init__(self):
|
|
33
35
|
self.body.extend(
|
|
@@ -112,3 +114,6 @@ class Utilities(Node):
|
|
|
112
114
|
"""
|
|
113
115
|
|
|
114
116
|
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
from .extension import ExtensionDef # noqa: E402
|
|
@@ -136,10 +136,16 @@ class PlaceholderSpec:
|
|
|
136
136
|
def parse_placeholder(match: "_re.Match[str]") -> PlaceholderSpec:
|
|
137
137
|
return PlaceholderSpec(
|
|
138
138
|
name=match.group(1),
|
|
139
|
-
type_name=
|
|
139
|
+
type_name=cast(
|
|
140
|
+
Literal["str", "int", "float", "bool"],
|
|
141
|
+
match.group(2) or "str",
|
|
142
|
+
),
|
|
140
143
|
is_array=bool(match.group(3)),
|
|
141
144
|
is_optional=bool(match.group(4)),
|
|
142
|
-
style=
|
|
145
|
+
style=cast(
|
|
146
|
+
Literal["repeat", "csv", "bracket", "pipe", "space"] | None,
|
|
147
|
+
match.group(5) or None,
|
|
148
|
+
),
|
|
143
149
|
)
|
|
144
150
|
|
|
145
151
|
|
|
@@ -260,7 +266,7 @@ class RequestHttp(Node):
|
|
|
260
266
|
|
|
261
267
|
Created once at parse time from ``parse_to_http(raw_payload)``;
|
|
262
268
|
converters read fields directly instead of re-parsing raw_payload.
|
|
263
|
-
String fields (url, headers, cookies, params,
|
|
269
|
+
String fields (url, headers, cookies, params, payload) are ``Template``
|
|
264
270
|
instances: tokenized literal/placeholder parts.
|
|
265
271
|
"""
|
|
266
272
|
|
|
@@ -270,7 +276,7 @@ class RequestHttp(Node):
|
|
|
270
276
|
cookies: dict[str, PlaceholderTemplate] = field(default_factory=dict)
|
|
271
277
|
params: dict[str, PlaceholderTemplate] = field(default_factory=dict)
|
|
272
278
|
body_kind: str = "empty" # "empty" | "json" | "form" | "raw"
|
|
273
|
-
|
|
279
|
+
payload: PlaceholderTemplate | dict[str, PlaceholderTemplate] | None = None
|
|
274
280
|
|
|
275
281
|
@property
|
|
276
282
|
def placeholders(self) -> list[PlaceholderSpec]:
|
|
@@ -289,10 +295,10 @@ class RequestHttp(Node):
|
|
|
289
295
|
yield self.url
|
|
290
296
|
for d in (self.headers, self.cookies, self.params):
|
|
291
297
|
yield from d.values()
|
|
292
|
-
if isinstance(self.
|
|
293
|
-
yield self.
|
|
294
|
-
elif isinstance(self.
|
|
295
|
-
yield from self.
|
|
298
|
+
if isinstance(self.payload, PlaceholderTemplate):
|
|
299
|
+
yield self.payload
|
|
300
|
+
elif isinstance(self.payload, dict):
|
|
301
|
+
yield from self.payload.values()
|
|
296
302
|
|
|
297
303
|
def with_renamed_placeholders(
|
|
298
304
|
self, transform: Callable[[str], str]
|
|
@@ -312,13 +318,13 @@ class RequestHttp(Node):
|
|
|
312
318
|
) -> dict[str, PlaceholderTemplate]:
|
|
313
319
|
return {k: v.renamed(mapping) for k, v in d.items()}
|
|
314
320
|
|
|
315
|
-
|
|
321
|
+
new_payload: (
|
|
316
322
|
PlaceholderTemplate | dict[str, PlaceholderTemplate] | None
|
|
317
|
-
) = self.
|
|
318
|
-
if isinstance(self.
|
|
319
|
-
|
|
320
|
-
elif isinstance(self.
|
|
321
|
-
|
|
323
|
+
) = self.payload
|
|
324
|
+
if isinstance(self.payload, PlaceholderTemplate):
|
|
325
|
+
new_payload = self.payload.renamed(mapping)
|
|
326
|
+
elif isinstance(self.payload, dict):
|
|
327
|
+
new_payload = _rename_dict(self.payload)
|
|
322
328
|
|
|
323
329
|
return RequestHttp(
|
|
324
330
|
method=self.method,
|
|
@@ -327,7 +333,7 @@ class RequestHttp(Node):
|
|
|
327
333
|
cookies=_rename_dict(self.cookies),
|
|
328
334
|
params=_rename_dict(self.params),
|
|
329
335
|
body_kind=self.body_kind,
|
|
330
|
-
|
|
336
|
+
payload=new_payload,
|
|
331
337
|
)
|
|
332
338
|
|
|
333
339
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
+
from typing import cast
|
|
3
4
|
|
|
4
5
|
from .base import Node
|
|
5
6
|
from .types import StructType
|
|
@@ -29,4 +30,4 @@ class TypeDef(Node):
|
|
|
29
30
|
|
|
30
31
|
@property
|
|
31
32
|
def fields(self) -> list[TypeDefField]:
|
|
32
|
-
return self.body
|
|
33
|
+
return cast(list[TypeDefField], self.body)
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from enum import Enum, auto
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from ssc_codegen.ast import StructBase, VariableType
|
|
9
|
+
from ssc_codegen.ast import ExtensionDef, StructBase, VariableType
|
|
10
10
|
from kdlquery import KdlNode, ReadDiagnostic, Severity
|
|
11
11
|
|
|
12
12
|
|
|
@@ -49,7 +49,9 @@ class ParseContext:
|
|
|
49
49
|
children_defines: dict[str, list[KdlNode]] = field(default_factory=dict)
|
|
50
50
|
structs: dict[str, StructBase] = field(default_factory=dict)
|
|
51
51
|
json_defs: dict[str, JsonDef] = field(default_factory=dict)
|
|
52
|
+
extensions: dict[str, ExtensionDef] = field(default_factory=dict)
|
|
52
53
|
source_path: Path | None = None
|
|
54
|
+
node_source_paths: dict[int, Path] = field(default_factory=dict)
|
|
53
55
|
|
|
54
56
|
def all_names(self) -> set[str]:
|
|
55
57
|
return (
|
|
@@ -57,6 +59,7 @@ class ParseContext:
|
|
|
57
59
|
| set(self.children_defines)
|
|
58
60
|
| set(self.structs)
|
|
59
61
|
| set(self.json_defs)
|
|
62
|
+
| set(self.extensions)
|
|
60
63
|
)
|
|
61
64
|
|
|
62
65
|
|
|
@@ -77,6 +80,7 @@ class LintContext:
|
|
|
77
80
|
)
|
|
78
81
|
_predicate_depth: int = field(default=0)
|
|
79
82
|
_predicate_context: str = ""
|
|
83
|
+
node_source_paths: dict[int, Path] = field(default_factory=dict)
|
|
80
84
|
|
|
81
85
|
def error(
|
|
82
86
|
self,
|
|
@@ -88,18 +92,22 @@ class LintContext:
|
|
|
88
92
|
label: str | None = None,
|
|
89
93
|
notes: list[str] | None = None,
|
|
90
94
|
) -> None:
|
|
91
|
-
self.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
scope = self.path
|
|
96
|
+
source_path = self.node_source_paths.get(id(node))
|
|
97
|
+
diagnostic_notes = tuple(notes) if notes else ()
|
|
98
|
+
if source_path is not None and scope:
|
|
99
|
+
diagnostic_notes = (*diagnostic_notes, f"scope: {scope}")
|
|
100
|
+
diagnostic = ReadDiagnostic(
|
|
101
|
+
message=message,
|
|
102
|
+
severity=Severity.ERROR,
|
|
103
|
+
span=node.span,
|
|
104
|
+
path=str(source_path) if source_path is not None else scope,
|
|
105
|
+
hint=hint,
|
|
106
|
+
code=code,
|
|
107
|
+
label=label,
|
|
108
|
+
notes=diagnostic_notes,
|
|
102
109
|
)
|
|
110
|
+
self.diagnostics.append(diagnostic)
|
|
103
111
|
|
|
104
112
|
def warning(
|
|
105
113
|
self,
|
|
@@ -111,18 +119,22 @@ class LintContext:
|
|
|
111
119
|
label: str | None = None,
|
|
112
120
|
notes: list[str] | None = None,
|
|
113
121
|
) -> None:
|
|
114
|
-
self.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
scope = self.path
|
|
123
|
+
source_path = self.node_source_paths.get(id(node))
|
|
124
|
+
diagnostic_notes = tuple(notes) if notes else ()
|
|
125
|
+
if source_path is not None and scope:
|
|
126
|
+
diagnostic_notes = (*diagnostic_notes, f"scope: {scope}")
|
|
127
|
+
diagnostic = ReadDiagnostic(
|
|
128
|
+
message=message,
|
|
129
|
+
severity=Severity.WARNING,
|
|
130
|
+
span=node.span,
|
|
131
|
+
path=str(source_path) if source_path is not None else scope,
|
|
132
|
+
hint=hint,
|
|
133
|
+
code=code,
|
|
134
|
+
label=label,
|
|
135
|
+
notes=diagnostic_notes,
|
|
125
136
|
)
|
|
137
|
+
self.diagnostics.append(diagnostic)
|
|
126
138
|
|
|
127
139
|
@property
|
|
128
140
|
def path(self) -> str:
|
|
@@ -16,6 +16,7 @@ from ssc_codegen.ast import (
|
|
|
16
16
|
CssSelect,
|
|
17
17
|
CssSelectAll,
|
|
18
18
|
Fallback,
|
|
19
|
+
ExtensionCall,
|
|
19
20
|
Field,
|
|
20
21
|
Filter,
|
|
21
22
|
Fmt,
|
|
@@ -80,6 +81,7 @@ from ssc_codegen.regex_utils import normalize_regex_pattern
|
|
|
80
81
|
from typing import cast
|
|
81
82
|
|
|
82
83
|
from ssc_codegen.core.contexts import LintContext, ParseContext, WalkCtx
|
|
84
|
+
from ssc_codegen.core.extensions import resolve_extension_call
|
|
83
85
|
from ssc_codegen.core.linter import (
|
|
84
86
|
lint_pipeline_op,
|
|
85
87
|
lint_validate_css,
|
|
@@ -106,6 +108,7 @@ FieldLikeNode: TypeAlias = (
|
|
|
106
108
|
| Field
|
|
107
109
|
| InitField
|
|
108
110
|
| FunctionDef
|
|
111
|
+
| CheckMethod
|
|
109
112
|
)
|
|
110
113
|
|
|
111
114
|
|
|
@@ -343,6 +346,9 @@ def _build_expression(
|
|
|
343
346
|
f"'self {ref_name}' syntax is no longer supported; use '@{ref_name}' instead"
|
|
344
347
|
)
|
|
345
348
|
|
|
349
|
+
if name.startswith("!"):
|
|
350
|
+
return _expr_extension(node, parent, ctx, lint)
|
|
351
|
+
|
|
346
352
|
# dispatch
|
|
347
353
|
handler = _EXPRESSION_HANDLERS.get(name)
|
|
348
354
|
if handler is None:
|
|
@@ -417,6 +423,13 @@ def parse_expressions(
|
|
|
417
423
|
raise BuildTimeError(
|
|
418
424
|
f"'self {ref_name}' syntax is no longer supported; use '@{ref_name}' instead"
|
|
419
425
|
)
|
|
426
|
+
if node.name.startswith("!"):
|
|
427
|
+
lint.push(node.name)
|
|
428
|
+
lint_pipeline_op(node, lint)
|
|
429
|
+
extension_expr = _expr_extension(node, parent, ctx, lint)
|
|
430
|
+
parent.body.append(extension_expr)
|
|
431
|
+
lint.pop()
|
|
432
|
+
continue
|
|
420
433
|
handler = _EXPRESSION_HANDLERS.get(node.name)
|
|
421
434
|
if handler is None:
|
|
422
435
|
lint_wildcard_op(node, ctx, lint)
|
|
@@ -501,7 +514,10 @@ def parse_expressions(
|
|
|
501
514
|
break
|
|
502
515
|
parent.is_array = is_arr
|
|
503
516
|
parent.ret_type_info = TypeInfo(
|
|
504
|
-
base=last_ret,
|
|
517
|
+
base=last_ret,
|
|
518
|
+
is_array=is_arr,
|
|
519
|
+
is_optional=last_ti.is_optional,
|
|
520
|
+
ref=ref or last_ti.ref,
|
|
505
521
|
)
|
|
506
522
|
|
|
507
523
|
lint.walk_context = prev_ctx
|
|
@@ -537,6 +553,59 @@ def _prev_type_info(
|
|
|
537
553
|
)
|
|
538
554
|
|
|
539
555
|
|
|
556
|
+
def _expr_extension(
|
|
557
|
+
node: KdlNode,
|
|
558
|
+
parent: FieldLikeNode,
|
|
559
|
+
ctx: ParseContext,
|
|
560
|
+
lint: LintContext,
|
|
561
|
+
) -> ExtensionCall:
|
|
562
|
+
ref = node.name[1:]
|
|
563
|
+
if node.args or node.properties or node.children:
|
|
564
|
+
lint.error(
|
|
565
|
+
node,
|
|
566
|
+
message=f"custom operation '!{ref}' does not accept entries",
|
|
567
|
+
code="E001",
|
|
568
|
+
)
|
|
569
|
+
definition = ctx.extensions.get(ref)
|
|
570
|
+
prev = _prev_type_info(parent)
|
|
571
|
+
if definition is None:
|
|
572
|
+
lint.error(
|
|
573
|
+
node,
|
|
574
|
+
message=f"unknown extension operation '{ref}'",
|
|
575
|
+
code="E300",
|
|
576
|
+
hint="import its extension namespace and check the operation name",
|
|
577
|
+
)
|
|
578
|
+
return ExtensionCall(
|
|
579
|
+
parent=parent,
|
|
580
|
+
qualified_name=ref,
|
|
581
|
+
accept_type_info=prev.ret_type_info,
|
|
582
|
+
ret_type_info=TypeInfo(base=VariableType.AUTO),
|
|
583
|
+
)
|
|
584
|
+
if not definition.accept.accepts(prev.ret_type_info):
|
|
585
|
+
expected = (
|
|
586
|
+
definition.accept.generic
|
|
587
|
+
or (definition.accept.base or VariableType.AUTO).name
|
|
588
|
+
)
|
|
589
|
+
lint.error(
|
|
590
|
+
node,
|
|
591
|
+
message=(
|
|
592
|
+
f"custom operation '{ref}' does not accept "
|
|
593
|
+
f"{prev.ret_type_info.base.name}; expected {expected}"
|
|
594
|
+
),
|
|
595
|
+
code="E100",
|
|
596
|
+
)
|
|
597
|
+
ret_type = resolve_extension_call(definition, prev.ret_type_info)
|
|
598
|
+
return ExtensionCall(
|
|
599
|
+
parent=parent,
|
|
600
|
+
qualified_name=ref,
|
|
601
|
+
definition=definition,
|
|
602
|
+
accept_type_info=prev.ret_type_info,
|
|
603
|
+
ret_type_info=ret_type,
|
|
604
|
+
is_array=ret_type.is_array,
|
|
605
|
+
span=node.span,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
|
|
540
609
|
# Ops whose handlers access ``parent.body[-1]`` unconditionally and therefore
|
|
541
610
|
# require a preceding node. Used by ``parse_expressions`` to emit a clean
|
|
542
611
|
# E100 diagnostic instead of crashing with IndexError on an empty pipeline.
|
|
@@ -606,6 +675,9 @@ def _reg_expr(name: str):
|
|
|
606
675
|
def _expr_css(
|
|
607
676
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
608
677
|
):
|
|
678
|
+
if not node.children and not node.args:
|
|
679
|
+
lint.error(node, message="'css' requires a selector", code="E001")
|
|
680
|
+
return None
|
|
609
681
|
if node.children:
|
|
610
682
|
queries = [
|
|
611
683
|
resolve_selector_child_name(c.name, ctx) for c in node.children
|
|
@@ -622,6 +694,9 @@ def _expr_css(
|
|
|
622
694
|
def _expr_css_all(
|
|
623
695
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
624
696
|
):
|
|
697
|
+
if not node.children and not node.args:
|
|
698
|
+
lint.error(node, message="'css-all' requires a selector", code="E001")
|
|
699
|
+
return None
|
|
625
700
|
if node.children:
|
|
626
701
|
queries = [
|
|
627
702
|
resolve_selector_child_name(c.name, ctx) for c in node.children
|
|
@@ -638,6 +713,9 @@ def _expr_css_all(
|
|
|
638
713
|
def _expr_xpath(
|
|
639
714
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
640
715
|
):
|
|
716
|
+
if not node.children and not node.args:
|
|
717
|
+
lint.error(node, message="'xpath' requires an expression", code="E001")
|
|
718
|
+
return None
|
|
641
719
|
if node.children:
|
|
642
720
|
queries = [
|
|
643
721
|
resolve_selector_child_name(c.name, ctx) for c in node.children
|
|
@@ -654,6 +732,11 @@ def _expr_xpath(
|
|
|
654
732
|
def _expr_xpath_all(
|
|
655
733
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
656
734
|
):
|
|
735
|
+
if not node.children and not node.args:
|
|
736
|
+
lint.error(
|
|
737
|
+
node, message="'xpath-all' requires an expression", code="E001"
|
|
738
|
+
)
|
|
739
|
+
return None
|
|
657
740
|
if node.children:
|
|
658
741
|
queries = [
|
|
659
742
|
resolve_selector_child_name(c.name, ctx) for c in node.children
|