ssc_codegen 0.8.16__tar.gz → 0.8.18__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.
Potentially problematic release.
This version of ssc_codegen might be problematic. Click here for more details.
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/PKG-INFO +2 -1
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/pyproject.toml +2 -1
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/__init__.py +7 -2
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/__init__.py +4 -1
- ssc_codegen-0.8.18/ssc_codegen/ast_/nodes_filter.py +151 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_build/main.py +19 -1
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/base.py +31 -6
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/go_goquery.py +1 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/helpers.py +9 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/js_pure.py +206 -6
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/py_base.py +193 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/document.py +210 -2
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/str_utils.py +4 -1
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/tokens.py +22 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/.gitignore +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/LICENSE +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/README.md +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/_compat.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/base.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_array.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_cast.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_core.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_selectors.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_string.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_validate.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_build/__init__.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_build/utils.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/__init__.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/cli_callbacks.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/cli_utils.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/code_callbacks.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/consts.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/main.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/runtime_parse_runners.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/compiler.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/consts.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/__init__.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/py_bs4.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/py_parsel.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/py_selectolax.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/templates/__init__.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/templates/go_goquery.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/templates/js_pure.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/document_utlis.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/json_struct.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/json_to_scc.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/logs.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/pseudo_selectors.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/schema.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/selector_utils.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/static_checker/__init__.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/static_checker/base.py +0 -0
- {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/static_checker/callbacks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssc_codegen
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.18
|
|
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
|
|
@@ -19,6 +19,7 @@ Classifier: Topic :: Software Development :: Code Generators
|
|
|
19
19
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
20
20
|
Classifier: Topic :: Utilities
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: bs4>=0.0.2
|
|
22
23
|
Requires-Dist: colorama>=0.4.6; sys_platform == 'win32'
|
|
23
24
|
Requires-Dist: cssselect>=1.2.0
|
|
24
25
|
Requires-Dist: httpx>=0.28.1
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ssc_codegen"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.18"
|
|
4
4
|
description = "Python-dsl code converter to html parser for web scraping "
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
|
+
"bs4>=0.0.2",
|
|
8
9
|
"colorama>=0.4.6 ; sys_platform == 'win32'",
|
|
9
10
|
"cssselect>=1.2.0",
|
|
10
11
|
"httpx>=0.28.1",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from ssc_codegen.document import HTMLDocument, StringDocument, ArrayDocument, AssertDocument, NestedDocument, \
|
|
2
2
|
DefaultDocument, \
|
|
3
|
-
NumericDocument, JsonDocument, BooleanDocument
|
|
3
|
+
NumericDocument, JsonDocument, BooleanDocument, DocumentFilter
|
|
4
4
|
from ssc_codegen.json_struct import Json
|
|
5
5
|
from ssc_codegen.logs import setup_logger
|
|
6
6
|
from ssc_codegen.schema import ItemSchema, DictSchema, ListSchema, FlatListSchema
|
|
7
7
|
|
|
8
8
|
setup_logger()
|
|
9
9
|
|
|
10
|
-
VERSION = "0.8.
|
|
10
|
+
VERSION = "0.8.18"
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class __MISSING(object):
|
|
@@ -59,3 +59,8 @@ def R(default: None | str | int | float | list | __MISSING = _NO_DEFAULT) -> Doc
|
|
|
59
59
|
if default == _NO_DEFAULT:
|
|
60
60
|
return Document().raw()
|
|
61
61
|
return Document().default(default).raw() # type: ignore
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def F() -> DocumentFilter:
|
|
65
|
+
"""Shortcut as a DocumentFilter() object"""
|
|
66
|
+
return DocumentFilter()
|
|
@@ -20,9 +20,12 @@ from .nodes_core import (ExprReturn,
|
|
|
20
20
|
TypeDef,
|
|
21
21
|
TypeDefField,
|
|
22
22
|
ExprCallStructMethod)
|
|
23
|
+
from .nodes_filter import FilterOr, FilterAnd, FilterNot, FilterNotEqual, FilterEqual, FilterStrIn, \
|
|
24
|
+
FilterStrStarts, FilterStrEnds, FilterStrRe, ExprFilter, FilterStrLenEq, FilterStrLenNe, FilterStrLenLt, \
|
|
25
|
+
FilterStrLenLe, FilterStrLenGt, FilterStrLenGe
|
|
26
|
+
|
|
23
27
|
from .nodes_selectors import ExprCss, ExprCssAll, ExprXpathAll, ExprXpath, ExprGetHtmlText, ExprGetHtmlRaw, \
|
|
24
28
|
ExprGetHtmlAttr, ExprGetHtmlAttrAll, ExprGetHtmlRawAll, ExprGetHtmlTextAll
|
|
25
|
-
|
|
26
29
|
from .nodes_string import ExprStringTrim, ExprStringRegex, ExprStringSplit, ExprStringReplace, ExprStringLeftTrim, \
|
|
27
30
|
ExprStringFormat, ExprListStringTrim, ExprStringRegexAll, ExprStringRegexSub, ExprStringRightTrim, \
|
|
28
31
|
ExprListStringFormat, ExprListStringReplace, ExprListStringRightTrim, ExprListStringLeftTrim, \
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TypedDict, ClassVar
|
|
3
|
+
|
|
4
|
+
from ssc_codegen.ast_.base import BaseAstNode, T_EMPTY_KWARGS
|
|
5
|
+
from ssc_codegen.tokens import TokenType, VariableType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# TODO: static analyzer for filter operations
|
|
9
|
+
# TODO: implement operators for DOCUMENT, INT, FLOAT
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(kw_only=True)
|
|
13
|
+
class FilterOr(BaseAstNode[T_EMPTY_KWARGS, tuple]):
|
|
14
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_OR
|
|
15
|
+
accept_type: VariableType = VariableType.STRING
|
|
16
|
+
ret_type: VariableType = VariableType.STRING
|
|
17
|
+
# TODO: typing body accept Filter-like nodes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(kw_only=True)
|
|
21
|
+
class FilterAnd(BaseAstNode[T_EMPTY_KWARGS, tuple]):
|
|
22
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_AND
|
|
23
|
+
accept_type: VariableType = VariableType.STRING
|
|
24
|
+
ret_type: VariableType = VariableType.STRING
|
|
25
|
+
# TODO: typing body accept Filter-like nodes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(kw_only=True)
|
|
29
|
+
class FilterNot(BaseAstNode[T_EMPTY_KWARGS, tuple]):
|
|
30
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_NOT
|
|
31
|
+
accept_type: VariableType = VariableType.STRING
|
|
32
|
+
ret_type: VariableType = VariableType.STRING
|
|
33
|
+
# TODO: typing body accept Filter-like nodes
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
KW_STR_IN = TypedDict("KW_STR_IN", {"substr": tuple[str, ...]})
|
|
37
|
+
ARGS_STR_IN = tuple[tuple[str, ...]]
|
|
38
|
+
|
|
39
|
+
KW_STR_STARTS_OR_ENDS = TypedDict(
|
|
40
|
+
"KW_STR_STARTS_OR_ENDS", {"substr": tuple[str, ...]}
|
|
41
|
+
)
|
|
42
|
+
ARGS_STR_STARTS_OR_ENDS = tuple[tuple[str, ...]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(kw_only=True)
|
|
46
|
+
class FilterStrIn(BaseAstNode[KW_STR_IN, ARGS_STR_IN]):
|
|
47
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_IN
|
|
48
|
+
accept_type: VariableType = VariableType.STRING
|
|
49
|
+
ret_type: VariableType = VariableType.STRING
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(kw_only=True)
|
|
53
|
+
class FilterStrStarts(
|
|
54
|
+
BaseAstNode[KW_STR_STARTS_OR_ENDS, ARGS_STR_STARTS_OR_ENDS]
|
|
55
|
+
):
|
|
56
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_STARTS
|
|
57
|
+
accept_type: VariableType = VariableType.STRING
|
|
58
|
+
ret_type: VariableType = VariableType.STRING
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(kw_only=True)
|
|
62
|
+
class FilterStrEnds(
|
|
63
|
+
BaseAstNode[KW_STR_STARTS_OR_ENDS, ARGS_STR_STARTS_OR_ENDS]
|
|
64
|
+
):
|
|
65
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_ENDS
|
|
66
|
+
accept_type: VariableType = VariableType.STRING
|
|
67
|
+
ret_type: VariableType = VariableType.STRING
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
KW_STR_RE = TypedDict("KW_STR_RE", {"pattern": str, "ignore_case": bool})
|
|
71
|
+
ARGS_STR_RE = tuple[str, bool]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(kw_only=True)
|
|
75
|
+
class FilterStrRe(BaseAstNode[KW_STR_RE, ARGS_STR_RE]):
|
|
76
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_RE
|
|
77
|
+
accept_type: VariableType = VariableType.STRING
|
|
78
|
+
ret_type: VariableType = VariableType.STRING
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
KW_STR_LEN = TypedDict("KW_STR_LEN", {"length": int})
|
|
82
|
+
ARGS_STR_LEN = tuple[int]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(kw_only=True)
|
|
86
|
+
class FilterStrLenEq(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
|
|
87
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_EQ
|
|
88
|
+
accept_type: VariableType = VariableType.STRING
|
|
89
|
+
ret_type: VariableType = VariableType.STRING
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(kw_only=True)
|
|
93
|
+
class FilterStrLenNe(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
|
|
94
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_NE
|
|
95
|
+
accept_type: VariableType = VariableType.STRING
|
|
96
|
+
ret_type: VariableType = VariableType.STRING
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(kw_only=True)
|
|
100
|
+
class FilterStrLenLt(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
|
|
101
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_LT
|
|
102
|
+
accept_type: VariableType = VariableType.STRING
|
|
103
|
+
ret_type: VariableType = VariableType.STRING
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(kw_only=True)
|
|
107
|
+
class FilterStrLenLe(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
|
|
108
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_LE
|
|
109
|
+
accept_type: VariableType = VariableType.STRING
|
|
110
|
+
ret_type: VariableType = VariableType.STRING
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(kw_only=True)
|
|
114
|
+
class FilterStrLenGt(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
|
|
115
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_GT
|
|
116
|
+
accept_type: VariableType = VariableType.STRING
|
|
117
|
+
ret_type: VariableType = VariableType.STRING
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(kw_only=True)
|
|
121
|
+
class FilterStrLenGe(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
|
|
122
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_GE
|
|
123
|
+
accept_type: VariableType = VariableType.STRING
|
|
124
|
+
ret_type: VariableType = VariableType.STRING
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# TODO: provide API for int, float, etc
|
|
128
|
+
KW_STR_EQ_OR_NE = TypedDict("KW_STR_EQ_OR_NE", {"values": tuple[str, ...]})
|
|
129
|
+
ARGS_STR_EQ_OR_NE = tuple[tuple[str, ...]]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(kw_only=True)
|
|
133
|
+
class FilterEqual(BaseAstNode[KW_STR_EQ_OR_NE, ARGS_STR_EQ_OR_NE]):
|
|
134
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_EQ
|
|
135
|
+
accept_type: VariableType = VariableType.STRING
|
|
136
|
+
ret_type: VariableType = VariableType.STRING
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(kw_only=True)
|
|
140
|
+
class FilterNotEqual(BaseAstNode[KW_STR_EQ_OR_NE, ARGS_STR_EQ_OR_NE]):
|
|
141
|
+
kind: ClassVar[TokenType] = TokenType.FILTER_NE
|
|
142
|
+
accept_type: VariableType = VariableType.STRING
|
|
143
|
+
ret_type: VariableType = VariableType.STRING
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(kw_only=True)
|
|
147
|
+
class ExprFilter(BaseAstNode[T_EMPTY_KWARGS, tuple]):
|
|
148
|
+
kind: ClassVar[TokenType] = TokenType.EXPR_FILTER
|
|
149
|
+
accept_type: VariableType = VariableType.LIST_STRING
|
|
150
|
+
ret_type: VariableType = VariableType.LIST_STRING
|
|
151
|
+
# TODO: typing body accept Filter-like nodes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import warnings
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Type, cast
|
|
3
|
+
from typing import Type, cast, MutableSequence
|
|
4
4
|
|
|
5
5
|
from ssc_codegen import Json
|
|
6
6
|
from ssc_codegen.ast_ import (
|
|
@@ -230,6 +230,15 @@ def build_ast_struct_parser(
|
|
|
230
230
|
return st
|
|
231
231
|
|
|
232
232
|
|
|
233
|
+
def set_parent_for_expr_filter(
|
|
234
|
+
parent: BaseAstNode, parent_body: MutableSequence[BaseAstNode]
|
|
235
|
+
) -> None:
|
|
236
|
+
for j in parent_body:
|
|
237
|
+
j.parent = parent
|
|
238
|
+
if j.body:
|
|
239
|
+
set_parent_for_expr_filter(j, j.body)
|
|
240
|
+
|
|
241
|
+
|
|
233
242
|
def _fetch_field_nodes(
|
|
234
243
|
body: list[BaseAstNode],
|
|
235
244
|
body_parse_method_expr: list[BaseAstNode],
|
|
@@ -263,8 +272,12 @@ def _fetch_field_nodes(
|
|
|
263
272
|
)
|
|
264
273
|
|
|
265
274
|
_unwrap_default_node(document, ret_type)
|
|
275
|
+
|
|
266
276
|
for i in document.stack:
|
|
267
277
|
i.parent = method
|
|
278
|
+
if i.kind == TokenType.EXPR_FILTER:
|
|
279
|
+
set_parent_for_expr_filter(i, i.body)
|
|
280
|
+
|
|
268
281
|
method.body = document.stack
|
|
269
282
|
|
|
270
283
|
body.append(method)
|
|
@@ -361,6 +374,9 @@ def _try_fetch_split_doc_node(
|
|
|
361
374
|
)
|
|
362
375
|
for i in split_doc.stack:
|
|
363
376
|
i.parent = method
|
|
377
|
+
if i.kind == TokenType.EXPR_FILTER:
|
|
378
|
+
set_parent_for_expr_filter(i, i.body)
|
|
379
|
+
|
|
364
380
|
method.body = split_doc.stack
|
|
365
381
|
body.append(method)
|
|
366
382
|
|
|
@@ -388,6 +404,8 @@ def _try_fetch_pre_validate_node(
|
|
|
388
404
|
validate_field.stack.append(ExprNoReturn())
|
|
389
405
|
for i in validate_field.stack:
|
|
390
406
|
i.parent = method
|
|
407
|
+
if i.kind == TokenType.EXPR_FILTER:
|
|
408
|
+
set_parent_for_expr_filter(i, i.body)
|
|
391
409
|
|
|
392
410
|
body.append(method)
|
|
393
411
|
call_methods_expr.append(
|
|
@@ -14,6 +14,9 @@ from ssc_codegen.ast_ import (
|
|
|
14
14
|
StructInitMethod,
|
|
15
15
|
StructPartDocMethod,
|
|
16
16
|
StructPreValidateMethod,
|
|
17
|
+
FilterOr,
|
|
18
|
+
FilterAnd,
|
|
19
|
+
FilterNot,
|
|
17
20
|
)
|
|
18
21
|
from ssc_codegen.tokens import TokenType
|
|
19
22
|
|
|
@@ -168,12 +171,22 @@ class BaseCodeConverter:
|
|
|
168
171
|
case TokenType.JSON_FIELD:
|
|
169
172
|
ast_entry = cast(JsonStructField, ast_entry)
|
|
170
173
|
self._convert_json_struct_field(ast_entry, acc)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
TokenType.
|
|
174
|
-
TokenType.
|
|
175
|
-
TokenType.
|
|
176
|
-
|
|
174
|
+
|
|
175
|
+
case (
|
|
176
|
+
TokenType.EXPR_FILTER
|
|
177
|
+
| TokenType.FILTER_OR
|
|
178
|
+
| TokenType.FILTER_AND
|
|
179
|
+
| TokenType.FILTER_NOT
|
|
180
|
+
):
|
|
181
|
+
ast_entry = cast(FilterOr | FilterAnd | FilterNot, ast_entry)
|
|
182
|
+
self._convert_logic_filter(ast_entry, acc)
|
|
183
|
+
|
|
184
|
+
case (
|
|
185
|
+
TokenType.STRUCT_PART_DOCUMENT
|
|
186
|
+
| TokenType.STRUCT_PRE_VALIDATE
|
|
187
|
+
| TokenType.STRUCT_PARSE_START
|
|
188
|
+
| TokenType.STRUCT_FIELD
|
|
189
|
+
):
|
|
177
190
|
ast_entry = cast(
|
|
178
191
|
StructFieldMethod
|
|
179
192
|
| StructInitMethod
|
|
@@ -186,6 +199,18 @@ class BaseCodeConverter:
|
|
|
186
199
|
self._convert_default(ast_entry, acc)
|
|
187
200
|
return acc
|
|
188
201
|
|
|
202
|
+
def _convert_logic_filter(
|
|
203
|
+
self, ast_entry: FilterOr | FilterAnd | FilterNot, acc: list[str]
|
|
204
|
+
) -> None:
|
|
205
|
+
# AND(body=[F1, F2]) ->
|
|
206
|
+
# AND ( (OPEN)
|
|
207
|
+
# F1 and F2
|
|
208
|
+
# ) (CLOSE)
|
|
209
|
+
acc.append(self._pre_convert_node(ast_entry))
|
|
210
|
+
for node in ast_entry.body:
|
|
211
|
+
self.convert(node, acc)
|
|
212
|
+
acc.append(self._post_convert_node(ast_entry))
|
|
213
|
+
|
|
189
214
|
def _convert_module(self, ast_entry: ModuleProgram, acc: list[str]) -> None:
|
|
190
215
|
"""Handle module conversion."""
|
|
191
216
|
for node in ast_entry.body:
|
|
@@ -47,3 +47,12 @@ def get_struct_field_method_by_name(
|
|
|
47
47
|
][0]
|
|
48
48
|
result = cast(StructFieldMethod, result)
|
|
49
49
|
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_prev_node_atomic_cond(node: BaseAstNode) -> bool:
|
|
53
|
+
"""return true if node is atomic condition (exclude body), NOT FIRST"""
|
|
54
|
+
return node.index != 0 and not node.parent.body[node.index_prev].body
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_first_node_cond(node: BaseAstNode) -> bool:
|
|
58
|
+
return node.index == 0 and node.parent.kind == TokenType.EXPR_FILTER
|
|
@@ -85,12 +85,30 @@ from ssc_codegen.ast_ import (
|
|
|
85
85
|
ExprListStringAllRegex,
|
|
86
86
|
ExprListHasAttr,
|
|
87
87
|
ExprHasAttr,
|
|
88
|
+
FilterEqual,
|
|
89
|
+
FilterNotEqual,
|
|
90
|
+
FilterStrRe,
|
|
91
|
+
FilterStrEnds,
|
|
92
|
+
FilterStrStarts,
|
|
93
|
+
FilterStrIn,
|
|
94
|
+
FilterNot,
|
|
95
|
+
FilterAnd,
|
|
96
|
+
FilterOr,
|
|
97
|
+
ExprFilter,
|
|
98
|
+
FilterStrLenEq,
|
|
99
|
+
FilterStrLenNe,
|
|
100
|
+
FilterStrLenLt,
|
|
101
|
+
FilterStrLenLe,
|
|
102
|
+
FilterStrLenGt,
|
|
103
|
+
FilterStrLenGe,
|
|
88
104
|
)
|
|
89
105
|
from ssc_codegen.converters.base import BaseCodeConverter
|
|
90
106
|
from ssc_codegen.converters.helpers import (
|
|
91
107
|
prev_next_var,
|
|
92
108
|
is_last_var_no_ret,
|
|
93
109
|
have_pre_validate_call,
|
|
110
|
+
is_first_node_cond,
|
|
111
|
+
is_prev_node_atomic_cond,
|
|
94
112
|
)
|
|
95
113
|
from ssc_codegen.converters.templates.js_pure import (
|
|
96
114
|
J2_STRUCT_INIT,
|
|
@@ -130,11 +148,15 @@ CONVERTER = BaseCodeConverter(debug_comment_prefix="// ")
|
|
|
130
148
|
|
|
131
149
|
|
|
132
150
|
# TODO: move to string_utils
|
|
133
|
-
def to_js_regexp(
|
|
151
|
+
def to_js_regexp(
|
|
152
|
+
pattern: str, ignore_case: bool = False, is_global: bool = True
|
|
153
|
+
) -> str:
|
|
134
154
|
"""helper function for convert string pattern to js"""
|
|
135
155
|
pattern = pattern.replace("<\\/", "</")
|
|
136
156
|
pattern = pattern.replace("/", "\\/")
|
|
137
|
-
pattern = f"/{pattern}/
|
|
157
|
+
pattern = f"/{pattern}/"
|
|
158
|
+
if is_global:
|
|
159
|
+
pattern += "g"
|
|
138
160
|
if ignore_case:
|
|
139
161
|
pattern += "i"
|
|
140
162
|
return pattern
|
|
@@ -368,8 +390,8 @@ def pre_list_str_replace(node: ExprListStringReplace) -> str:
|
|
|
368
390
|
def pre_str_regex(node: ExprStringRegex) -> str:
|
|
369
391
|
prv, nxt = prev_next_var(node)
|
|
370
392
|
pattern, group, ignore_case = node.unpack_args()
|
|
371
|
-
pattern = to_js_regexp(pattern, ignore_case)
|
|
372
|
-
return f"let {nxt} =
|
|
393
|
+
pattern = to_js_regexp(pattern, ignore_case, is_global=False)
|
|
394
|
+
return f"let {nxt} = {prv}.match({pattern})[{group}];"
|
|
373
395
|
|
|
374
396
|
|
|
375
397
|
@CONVERTER(ExprStringRegexAll.kind)
|
|
@@ -377,8 +399,7 @@ def pre_str_regex_all(node: ExprStringRegexAll) -> str:
|
|
|
377
399
|
prv, nxt = prev_next_var(node)
|
|
378
400
|
pattern, ignore_case = node.unpack_args()
|
|
379
401
|
pattern = to_js_regexp(pattern, ignore_case)
|
|
380
|
-
|
|
381
|
-
return f"let {nxt} = (new RegExp({pattern})).exec({prv});"
|
|
402
|
+
return f"let {nxt} = Array.from({prv}.match({pattern}));"
|
|
382
403
|
|
|
383
404
|
|
|
384
405
|
@CONVERTER(ExprStringRegexSub.kind)
|
|
@@ -714,3 +735,182 @@ def pre_list_has_attr(node: ExprListHasAttr) -> str:
|
|
|
714
735
|
if is_last_var_no_ret(node):
|
|
715
736
|
return expr
|
|
716
737
|
return expr + f"let {nxt} = {prv};"
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@CONVERTER(ExprFilter.kind)
|
|
741
|
+
def pre_expr_filter(node: ExprFilter) -> str:
|
|
742
|
+
prv, nxt = prev_next_var(node)
|
|
743
|
+
return f"let {nxt} = {prv}.filter(i => "
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@CONVERTER.post(ExprFilter.kind)
|
|
747
|
+
def post_expr_filter(_node: ExprFilter) -> str:
|
|
748
|
+
return ");"
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
@CONVERTER(FilterOr.kind)
|
|
752
|
+
def pre_filter_or(_node: FilterOr) -> str:
|
|
753
|
+
return " || ("
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@CONVERTER.post(FilterOr.kind)
|
|
757
|
+
def post_filter_or(_node: FilterOr) -> str:
|
|
758
|
+
return ")"
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
@CONVERTER(FilterAnd.kind)
|
|
762
|
+
def pre_filter_and(_node: FilterAnd) -> str:
|
|
763
|
+
return " && ("
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
@CONVERTER.post(FilterAnd.kind)
|
|
767
|
+
def post_filter_and(_node: FilterAnd) -> str:
|
|
768
|
+
return ")"
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
@CONVERTER(FilterNot.kind)
|
|
772
|
+
def pre_filter_not(_node: FilterNot) -> str: # type: ignore
|
|
773
|
+
return "!("
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@CONVERTER.post(FilterNot.kind)
|
|
777
|
+
def post_filter_not(_node: FilterNot) -> str:
|
|
778
|
+
return ")"
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
@CONVERTER(FilterStrIn.kind)
|
|
782
|
+
def pre_filter_in(node: FilterStrIn) -> str:
|
|
783
|
+
values, *_ = node.unpack_args()
|
|
784
|
+
if len(values) == 1:
|
|
785
|
+
expr = f"i.includes({values[0]!r})"
|
|
786
|
+
else:
|
|
787
|
+
# to js array
|
|
788
|
+
val_arr = str(values)
|
|
789
|
+
val_arr = "[" + val_arr[1:-1] + "]"
|
|
790
|
+
expr = f"{val_arr}.some(e => i.includes(e))"
|
|
791
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
792
|
+
return f" && {expr}"
|
|
793
|
+
return expr
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@CONVERTER(FilterStrStarts.kind)
|
|
797
|
+
def pre_filter_starts_with(node: FilterStrStarts) -> str:
|
|
798
|
+
values, *_ = node.unpack_args()
|
|
799
|
+
if len(values) == 1:
|
|
800
|
+
expr = f"i.startsWith({values[0]!r})"
|
|
801
|
+
else:
|
|
802
|
+
# to js array
|
|
803
|
+
val_arr = str(values)
|
|
804
|
+
val_arr = "[" + val_arr[1:-1] + "]"
|
|
805
|
+
expr = f"{val_arr}.some(e => i.startsWith(e))"
|
|
806
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
807
|
+
return f" && {expr}"
|
|
808
|
+
return expr
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
@CONVERTER(FilterStrEnds.kind)
|
|
812
|
+
def pre_filter_ends_with(node: FilterStrEnds) -> str:
|
|
813
|
+
values, *_ = node.unpack_args()
|
|
814
|
+
if len(values) == 1:
|
|
815
|
+
expr = f"i.endsWith({values[0]!r})"
|
|
816
|
+
else:
|
|
817
|
+
# to js array
|
|
818
|
+
val_arr = str(values)
|
|
819
|
+
val_arr = "[" + val_arr[1:-1] + "]"
|
|
820
|
+
expr = f"{val_arr}.some(e => i.endsWith(e))"
|
|
821
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
822
|
+
return f" && {expr}"
|
|
823
|
+
return expr
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@CONVERTER(FilterStrRe.kind)
|
|
827
|
+
def pre_filter_re(node: FilterStrRe) -> str:
|
|
828
|
+
pattern, ignore_case, *_ = node.unpack_args()
|
|
829
|
+
pattern = to_js_regexp(pattern, ignore_case, is_global=False)
|
|
830
|
+
|
|
831
|
+
expr = f"(new RegExp({pattern})).test(i)"
|
|
832
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
833
|
+
return f" && {expr}"
|
|
834
|
+
return expr
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
@CONVERTER(FilterEqual.kind)
|
|
838
|
+
def pre_filter_eq(node: FilterEqual) -> str:
|
|
839
|
+
values, *_ = node.unpack_args()
|
|
840
|
+
if len(values) == 1:
|
|
841
|
+
expr = f"i === {values[0]!r}"
|
|
842
|
+
else:
|
|
843
|
+
val_arr = str(values)
|
|
844
|
+
val_arr = "[" + val_arr[1:-1] + "]"
|
|
845
|
+
expr = f"{val_arr}.every(e => i === e)"
|
|
846
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
847
|
+
return f" && {expr}"
|
|
848
|
+
return expr
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
@CONVERTER(FilterNotEqual.kind)
|
|
852
|
+
def pre_filter_ne(node: FilterNotEqual) -> str:
|
|
853
|
+
values, *_ = node.unpack_args()
|
|
854
|
+
if len(values) == 1:
|
|
855
|
+
expr = f"i !== {values[0]!r}"
|
|
856
|
+
else:
|
|
857
|
+
val_arr = str(values)
|
|
858
|
+
val_arr = "[" + val_arr[1:-1] + "]"
|
|
859
|
+
expr = f"{val_arr}.every(e => i !== e)"
|
|
860
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
861
|
+
return f" && {expr}"
|
|
862
|
+
return expr
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
@CONVERTER(FilterStrLenEq.kind)
|
|
866
|
+
def pre_filter_str_len_eq(node: FilterStrLenEq) -> str:
|
|
867
|
+
length, *_ = node.unpack_args()
|
|
868
|
+
expr = f"i.length == {length}"
|
|
869
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
870
|
+
return f" && {expr}"
|
|
871
|
+
return expr
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
@CONVERTER(FilterStrLenNe.kind)
|
|
875
|
+
def pre_filter_str_len_ne(node: FilterStrLenNe) -> str:
|
|
876
|
+
length, *_ = node.unpack_args()
|
|
877
|
+
expr = f"i.length != {length}"
|
|
878
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
879
|
+
return f" && {expr}"
|
|
880
|
+
return expr
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
@CONVERTER(FilterStrLenLt.kind)
|
|
884
|
+
def pre_filter_str_len_lt(node: FilterStrLenLt) -> str:
|
|
885
|
+
length, *_ = node.unpack_args()
|
|
886
|
+
expr = f"i.length < {length}"
|
|
887
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
888
|
+
return f" && {expr}"
|
|
889
|
+
return expr
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@CONVERTER(FilterStrLenLe.kind)
|
|
893
|
+
def pre_filter_str_len_le(node: FilterStrLenLe) -> str:
|
|
894
|
+
length, *_ = node.unpack_args()
|
|
895
|
+
expr = f"i.length <= {length}"
|
|
896
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
897
|
+
return f" && {expr}"
|
|
898
|
+
return expr
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
@CONVERTER(FilterStrLenGt.kind)
|
|
902
|
+
def pre_filter_str_len_gt(node: FilterStrLenGt) -> str:
|
|
903
|
+
length, *_ = node.unpack_args()
|
|
904
|
+
expr = f"i.length > {length}"
|
|
905
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
906
|
+
return f" && {expr}"
|
|
907
|
+
return expr
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
@CONVERTER(FilterStrLenGe.kind)
|
|
911
|
+
def pre_filter_str_len_ge(node: FilterStrLenGe) -> str:
|
|
912
|
+
length, *_ = node.unpack_args()
|
|
913
|
+
expr = f"i.length >= {length}"
|
|
914
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
915
|
+
return f" && {expr}"
|
|
916
|
+
return expr
|
|
@@ -87,6 +87,22 @@ from ssc_codegen.ast_ import (
|
|
|
87
87
|
ExprListStringRmPrefixAndSuffix,
|
|
88
88
|
ExprListStringAnyRegex,
|
|
89
89
|
ExprListStringAllRegex,
|
|
90
|
+
FilterOr,
|
|
91
|
+
FilterAnd,
|
|
92
|
+
FilterNot,
|
|
93
|
+
ExprFilter,
|
|
94
|
+
FilterStrIn,
|
|
95
|
+
FilterStrStarts,
|
|
96
|
+
FilterStrEnds,
|
|
97
|
+
FilterStrRe,
|
|
98
|
+
FilterEqual,
|
|
99
|
+
FilterNotEqual,
|
|
100
|
+
FilterStrLenEq,
|
|
101
|
+
FilterStrLenNe,
|
|
102
|
+
FilterStrLenLt,
|
|
103
|
+
FilterStrLenLe,
|
|
104
|
+
FilterStrLenGt,
|
|
105
|
+
FilterStrLenGe,
|
|
90
106
|
)
|
|
91
107
|
from ssc_codegen.converters.base import (
|
|
92
108
|
BaseCodeConverter,
|
|
@@ -98,6 +114,8 @@ from ssc_codegen.converters.helpers import (
|
|
|
98
114
|
have_pre_validate_call,
|
|
99
115
|
prev_next_var,
|
|
100
116
|
is_last_var_no_ret,
|
|
117
|
+
is_prev_node_atomic_cond,
|
|
118
|
+
is_first_node_cond,
|
|
101
119
|
)
|
|
102
120
|
from ssc_codegen.tokens import StructType, VariableType, JsonVariableType
|
|
103
121
|
|
|
@@ -181,6 +199,8 @@ class BasePyCodeConverter(BaseCodeConverter):
|
|
|
181
199
|
)
|
|
182
200
|
from ssc_codegen.ast_ import ExprListStringRmPrefix
|
|
183
201
|
|
|
202
|
+
from ssc_codegen.ast_ import FilterNotEqual
|
|
203
|
+
|
|
184
204
|
self.pre_definitions = {
|
|
185
205
|
Docstring.kind: pre_docstring,
|
|
186
206
|
ModuleImports.kind: pre_imports,
|
|
@@ -234,12 +254,33 @@ class BasePyCodeConverter(BaseCodeConverter):
|
|
|
234
254
|
ExprToListLength.kind: pre_to_len,
|
|
235
255
|
ExprToBool.kind: pre_to_bool,
|
|
236
256
|
ExprJsonify.kind: pre_jsonify,
|
|
257
|
+
# FILTER,
|
|
258
|
+
ExprFilter.kind: pre_expr_filter,
|
|
259
|
+
FilterAnd.kind: pre_filter_and,
|
|
260
|
+
FilterOr.kind: pre_filter_or,
|
|
261
|
+
FilterNot.kind: pre_filter_not,
|
|
262
|
+
FilterStrStarts.kind: pre_filter_starts_with,
|
|
263
|
+
FilterStrEnds.kind: pre_filter_ends_with,
|
|
264
|
+
FilterStrIn.kind: pre_filter_in,
|
|
265
|
+
FilterStrRe.kind: pre_filter_re,
|
|
266
|
+
FilterEqual.kind: pre_filter_eq,
|
|
267
|
+
FilterNotEqual.kind: pre_filter_ne,
|
|
268
|
+
FilterStrLenEq.kind: pre_filter_str_len_eq,
|
|
269
|
+
FilterStrLenNe.kind: pre_filter_str_len_ne,
|
|
270
|
+
FilterStrLenLt.kind: pre_filter_str_len_lt,
|
|
271
|
+
FilterStrLenLe.kind: pre_filter_str_len_le,
|
|
272
|
+
FilterStrLenGe.kind: pre_filter_str_len_ge,
|
|
273
|
+
FilterStrLenGt.kind: pre_filter_str_len_gt,
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
self.post_definitions = {
|
|
240
277
|
TypeDef.kind: post_typedef,
|
|
241
278
|
JsonStruct.kind: post_json_struct,
|
|
242
279
|
StartParseMethod.kind: post_start_parse,
|
|
280
|
+
ExprFilter.kind: post_expr_filter,
|
|
281
|
+
FilterAnd.kind: post_filter_and,
|
|
282
|
+
FilterOr.kind: post_filter_or,
|
|
283
|
+
FilterNot.kind: post_filter_not,
|
|
243
284
|
}
|
|
244
285
|
|
|
245
286
|
|
|
@@ -876,3 +917,155 @@ def pre_jsonify(node: ExprJsonify) -> str:
|
|
|
876
917
|
prv, nxt = prev_next_var(node)
|
|
877
918
|
_name, _is_array = node.unpack_args()
|
|
878
919
|
return indent + f"{nxt} = json.loads({prv})"
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
# FILTERS
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def pre_expr_filter(node: ExprFilter) -> str:
|
|
926
|
+
indent = (
|
|
927
|
+
INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
|
|
928
|
+
)
|
|
929
|
+
prv, nxt = prev_next_var(node)
|
|
930
|
+
return indent + f"{nxt} = [i for i in {prv} if "
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def post_expr_filter(_node: ExprFilter) -> str:
|
|
934
|
+
return "]"
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def pre_filter_or(_node: FilterOr) -> str:
|
|
938
|
+
return " or ("
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def post_filter_or(_node: FilterOr) -> str:
|
|
942
|
+
return ")"
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def pre_filter_and(_node: FilterAnd) -> str:
|
|
946
|
+
return " and ("
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def post_filter_and(_node: FilterAnd) -> str:
|
|
950
|
+
return ")"
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def pre_filter_not(_node: FilterNot) -> str:
|
|
954
|
+
return "not ("
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def post_filter_not(_node: FilterNot) -> str:
|
|
958
|
+
return ")"
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def pre_filter_in(node: FilterStrIn) -> str:
|
|
962
|
+
values, *_ = node.unpack_args()
|
|
963
|
+
|
|
964
|
+
if len(values) == 1:
|
|
965
|
+
expr = f"{values[0]!r} in i"
|
|
966
|
+
else:
|
|
967
|
+
expr = f"any(s in i for s in {values})"
|
|
968
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
969
|
+
return f" and {expr}"
|
|
970
|
+
return expr
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def pre_filter_starts_with(node: FilterStrStarts) -> str:
|
|
974
|
+
start_, *_ = node.unpack_args()
|
|
975
|
+
# build-in python startswith accept tuple[str, ...]
|
|
976
|
+
expr = f"i.startswith({start_})"
|
|
977
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
978
|
+
return f" and {expr}"
|
|
979
|
+
return expr
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def pre_filter_ends_with(node: FilterStrEnds) -> str:
|
|
983
|
+
suffix_, *_ = node.unpack_args()
|
|
984
|
+
# build-in python endswith accept tuple[str, ...]
|
|
985
|
+
expr = f"i.endswith({suffix_})"
|
|
986
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
987
|
+
return f" and {expr}"
|
|
988
|
+
return expr
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def pre_filter_re(node: FilterStrRe) -> str:
|
|
992
|
+
pattern, ignore_case, *_ = node.unpack_args()
|
|
993
|
+
if ignore_case:
|
|
994
|
+
expr = f"re.search({pattern!r}, i, re.IGNORECASE)"
|
|
995
|
+
else:
|
|
996
|
+
expr = f"re.search({pattern!r}, i)"
|
|
997
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
998
|
+
return f" and {expr}"
|
|
999
|
+
return expr
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def pre_filter_eq(node: FilterEqual) -> str:
|
|
1003
|
+
values, *_ = node.unpack_args()
|
|
1004
|
+
# currently support only str
|
|
1005
|
+
if len(values) == 1:
|
|
1006
|
+
expr = f"i == {values[0]!r}"
|
|
1007
|
+
else:
|
|
1008
|
+
expr = f"all(s == i for s in {values})"
|
|
1009
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
1010
|
+
return f" and {expr}"
|
|
1011
|
+
return expr
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def pre_filter_ne(node: FilterNotEqual) -> str:
|
|
1015
|
+
values, *_ = node.unpack_args()
|
|
1016
|
+
# currently support only str
|
|
1017
|
+
if len(values) == 1:
|
|
1018
|
+
expr = f"i != {values[0]!r}"
|
|
1019
|
+
else:
|
|
1020
|
+
expr = f"all(s != i for s in {values})"
|
|
1021
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
1022
|
+
return f" and {expr}"
|
|
1023
|
+
return expr
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def pre_filter_str_len_eq(node: FilterStrLenEq) -> str:
|
|
1027
|
+
length, *_ = node.unpack_args()
|
|
1028
|
+
expr = f"len(i) == {length}"
|
|
1029
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
1030
|
+
return f" and {expr}"
|
|
1031
|
+
return expr
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def pre_filter_str_len_ne(node: FilterStrLenNe) -> str:
|
|
1035
|
+
length, *_ = node.unpack_args()
|
|
1036
|
+
expr = f"len(i) != {length}"
|
|
1037
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
1038
|
+
return f" and {expr}"
|
|
1039
|
+
return expr
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def pre_filter_str_len_lt(node: FilterStrLenLt) -> str:
|
|
1043
|
+
length, *_ = node.unpack_args()
|
|
1044
|
+
expr = f"len(i) < {length}"
|
|
1045
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
1046
|
+
return f" and {expr}"
|
|
1047
|
+
return expr
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def pre_filter_str_len_le(node: FilterStrLenLe) -> str:
|
|
1051
|
+
length, *_ = node.unpack_args()
|
|
1052
|
+
expr = f"len(i) <= {length}"
|
|
1053
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
1054
|
+
return f" and {expr}"
|
|
1055
|
+
return expr
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def pre_filter_str_len_gt(node: FilterStrLenGt) -> str:
|
|
1059
|
+
length, *_ = node.unpack_args()
|
|
1060
|
+
expr = f"len(i) > {length}"
|
|
1061
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
1062
|
+
return f" and {expr}"
|
|
1063
|
+
return expr
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def pre_filter_str_len_ge(node: FilterStrLenGe) -> str:
|
|
1067
|
+
length, *_ = node.unpack_args()
|
|
1068
|
+
expr = f"len(i) >= {length}"
|
|
1069
|
+
if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
|
|
1070
|
+
return f" and {expr}"
|
|
1071
|
+
return expr
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""high-level AST builder interface"""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Type, Pattern
|
|
4
|
+
from typing import Type, Pattern, Sequence
|
|
5
5
|
|
|
6
6
|
from cssselect import SelectorSyntaxError
|
|
7
7
|
from typing_extensions import Self, assert_never
|
|
@@ -59,18 +59,34 @@ from ssc_codegen.ast_ import (
|
|
|
59
59
|
ExprListStringAnyRegex,
|
|
60
60
|
ExprHasAttr,
|
|
61
61
|
ExprListHasAttr,
|
|
62
|
+
ExprFilter,
|
|
63
|
+
FilterEqual,
|
|
64
|
+
FilterNotEqual,
|
|
65
|
+
FilterStrStarts,
|
|
66
|
+
FilterStrEnds,
|
|
67
|
+
FilterStrIn,
|
|
68
|
+
FilterStrRe,
|
|
69
|
+
FilterAnd,
|
|
70
|
+
FilterOr,
|
|
71
|
+
FilterNot,
|
|
72
|
+
FilterStrLenEq,
|
|
73
|
+
FilterStrLenNe,
|
|
74
|
+
FilterStrLenLt,
|
|
75
|
+
FilterStrLenLe,
|
|
76
|
+
FilterStrLenGt,
|
|
77
|
+
FilterStrLenGe,
|
|
62
78
|
)
|
|
63
79
|
from ssc_codegen.document_utlis import (
|
|
64
80
|
analyze_re_expression,
|
|
65
81
|
unverbosify_regex,
|
|
66
82
|
is_ignore_case_regex,
|
|
67
83
|
)
|
|
84
|
+
from ssc_codegen.json_struct import Json
|
|
68
85
|
from ssc_codegen.pseudo_selectors import (
|
|
69
86
|
parse_pseudo_xpath_query,
|
|
70
87
|
parse_pseudo_css_query,
|
|
71
88
|
PseudoAction,
|
|
72
89
|
)
|
|
73
|
-
from ssc_codegen.json_struct import Json
|
|
74
90
|
from ssc_codegen.schema import BaseSchema
|
|
75
91
|
from ssc_codegen.selector_utils import validate_css_query, validate_xpath_query
|
|
76
92
|
from ssc_codegen.tokens import VariableType
|
|
@@ -457,6 +473,20 @@ class ArrayDocument(BaseDocument):
|
|
|
457
473
|
self._add(ExprToListLength(accept_type=expected_type))
|
|
458
474
|
return self
|
|
459
475
|
|
|
476
|
+
def filter(self, expr: "DocumentFilter") -> Self:
|
|
477
|
+
"""filter array of resuts by expr
|
|
478
|
+
|
|
479
|
+
- accept LIST_STRING, return LIST_STRING
|
|
480
|
+
"""
|
|
481
|
+
if self.stack_last_ret != VariableType.LIST_STRING:
|
|
482
|
+
LOGGER.warning(
|
|
483
|
+
"to_len(): Expected type(s) %s got %s",
|
|
484
|
+
(VariableType.LIST_STRING.name,),
|
|
485
|
+
self.stack_last_ret.name,
|
|
486
|
+
)
|
|
487
|
+
self._add(ExprFilter(body=expr.stack))
|
|
488
|
+
return self
|
|
489
|
+
|
|
460
490
|
|
|
461
491
|
class StringDocument(BaseDocument):
|
|
462
492
|
def rm_prefix(self, substr: str) -> Self:
|
|
@@ -1236,3 +1266,181 @@ class JsonDocument(BaseDocument):
|
|
|
1236
1266
|
# HACK: store to class instance for generate docstring signature
|
|
1237
1267
|
BaseSchema.__JSON_SCHEMAS__[struct.__name__] = struct # type: ignore
|
|
1238
1268
|
return self
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
class DocumentFilter(BaseDocument):
|
|
1272
|
+
def eq(self, *values: str) -> Self:
|
|
1273
|
+
"""check if value equal
|
|
1274
|
+
|
|
1275
|
+
multiple values converts to logical AND
|
|
1276
|
+
|
|
1277
|
+
pseudocode example:
|
|
1278
|
+
|
|
1279
|
+
F().eq("foo") -> value == "foo"
|
|
1280
|
+
|
|
1281
|
+
F().eq("bar", "foo") -> (value == "bar" && value == "foo")
|
|
1282
|
+
"""
|
|
1283
|
+
self._add(FilterEqual(kwargs={"values": values}))
|
|
1284
|
+
return self
|
|
1285
|
+
|
|
1286
|
+
def ne(self, *values: str) -> Self:
|
|
1287
|
+
"""check if value not equal
|
|
1288
|
+
|
|
1289
|
+
multiple values converts to logical AND
|
|
1290
|
+
|
|
1291
|
+
pseudocode example:
|
|
1292
|
+
|
|
1293
|
+
F().eq("foo") -> value == "foo"
|
|
1294
|
+
|
|
1295
|
+
F().eq("bar", "foo") -> (value != "bar" && value != "foo")
|
|
1296
|
+
"""
|
|
1297
|
+
self._add(FilterNotEqual(kwargs={"values": values}))
|
|
1298
|
+
return self
|
|
1299
|
+
|
|
1300
|
+
def starts(self, *values: str) -> Self:
|
|
1301
|
+
"""check if value starts by substring
|
|
1302
|
+
|
|
1303
|
+
multiple values converts to logical OR
|
|
1304
|
+
|
|
1305
|
+
pseudocode example:
|
|
1306
|
+
|
|
1307
|
+
F().eq("foo") -> value == "foo"
|
|
1308
|
+
|
|
1309
|
+
F().eq("foo", "bar") -> (value.starts_with("bar") || value.starts_with("foo"))
|
|
1310
|
+
"""
|
|
1311
|
+
self._add(FilterStrStarts(kwargs={"substr": values}))
|
|
1312
|
+
return self
|
|
1313
|
+
|
|
1314
|
+
def ends(self, *values: str) -> Self:
|
|
1315
|
+
"""check if value starts by substring
|
|
1316
|
+
|
|
1317
|
+
multiple values converts to logical OR
|
|
1318
|
+
|
|
1319
|
+
pseudocode example:
|
|
1320
|
+
|
|
1321
|
+
F().eq("foo") -> value == "foo"
|
|
1322
|
+
|
|
1323
|
+
F().eq("foo", "bar") -> (value.ends_with("bar") || value.ends_with("foo"))
|
|
1324
|
+
"""
|
|
1325
|
+
self._add(FilterStrEnds(kwargs={"substr": values}))
|
|
1326
|
+
return self
|
|
1327
|
+
|
|
1328
|
+
def contains(self, *values: str) -> Self:
|
|
1329
|
+
"""check if value contains by substring
|
|
1330
|
+
|
|
1331
|
+
multiple values converts to logical OR
|
|
1332
|
+
|
|
1333
|
+
pseudocode example:
|
|
1334
|
+
|
|
1335
|
+
F().eq("foo") -> value == "foo"
|
|
1336
|
+
|
|
1337
|
+
F().eq("foo", "bar") -> (value.include("bar") || value.include("foo"))
|
|
1338
|
+
"""
|
|
1339
|
+
self._add(FilterStrIn(kwargs={"substr": values}))
|
|
1340
|
+
return self
|
|
1341
|
+
|
|
1342
|
+
def re(
|
|
1343
|
+
self, pattern: str | Pattern[str], ignore_case: bool = False
|
|
1344
|
+
) -> Self:
|
|
1345
|
+
"""check if pattern matched result in value"""
|
|
1346
|
+
if not isinstance(pattern, str):
|
|
1347
|
+
ignore_case = is_ignore_case_regex(pattern)
|
|
1348
|
+
|
|
1349
|
+
pattern = unverbosify_regex(pattern)
|
|
1350
|
+
result = analyze_re_expression(pattern, allow_empty_groups=True)
|
|
1351
|
+
if not result:
|
|
1352
|
+
LOGGER.warning(result.msg)
|
|
1353
|
+
self._add(
|
|
1354
|
+
FilterStrRe(kwargs={"pattern": pattern, "ignore_case": ignore_case})
|
|
1355
|
+
)
|
|
1356
|
+
return self
|
|
1357
|
+
|
|
1358
|
+
def and_(self, filter_expr: "DocumentFilter") -> Self:
|
|
1359
|
+
if self.stack[0].kind in (FilterAnd.kind, FilterOr.kind):
|
|
1360
|
+
LOGGER.warning(
|
|
1361
|
+
"logic AND: first node is `%s`", self.stack[0].kind.name
|
|
1362
|
+
)
|
|
1363
|
+
tmp_stack = filter_expr.stack.copy()
|
|
1364
|
+
self._add(FilterAnd(body=tmp_stack))
|
|
1365
|
+
return self
|
|
1366
|
+
|
|
1367
|
+
def or_(self, filter_expr: "DocumentFilter") -> Self:
|
|
1368
|
+
if self.stack[0].kind in (FilterAnd.kind, FilterOr.kind):
|
|
1369
|
+
LOGGER.warning(
|
|
1370
|
+
"logic OR: first node is `%s`", self.stack[0].kind.name
|
|
1371
|
+
)
|
|
1372
|
+
tmp_stack = filter_expr.stack.copy()
|
|
1373
|
+
self._add(FilterOr(body=tmp_stack))
|
|
1374
|
+
return self
|
|
1375
|
+
|
|
1376
|
+
def len_eq(self, value: int) -> Self:
|
|
1377
|
+
self._add(FilterStrLenEq(kwargs={"length": value}))
|
|
1378
|
+
return self
|
|
1379
|
+
|
|
1380
|
+
def len_ne(self, value: int) -> Self:
|
|
1381
|
+
self._add(FilterStrLenNe(kwargs={"length": value}))
|
|
1382
|
+
return self
|
|
1383
|
+
|
|
1384
|
+
def len_lt(self, value: int) -> Self:
|
|
1385
|
+
self._add(FilterStrLenLt(kwargs={"length": value}))
|
|
1386
|
+
return self
|
|
1387
|
+
|
|
1388
|
+
def len_le(self, value: int) -> Self:
|
|
1389
|
+
self._add(FilterStrLenLe(kwargs={"length": value}))
|
|
1390
|
+
return self
|
|
1391
|
+
|
|
1392
|
+
def len_gt(self, value: int) -> Self:
|
|
1393
|
+
self._add(FilterStrLenGt(kwargs={"length": value}))
|
|
1394
|
+
return self
|
|
1395
|
+
|
|
1396
|
+
def len_ge(self, value: int) -> Self:
|
|
1397
|
+
self._add(FilterStrLenGe(kwargs={"length": value}))
|
|
1398
|
+
return self
|
|
1399
|
+
|
|
1400
|
+
def not_(self, filter_expr: "DocumentFilter") -> Self:
|
|
1401
|
+
tmp_stack = filter_expr.stack.copy()
|
|
1402
|
+
self._add(FilterNot(body=tmp_stack))
|
|
1403
|
+
return self
|
|
1404
|
+
|
|
1405
|
+
def __or__(self, other: "DocumentFilter") -> "DocumentFilter":
|
|
1406
|
+
"""syntax suger F().or_(...)"""
|
|
1407
|
+
|
|
1408
|
+
return self.or_(other)
|
|
1409
|
+
|
|
1410
|
+
def __and__(self, other: "DocumentFilter") -> "DocumentFilter":
|
|
1411
|
+
"""syntax sugar F().and_(...)"""
|
|
1412
|
+
return self.and_(other)
|
|
1413
|
+
|
|
1414
|
+
def __invert__(self) -> "DocumentFilter":
|
|
1415
|
+
"""syntax sugar F().not_(...)"""
|
|
1416
|
+
return DocumentFilter().not_(self)
|
|
1417
|
+
|
|
1418
|
+
def __eq__(self, other: int | str | Sequence[str]) -> Self:
|
|
1419
|
+
"""syntax sugar F().eq(...)"""
|
|
1420
|
+
if isinstance(other, int):
|
|
1421
|
+
return self.len_eq(other)
|
|
1422
|
+
|
|
1423
|
+
if isinstance(other, str):
|
|
1424
|
+
other = (other,)
|
|
1425
|
+
return self.eq(*other)
|
|
1426
|
+
|
|
1427
|
+
def __ne__(self, other: int | str | Sequence[str]) -> Self:
|
|
1428
|
+
"""syntax sugar F().ne(...)"""
|
|
1429
|
+
if isinstance(other, int):
|
|
1430
|
+
return self.len_ne(other)
|
|
1431
|
+
|
|
1432
|
+
if isinstance(other, str):
|
|
1433
|
+
other = (other,)
|
|
1434
|
+
return self.eq(*other)
|
|
1435
|
+
|
|
1436
|
+
def __lt__(self, other: int) -> Self:
|
|
1437
|
+
return self.len_lt(other)
|
|
1438
|
+
|
|
1439
|
+
def __le__(self, other: int) -> Self:
|
|
1440
|
+
return self.len_le(other)
|
|
1441
|
+
|
|
1442
|
+
def __gt__(self, other: int) -> Self:
|
|
1443
|
+
return self.len_gt(other)
|
|
1444
|
+
|
|
1445
|
+
def __ge__(self, other: int) -> Self:
|
|
1446
|
+
return self.len_ge(other)
|
|
@@ -103,7 +103,7 @@ RE_PY_METHOD_BLOCK = re.compile(
|
|
|
103
103
|
r"""
|
|
104
104
|
def\s_(?:parse|split)\w+\(.*\:\s* # SPLIT_DOC STRUCT_PARSE HEAD
|
|
105
105
|
(?:\n|.)*? # BLOCK OF CODE
|
|
106
|
-
return\s(?P<ret_var>\w+)
|
|
106
|
+
return\s(?P<ret_var>\w+) # RET STMT
|
|
107
107
|
""",
|
|
108
108
|
re.X,
|
|
109
109
|
)
|
|
@@ -148,6 +148,9 @@ def py_optimize_return_naive(py_code: str) -> str:
|
|
|
148
148
|
re_expr = f"{ret_var}\\s*=\\s*(?P<expr>.*)"
|
|
149
149
|
if match := re.search(re_expr, method_block[0]):
|
|
150
150
|
expr = match["expr"] # noqa
|
|
151
|
+
# NAIVE apologize, its .filter(...) expr, skip optimization
|
|
152
|
+
if expr.strip().endswith(" if"):
|
|
153
|
+
continue
|
|
151
154
|
|
|
152
155
|
# optimization function convert double backslash to single (\\ -> \) in regex patterns
|
|
153
156
|
# add `r` prefix for avoid SyntaxWarning then compile generated code to cpython bytecode
|
|
@@ -174,6 +174,28 @@ class TokenType(IntEnum):
|
|
|
174
174
|
# bool
|
|
175
175
|
TO_BOOL = auto()
|
|
176
176
|
|
|
177
|
+
# FILTER
|
|
178
|
+
EXPR_FILTER = auto()
|
|
179
|
+
|
|
180
|
+
FILTER_OR = auto()
|
|
181
|
+
FILTER_AND = auto()
|
|
182
|
+
FILTER_NOT = auto()
|
|
183
|
+
# FILTER EXPR (STR)
|
|
184
|
+
FILTER_STR_IN = auto()
|
|
185
|
+
FILTER_STR_STARTS = auto()
|
|
186
|
+
FILTER_STR_ENDS = auto()
|
|
187
|
+
FILTER_STR_RE = auto()
|
|
188
|
+
|
|
189
|
+
FILTER_STR_LEN_EQ = auto()
|
|
190
|
+
FILTER_STR_LEN_NE = auto()
|
|
191
|
+
FILTER_STR_LEN_LT = auto()
|
|
192
|
+
FILTER_STR_LEN_LE = auto()
|
|
193
|
+
FILTER_STR_LEN_GT = auto()
|
|
194
|
+
FILTER_STR_LEN_GE = auto()
|
|
195
|
+
|
|
196
|
+
FILTER_EQ = auto()
|
|
197
|
+
FILTER_NE = auto()
|
|
198
|
+
|
|
177
199
|
@classmethod
|
|
178
200
|
def default_tokens(cls) -> tuple["TokenType", ...]:
|
|
179
201
|
return (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|