pyrsql 0.0.1__py3-none-any.whl

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 (74) hide show
  1. pyrsql/__init__.py +63 -0
  2. pyrsql/adapters/__init__.py +3 -0
  3. pyrsql/adapters/fastapi/__init__.py +15 -0
  4. pyrsql/adapters/fastapi/config.py +76 -0
  5. pyrsql/adapters/fastapi/criteria.py +56 -0
  6. pyrsql/adapters/fastapi/dependency.py +217 -0
  7. pyrsql/adapters/fastapi/errors.py +107 -0
  8. pyrsql/api.py +76 -0
  9. pyrsql/core/__init__.py +45 -0
  10. pyrsql/core/compiler.py +81 -0
  11. pyrsql/core/conversion.py +206 -0
  12. pyrsql/core/custom.py +14 -0
  13. pyrsql/core/field_policy.py +97 -0
  14. pyrsql/core/joins.py +11 -0
  15. pyrsql/core/json/__init__.py +19 -0
  16. pyrsql/core/json/options.py +31 -0
  17. pyrsql/core/json/path.py +35 -0
  18. pyrsql/core/json/query.py +39 -0
  19. pyrsql/core/json/values.py +72 -0
  20. pyrsql/core/options.py +416 -0
  21. pyrsql/core/page.py +115 -0
  22. pyrsql/core/procedure_policy.py +61 -0
  23. pyrsql/core/query.py +160 -0
  24. pyrsql/core/sort.py +153 -0
  25. pyrsql/integrations/__init__.py +8 -0
  26. pyrsql/integrations/fastapi/__init__.py +8 -0
  27. pyrsql/integrations/fastapi/sqlalchemy.py +251 -0
  28. pyrsql/orms/__init__.py +15 -0
  29. pyrsql/orms/base.py +99 -0
  30. pyrsql/orms/sqlalchemy/__init__.py +39 -0
  31. pyrsql/orms/sqlalchemy/coercion.py +57 -0
  32. pyrsql/orms/sqlalchemy/compiled.py +56 -0
  33. pyrsql/orms/sqlalchemy/compiled_page.py +40 -0
  34. pyrsql/orms/sqlalchemy/compiled_sort.py +56 -0
  35. pyrsql/orms/sqlalchemy/custom.py +25 -0
  36. pyrsql/orms/sqlalchemy/errors.py +17 -0
  37. pyrsql/orms/sqlalchemy/introspection.py +107 -0
  38. pyrsql/orms/sqlalchemy/json_path.py +351 -0
  39. pyrsql/orms/sqlalchemy/orm.py +65 -0
  40. pyrsql/orms/sqlalchemy/resolver.py +202 -0
  41. pyrsql/orms/sqlalchemy/sorter.py +173 -0
  42. pyrsql/orms/sqlalchemy/statement.py +40 -0
  43. pyrsql/orms/sqlalchemy/translator.py +560 -0
  44. pyrsql/orms/sqlalchemy/type_inference.py +27 -0
  45. pyrsql/orms/sqlalchemy/types.py +64 -0
  46. pyrsql/parsing/__init__.py +32 -0
  47. pyrsql/parsing/ast.py +61 -0
  48. pyrsql/parsing/errors.py +25 -0
  49. pyrsql/parsing/lexer.py +264 -0
  50. pyrsql/parsing/limits.py +15 -0
  51. pyrsql/parsing/operators.py +201 -0
  52. pyrsql/parsing/parser.py +292 -0
  53. pyrsql/parsing/source.py +48 -0
  54. pyrsql/parsing/tokens.py +30 -0
  55. pyrsql/py.typed +0 -0
  56. pyrsql/selector/__init__.py +30 -0
  57. pyrsql/selector/analyzer.py +58 -0
  58. pyrsql/selector/ast.py +31 -0
  59. pyrsql/selector/parser.py +130 -0
  60. pyrsql/selector/semantic.py +34 -0
  61. pyrsql/semantic/__init__.py +23 -0
  62. pyrsql/semantic/analyzer.py +116 -0
  63. pyrsql/semantic/ast.py +35 -0
  64. pyrsql/semantic/errors.py +40 -0
  65. pyrsql/sorting/__init__.py +26 -0
  66. pyrsql/sorting/analyzer.py +74 -0
  67. pyrsql/sorting/ast.py +46 -0
  68. pyrsql/sorting/errors.py +21 -0
  69. pyrsql/sorting/limits.py +15 -0
  70. pyrsql/sorting/parser.py +150 -0
  71. pyrsql/sorting/semantic.py +15 -0
  72. pyrsql-0.0.1.dist-info/METADATA +72 -0
  73. pyrsql-0.0.1.dist-info/RECORD +74 -0
  74. pyrsql-0.0.1.dist-info/WHEEL +4 -0
pyrsql/parsing/ast.py ADDED
@@ -0,0 +1,61 @@
1
+ """AST node definitions for pyrsql parsing."""
2
+
3
+ import sys
4
+ from dataclasses import dataclass, replace
5
+ from enum import Enum, auto
6
+
7
+ from pyrsql.parsing.operators import ComparisonOperator
8
+ from pyrsql.parsing.source import SourceSpan
9
+ from pyrsql.selector.ast import Selector
10
+
11
+ if sys.version_info >= (3, 11):
12
+ from typing import Self
13
+ else:
14
+ from typing_extensions import Self
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class Argument:
19
+ """Represents a comparison argument."""
20
+
21
+ text: str
22
+ quoted: bool
23
+ span: SourceSpan
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class Node:
28
+ """Base AST node."""
29
+
30
+ span: SourceSpan
31
+
32
+ def with_span(self, span: SourceSpan) -> Self:
33
+ """Returns a copy of the node with an updated source span."""
34
+ return replace(self, span=span)
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class ComparisonNode(Node):
39
+ """Leaf node for selector/operator/arguments expressions."""
40
+
41
+ selector: Selector
42
+ operator: ComparisonOperator
43
+ arguments: tuple[Argument, ...]
44
+
45
+
46
+ class LogicalOperator(Enum):
47
+ """Logical operators supported in the AST."""
48
+
49
+ AND = auto()
50
+ OR = auto()
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class LogicalNode(Node):
55
+ """Logical expression with two or more child nodes."""
56
+
57
+ operator: LogicalOperator
58
+ children: tuple["Expression", ...]
59
+
60
+
61
+ Expression = ComparisonNode | LogicalNode
@@ -0,0 +1,25 @@
1
+ """Parsing error types."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pyrsql.parsing.source import SourceSpan
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ParseError(ValueError):
10
+ """Base exception for parsing failures."""
11
+
12
+ message: str
13
+ span: SourceSpan
14
+
15
+ def __str__(self) -> str:
16
+ """Formats the parse error with source location data."""
17
+ return (
18
+ f"{self.message} at index {self.span.start.index} "
19
+ f"(line {self.span.start.line}, column {self.span.start.column})"
20
+ )
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class LexError(ParseError):
25
+ """Raised when lexing fails."""
@@ -0,0 +1,264 @@
1
+ """Single-pass lexer for pyrsql query strings."""
2
+
3
+ from collections.abc import Mapping
4
+
5
+ from pyrsql.parsing.errors import LexError
6
+ from pyrsql.parsing.limits import ParseLimits
7
+ from pyrsql.parsing.operators import OPERATOR_SPELLINGS
8
+ from pyrsql.parsing.source import SourcePosition, SourceSpan, SourceText
9
+ from pyrsql.parsing.tokens import Token, TokenKind
10
+
11
+
12
+ class Lexer:
13
+ """Converts raw query text into a token stream."""
14
+
15
+ def __init__(
16
+ self,
17
+ source: str | SourceText,
18
+ *,
19
+ limits: ParseLimits | None = None,
20
+ operator_spellings: tuple[str, ...] = OPERATOR_SPELLINGS,
21
+ ) -> None:
22
+ self._source = (
23
+ source if isinstance(source, SourceText) else SourceText(source)
24
+ )
25
+ self._limits = limits or ParseLimits()
26
+ self._operator_spellings = operator_spellings
27
+ self._operator_spellings_by_prefix = self._index_operator_spellings(
28
+ operator_spellings
29
+ )
30
+ self._index = 0
31
+ self._line = 1
32
+ self._column = 1
33
+ self._validate_source_length()
34
+
35
+ def tokenize(self) -> tuple[Token, ...]:
36
+ """Tokenizes the configured source."""
37
+ tokens: list[Token] = []
38
+ while not self._is_at_end():
39
+ self._skip_whitespace()
40
+ if self._is_at_end():
41
+ break
42
+ tokens.append(self._next_token())
43
+ tokens.append(
44
+ self._make_token(
45
+ TokenKind.EOF,
46
+ "",
47
+ self._current_position(),
48
+ )
49
+ )
50
+ return tuple(tokens)
51
+
52
+ def _next_token(self) -> Token:
53
+ """Returns the next token from the stream."""
54
+ if operator := self._match_operator():
55
+ return operator
56
+
57
+ current_char = self._peek()
58
+ match current_char:
59
+ case "(":
60
+ return self._single_char_token(TokenKind.LPAREN)
61
+ case ")":
62
+ return self._single_char_token(TokenKind.RPAREN)
63
+ case ",":
64
+ return self._single_char_token(TokenKind.COMMA)
65
+ case ";":
66
+ return self._single_char_token(TokenKind.SEMICOLON)
67
+ case "'":
68
+ return self._consume_quoted_text()
69
+ case _:
70
+ return self._consume_unquoted_text()
71
+
72
+ def _consume_quoted_text(self) -> Token:
73
+ """Consumes a single-quoted text token."""
74
+ start = self._current_position()
75
+ self._advance()
76
+ characters: list[str] = []
77
+ while not self._is_at_end():
78
+ current_char = self._advance()
79
+ if current_char == "'":
80
+ lexeme = "".join(characters)
81
+ self._enforce_argument_length(lexeme, start)
82
+ return self._make_token(TokenKind.QUOTED_TEXT, lexeme, start)
83
+ if current_char == "\\" and not self._is_at_end():
84
+ escaped_char = self._advance()
85
+ characters.append(escaped_char)
86
+ continue
87
+ characters.append(current_char)
88
+ raise LexError(
89
+ message="Unterminated quoted string",
90
+ span=SourceSpan(start=start, end=self._current_position()),
91
+ )
92
+
93
+ def _consume_unquoted_text(self) -> Token:
94
+ """Consumes an unquoted text token."""
95
+ start = self._current_position()
96
+ start_index = self._index
97
+ while not self._is_at_end():
98
+ current_char = self._peek()
99
+ if current_char.isspace():
100
+ break
101
+ if current_char in "(),;":
102
+ break
103
+ if self._starts_with_operator():
104
+ break
105
+ self._advance()
106
+
107
+ lexeme = self._source.slice(start_index, self._index)
108
+ if not lexeme:
109
+ raise LexError(
110
+ message=f"Unexpected character {self._peek()!r}",
111
+ span=SourceSpan(start=start, end=self._current_position()),
112
+ )
113
+
114
+ match lexeme:
115
+ case "and":
116
+ return self._make_token(TokenKind.AND, lexeme, start)
117
+ case "or":
118
+ return self._make_token(TokenKind.OR, lexeme, start)
119
+ case _:
120
+ pass
121
+
122
+ self._enforce_selector_length(lexeme, start)
123
+ return self._make_token(TokenKind.UNQUOTED_TEXT, lexeme, start)
124
+
125
+ def _single_char_token(self, kind: TokenKind) -> Token:
126
+ """Consumes a fixed-width token."""
127
+ start = self._current_position()
128
+ lexeme = self._advance()
129
+ return self._make_token(kind, lexeme, start)
130
+
131
+ def _match_operator(self) -> Token | None:
132
+ """Consumes a comparison operator when present."""
133
+ start = self._current_position()
134
+ for spelling in self._candidate_operator_spellings():
135
+ if self._source.text.startswith(spelling, self._index):
136
+ self._advance_operator(spelling)
137
+ return self._make_token(
138
+ TokenKind.COMPARISON_OPERATOR,
139
+ spelling,
140
+ start,
141
+ )
142
+ return None
143
+
144
+ def _starts_with_operator(self) -> bool:
145
+ """Checks whether the current position begins an operator."""
146
+ for spelling in self._candidate_operator_spellings():
147
+ if self._source.text.startswith(spelling, self._index):
148
+ return True
149
+ return False
150
+
151
+ def _candidate_operator_spellings(self) -> tuple[str, ...]:
152
+ """Returns spellings matching the current prefix character."""
153
+ current_char = self._peek()
154
+ return self._operator_spellings_by_prefix.get(
155
+ current_char,
156
+ self._operator_spellings,
157
+ )
158
+
159
+ def _index_operator_spellings(
160
+ self,
161
+ operator_spellings: tuple[str, ...],
162
+ ) -> Mapping[str, tuple[str, ...]]:
163
+ """Indexes operator spellings by their first character."""
164
+ spellings_by_prefix: dict[str, list[str]] = {}
165
+ for spelling in operator_spellings:
166
+ spellings_by_prefix.setdefault(spelling[0], []).append(spelling)
167
+ return {
168
+ prefix: tuple(spellings)
169
+ for prefix, spellings in spellings_by_prefix.items()
170
+ }
171
+
172
+ def _skip_whitespace(self) -> None:
173
+ """Skips ASCII whitespace characters."""
174
+ while not self._is_at_end():
175
+ if not self._peek().isspace():
176
+ break
177
+ self._advance()
178
+
179
+ def _validate_source_length(self) -> None:
180
+ """Enforces the global query size limit."""
181
+ if self._source.length > self._limits.max_query_length:
182
+ position = SourcePosition(index=0, line=1, column=1)
183
+ raise LexError(
184
+ message=(
185
+ "Query exceeds the maximum supported length of "
186
+ f"{self._limits.max_query_length} characters"
187
+ ),
188
+ span=SourceSpan(start=position, end=position),
189
+ )
190
+
191
+ def _enforce_selector_length(
192
+ self,
193
+ lexeme: str,
194
+ position: SourcePosition,
195
+ ) -> None:
196
+ """Enforces the unquoted text length limit."""
197
+ if len(lexeme) > self._limits.max_selector_length:
198
+ raise LexError(
199
+ message=(
200
+ "Unquoted token exceeds the maximum supported length of "
201
+ f"{self._limits.max_selector_length} characters"
202
+ ),
203
+ span=SourceSpan(start=position, end=self._current_position()),
204
+ )
205
+
206
+ def _enforce_argument_length(
207
+ self,
208
+ lexeme: str,
209
+ position: SourcePosition,
210
+ ) -> None:
211
+ """Enforces the quoted argument length limit."""
212
+ if len(lexeme) > self._limits.max_argument_length:
213
+ raise LexError(
214
+ message=(
215
+ "Quoted token exceeds the maximum supported length of "
216
+ f"{self._limits.max_argument_length} characters"
217
+ ),
218
+ span=SourceSpan(start=position, end=self._current_position()),
219
+ )
220
+
221
+ def _advance(self) -> str:
222
+ """Consumes the current character and updates source position."""
223
+ character = self._source.text[self._index]
224
+ self._index += 1
225
+ if character == "\n":
226
+ self._line += 1
227
+ self._column = 1
228
+ else:
229
+ self._column += 1
230
+ return character
231
+
232
+ def _advance_operator(self, operator: str) -> None:
233
+ """Consumes a comparison operator in one step."""
234
+ self._index += len(operator)
235
+ self._column += len(operator)
236
+
237
+ def _peek(self) -> str:
238
+ """Returns the current character without consuming it."""
239
+ return self._source.text[self._index]
240
+
241
+ def _current_position(self) -> SourcePosition:
242
+ """Returns the current source position."""
243
+ return SourcePosition(
244
+ index=self._index,
245
+ line=self._line,
246
+ column=self._column,
247
+ )
248
+
249
+ def _make_token(
250
+ self,
251
+ kind: TokenKind,
252
+ lexeme: str,
253
+ start: SourcePosition,
254
+ ) -> Token:
255
+ """Builds a token from a start position and current cursor."""
256
+ return Token(
257
+ kind=kind,
258
+ lexeme=lexeme,
259
+ span=SourceSpan(start=start, end=self._current_position()),
260
+ )
261
+
262
+ def _is_at_end(self) -> bool:
263
+ """Checks whether the lexer reached the end of the source."""
264
+ return self._index >= self._source.length
@@ -0,0 +1,15 @@
1
+ """Safety limits for lexing and parsing."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class ParseLimits:
8
+ """Configurable safety limits for query processing."""
9
+
10
+ max_query_length: int = 4096
11
+ max_selector_length: int = 256
12
+ max_argument_length: int = 1024
13
+ max_arguments_per_list: int = 100
14
+ max_expression_depth: int = 16
15
+ max_node_count: int = 1024
@@ -0,0 +1,201 @@
1
+ """Supported comparison operators."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field
5
+ from types import MappingProxyType
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ComparisonOperator:
10
+ """Represents a supported comparison operator."""
11
+
12
+ name: str
13
+ spellings: tuple[str, ...]
14
+ minimum_arguments: int
15
+ maximum_arguments: int | None
16
+
17
+ def __post_init__(self) -> None:
18
+ """Validates operator invariants."""
19
+ if not self.name:
20
+ raise ValueError("Operator name cannot be empty.")
21
+ if not self.spellings:
22
+ raise ValueError("Operator must define at least one spelling.")
23
+ if self.minimum_arguments < 0:
24
+ raise ValueError("minimum_arguments cannot be negative.")
25
+ if (
26
+ self.maximum_arguments is not None
27
+ and self.maximum_arguments < self.minimum_arguments
28
+ ):
29
+ raise ValueError(
30
+ "maximum_arguments cannot be less than minimum_arguments."
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class OperatorRegistry:
36
+ """Immutable registry of supported comparison operators."""
37
+
38
+ operators: tuple[ComparisonOperator, ...] = field(default_factory=tuple)
39
+ operators_by_spelling: Mapping[str, ComparisonOperator] = field(init=False)
40
+ operator_spellings: tuple[str, ...] = field(init=False)
41
+
42
+ def __post_init__(self) -> None:
43
+ """Builds lookup structures and validates uniqueness."""
44
+ operators_by_spelling: dict[str, ComparisonOperator] = {}
45
+ for operator in self.operators:
46
+ for spelling in operator.spellings:
47
+ if spelling in operators_by_spelling:
48
+ raise ValueError(
49
+ f"Duplicate operator spelling registered: {spelling!r}."
50
+ )
51
+ operators_by_spelling[spelling] = operator
52
+ object.__setattr__(
53
+ self,
54
+ "operators_by_spelling",
55
+ MappingProxyType(operators_by_spelling),
56
+ )
57
+ object.__setattr__(
58
+ self,
59
+ "operator_spellings",
60
+ tuple(
61
+ sorted(
62
+ operators_by_spelling,
63
+ key=len,
64
+ reverse=True,
65
+ )
66
+ ),
67
+ )
68
+
69
+ def get(self, spelling: str) -> ComparisonOperator:
70
+ """Returns the operator registered for the provided spelling."""
71
+ return self.operators_by_spelling[spelling]
72
+
73
+
74
+ EQUAL = ComparisonOperator(
75
+ name="equal",
76
+ spellings=("==",),
77
+ minimum_arguments=1,
78
+ maximum_arguments=1,
79
+ )
80
+ NOT_EQUAL = ComparisonOperator(
81
+ name="not_equal",
82
+ spellings=("!=",),
83
+ minimum_arguments=1,
84
+ maximum_arguments=1,
85
+ )
86
+ GREATER_THAN = ComparisonOperator(
87
+ name="greater_than",
88
+ spellings=("=gt=", ">"),
89
+ minimum_arguments=1,
90
+ maximum_arguments=1,
91
+ )
92
+ GREATER_THAN_OR_EQUAL = ComparisonOperator(
93
+ name="greater_than_or_equal",
94
+ spellings=("=ge=", ">="),
95
+ minimum_arguments=1,
96
+ maximum_arguments=1,
97
+ )
98
+ LESS_THAN = ComparisonOperator(
99
+ name="less_than",
100
+ spellings=("=lt=", "<"),
101
+ minimum_arguments=1,
102
+ maximum_arguments=1,
103
+ )
104
+ LESS_THAN_OR_EQUAL = ComparisonOperator(
105
+ name="less_than_or_equal",
106
+ spellings=("=le=", "<="),
107
+ minimum_arguments=1,
108
+ maximum_arguments=1,
109
+ )
110
+ IN = ComparisonOperator(
111
+ name="in",
112
+ spellings=("=in=",),
113
+ minimum_arguments=1,
114
+ maximum_arguments=None,
115
+ )
116
+ NOT_IN = ComparisonOperator(
117
+ name="not_in",
118
+ spellings=("=out=",),
119
+ minimum_arguments=1,
120
+ maximum_arguments=None,
121
+ )
122
+ IS_NULL = ComparisonOperator(
123
+ name="is_null",
124
+ spellings=("=na=", "=isnull=", "=null="),
125
+ minimum_arguments=0,
126
+ maximum_arguments=1,
127
+ )
128
+ NOT_NULL = ComparisonOperator(
129
+ name="not_null",
130
+ spellings=("=nn=", "=notnull=", "=isnotnull="),
131
+ minimum_arguments=0,
132
+ maximum_arguments=1,
133
+ )
134
+ LIKE = ComparisonOperator(
135
+ name="like",
136
+ spellings=("=like=", "=ke="),
137
+ minimum_arguments=1,
138
+ maximum_arguments=1,
139
+ )
140
+ NOT_LIKE = ComparisonOperator(
141
+ name="not_like",
142
+ spellings=("=notlike=", "=nk="),
143
+ minimum_arguments=1,
144
+ maximum_arguments=1,
145
+ )
146
+ IGNORE_CASE = ComparisonOperator(
147
+ name="ignore_case",
148
+ spellings=("=ic=", "=icase="),
149
+ minimum_arguments=1,
150
+ maximum_arguments=1,
151
+ )
152
+ IGNORE_CASE_LIKE = ComparisonOperator(
153
+ name="ignore_case_like",
154
+ spellings=("=ilike=", "=ik="),
155
+ minimum_arguments=1,
156
+ maximum_arguments=1,
157
+ )
158
+ IGNORE_CASE_NOT_LIKE = ComparisonOperator(
159
+ name="ignore_case_not_like",
160
+ spellings=("=inotlike=", "=ni="),
161
+ minimum_arguments=1,
162
+ maximum_arguments=1,
163
+ )
164
+ BETWEEN = ComparisonOperator(
165
+ name="between",
166
+ spellings=("=bt=", "=between="),
167
+ minimum_arguments=2,
168
+ maximum_arguments=2,
169
+ )
170
+ NOT_BETWEEN = ComparisonOperator(
171
+ name="not_between",
172
+ spellings=("=nb=", "=notbetween="),
173
+ minimum_arguments=2,
174
+ maximum_arguments=2,
175
+ )
176
+
177
+ SUPPORTED_COMPARISON_OPERATORS = (
178
+ EQUAL,
179
+ NOT_EQUAL,
180
+ GREATER_THAN,
181
+ GREATER_THAN_OR_EQUAL,
182
+ LESS_THAN,
183
+ LESS_THAN_OR_EQUAL,
184
+ IN,
185
+ NOT_IN,
186
+ IS_NULL,
187
+ NOT_NULL,
188
+ LIKE,
189
+ NOT_LIKE,
190
+ IGNORE_CASE,
191
+ IGNORE_CASE_LIKE,
192
+ IGNORE_CASE_NOT_LIKE,
193
+ BETWEEN,
194
+ NOT_BETWEEN,
195
+ )
196
+
197
+ DEFAULT_OPERATOR_REGISTRY = OperatorRegistry(
198
+ operators=SUPPORTED_COMPARISON_OPERATORS
199
+ )
200
+ OPERATOR_SPELLINGS = DEFAULT_OPERATOR_REGISTRY.operator_spellings
201
+ OPERATORS_BY_SPELLING = DEFAULT_OPERATOR_REGISTRY.operators_by_spelling