sqlrules 0.1.0__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.
- sqlrules/__init__.py +37 -0
- sqlrules/columns.py +76 -0
- sqlrules/compiler.py +82 -0
- sqlrules/constraints.py +256 -0
- sqlrules/errors.py +97 -0
- sqlrules/inspectors.py +54 -0
- sqlrules/ir.py +28 -0
- sqlrules/py.typed +0 -0
- sqlrules/translators.py +150 -0
- sqlrules-0.1.0.dist-info/METADATA +127 -0
- sqlrules-0.1.0.dist-info/RECORD +13 -0
- sqlrules-0.1.0.dist-info/WHEEL +4 -0
- sqlrules-0.1.0.dist-info/licenses/LICENSE +21 -0
sqlrules/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from sqlrules.compiler import Compiler, compile, flatten, where
|
|
2
|
+
from sqlrules.errors import (
|
|
3
|
+
ConfigurationError,
|
|
4
|
+
InternalCompilerError,
|
|
5
|
+
InvalidModelError,
|
|
6
|
+
InvalidTranslatorError,
|
|
7
|
+
MissingColumnError,
|
|
8
|
+
RegistryError,
|
|
9
|
+
SQLRulesError,
|
|
10
|
+
TranslatorError,
|
|
11
|
+
UnsupportedConstraintError,
|
|
12
|
+
)
|
|
13
|
+
from sqlrules.ir import CompilationContext, Constraint, FieldDescriptor
|
|
14
|
+
from sqlrules.translators import SQLRulesWarning
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CompilationContext",
|
|
20
|
+
"Compiler",
|
|
21
|
+
"ConfigurationError",
|
|
22
|
+
"Constraint",
|
|
23
|
+
"FieldDescriptor",
|
|
24
|
+
"InternalCompilerError",
|
|
25
|
+
"InvalidModelError",
|
|
26
|
+
"InvalidTranslatorError",
|
|
27
|
+
"MissingColumnError",
|
|
28
|
+
"RegistryError",
|
|
29
|
+
"SQLRulesError",
|
|
30
|
+
"SQLRulesWarning",
|
|
31
|
+
"TranslatorError",
|
|
32
|
+
"UnsupportedConstraintError",
|
|
33
|
+
"__version__",
|
|
34
|
+
"compile",
|
|
35
|
+
"flatten",
|
|
36
|
+
"where",
|
|
37
|
+
]
|
sqlrules/columns.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
7
|
+
|
|
8
|
+
from sqlrules.errors import MissingColumnError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _as_column_element(value: Any) -> ColumnElement[Any] | None:
|
|
12
|
+
"""Return a ColumnElement if *value* is (or unwraps to) one."""
|
|
13
|
+
if isinstance(value, ColumnElement):
|
|
14
|
+
return value
|
|
15
|
+
clause_element = getattr(value, "__clause_element__", None)
|
|
16
|
+
if callable(clause_element):
|
|
17
|
+
try:
|
|
18
|
+
unwrapped = clause_element()
|
|
19
|
+
except Exception: # pragma: no cover - defensive
|
|
20
|
+
return None
|
|
21
|
+
if isinstance(unwrapped, ColumnElement):
|
|
22
|
+
return unwrapped
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _candidates(field_name: str, aliases: Sequence[str] | None) -> list[str]:
|
|
27
|
+
"""Build lookup names: aliases first (when set), then the field name."""
|
|
28
|
+
ordered: list[str] = []
|
|
29
|
+
if aliases:
|
|
30
|
+
for alias in aliases:
|
|
31
|
+
if alias and alias not in ordered:
|
|
32
|
+
ordered.append(alias)
|
|
33
|
+
if field_name not in ordered:
|
|
34
|
+
ordered.append(field_name)
|
|
35
|
+
return ordered
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_column(
|
|
39
|
+
field_name: str,
|
|
40
|
+
table: Any,
|
|
41
|
+
column_map: Mapping[str, ColumnElement[Any]] | None = None,
|
|
42
|
+
*,
|
|
43
|
+
alias: str | None = None,
|
|
44
|
+
aliases: Sequence[str] | None = None,
|
|
45
|
+
) -> ColumnElement[Any]:
|
|
46
|
+
alias_list: list[str] = []
|
|
47
|
+
if aliases:
|
|
48
|
+
alias_list.extend(aliases)
|
|
49
|
+
elif alias:
|
|
50
|
+
alias_list.append(alias)
|
|
51
|
+
|
|
52
|
+
candidates = _candidates(field_name, alias_list)
|
|
53
|
+
|
|
54
|
+
if column_map:
|
|
55
|
+
for candidate in candidates:
|
|
56
|
+
if candidate in column_map:
|
|
57
|
+
column = _as_column_element(column_map[candidate])
|
|
58
|
+
if column is not None:
|
|
59
|
+
return column
|
|
60
|
+
|
|
61
|
+
columns = getattr(table, "c", None)
|
|
62
|
+
if columns is not None:
|
|
63
|
+
for candidate in candidates:
|
|
64
|
+
if candidate in columns:
|
|
65
|
+
column = _as_column_element(columns[candidate])
|
|
66
|
+
if column is not None:
|
|
67
|
+
return column
|
|
68
|
+
|
|
69
|
+
# ORM mapped classes expose columns as attributes; only accept clause elements.
|
|
70
|
+
for candidate in candidates:
|
|
71
|
+
attr = getattr(table, candidate, None)
|
|
72
|
+
column = _as_column_element(attr)
|
|
73
|
+
if column is not None:
|
|
74
|
+
return column
|
|
75
|
+
|
|
76
|
+
raise MissingColumnError(field=field_name)
|
sqlrules/compiler.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from itertools import chain
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
9
|
+
|
|
10
|
+
from sqlrules.columns import resolve_column
|
|
11
|
+
from sqlrules.constraints import ensure_supported_type, extract_constraints
|
|
12
|
+
from sqlrules.errors import ConfigurationError
|
|
13
|
+
from sqlrules.inspectors import inspect_model
|
|
14
|
+
from sqlrules.ir import CompilationContext, OnUnsupported
|
|
15
|
+
from sqlrules.translators import TranslatorRegistry, default_registry
|
|
16
|
+
|
|
17
|
+
RulesDict = dict[str, list[ColumnElement[bool]]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Compiler:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
registry: TranslatorRegistry | None = None,
|
|
25
|
+
on_unsupported: OnUnsupported = "raise",
|
|
26
|
+
) -> None:
|
|
27
|
+
if on_unsupported not in {"raise", "warn", "ignore"}:
|
|
28
|
+
raise ConfigurationError(option="on_unsupported", value=on_unsupported)
|
|
29
|
+
self.registry = registry or default_registry()
|
|
30
|
+
self.context = CompilationContext(on_unsupported=on_unsupported)
|
|
31
|
+
|
|
32
|
+
def compile(
|
|
33
|
+
self,
|
|
34
|
+
model: type[BaseModel],
|
|
35
|
+
table: Any,
|
|
36
|
+
*,
|
|
37
|
+
column_map: Mapping[str, ColumnElement[Any]] | None = None,
|
|
38
|
+
) -> RulesDict:
|
|
39
|
+
rules: RulesDict = {}
|
|
40
|
+
|
|
41
|
+
for field in inspect_model(model):
|
|
42
|
+
ensure_supported_type(field)
|
|
43
|
+
constraints = extract_constraints(field)
|
|
44
|
+
if not constraints:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
column = resolve_column(
|
|
48
|
+
field.name,
|
|
49
|
+
table,
|
|
50
|
+
column_map,
|
|
51
|
+
alias=field.alias,
|
|
52
|
+
aliases=field.aliases,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expressions: list[ColumnElement[bool]] = []
|
|
56
|
+
for constraint in constraints:
|
|
57
|
+
expression = self.registry.translate(constraint, column, self.context)
|
|
58
|
+
if expression is not None:
|
|
59
|
+
expressions.append(expression)
|
|
60
|
+
|
|
61
|
+
if expressions:
|
|
62
|
+
rules[field.name] = expressions
|
|
63
|
+
|
|
64
|
+
return rules
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compile(
|
|
68
|
+
model: type[BaseModel],
|
|
69
|
+
table: Any,
|
|
70
|
+
*,
|
|
71
|
+
column_map: Mapping[str, ColumnElement[Any]] | None = None,
|
|
72
|
+
on_unsupported: OnUnsupported = "raise",
|
|
73
|
+
) -> RulesDict:
|
|
74
|
+
return Compiler(on_unsupported=on_unsupported).compile(model, table, column_map=column_map)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def flatten(rules: RulesDict) -> list[ColumnElement[bool]]:
|
|
78
|
+
return list(chain.from_iterable(rules.values()))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def where(rules: RulesDict) -> list[ColumnElement[bool]]:
|
|
82
|
+
return flatten(rules)
|
sqlrules/constraints.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime, time, timedelta
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from types import UnionType
|
|
7
|
+
from typing import Annotated, Any, Literal, Union, get_args, get_origin
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from annotated_types import (
|
|
11
|
+
Ge,
|
|
12
|
+
GroupedMetadata,
|
|
13
|
+
Gt,
|
|
14
|
+
Le,
|
|
15
|
+
Lt,
|
|
16
|
+
MaxLen,
|
|
17
|
+
MinLen,
|
|
18
|
+
MultipleOf,
|
|
19
|
+
Predicate,
|
|
20
|
+
)
|
|
21
|
+
from pydantic.fields import FieldInfo
|
|
22
|
+
|
|
23
|
+
from sqlrules.errors import UnsupportedConstraintError
|
|
24
|
+
from sqlrules.ir import Constraint, FieldDescriptor
|
|
25
|
+
|
|
26
|
+
_SUPPORTED_TYPES: frozenset[type[Any]] = frozenset({bool, int, float, Decimal, str, date, datetime})
|
|
27
|
+
_UNSUPPORTED_TYPES: frozenset[type[Any]] = frozenset({UUID, time, timedelta})
|
|
28
|
+
_UNSUPPORTED_ORIGINS: frozenset[Any] = frozenset({list, dict, tuple, set, frozenset})
|
|
29
|
+
_LENGTH_OPERATORS: frozenset[str] = frozenset({"min_length", "max_length"})
|
|
30
|
+
_NUMERIC_OPERATORS: frozenset[str] = frozenset({"gt", "ge", "lt", "le", "multiple_of"})
|
|
31
|
+
_VALIDATOR_TYPE_NAMES = frozenset(
|
|
32
|
+
{
|
|
33
|
+
"AfterValidator",
|
|
34
|
+
"BeforeValidator",
|
|
35
|
+
"PlainValidator",
|
|
36
|
+
"WrapValidator",
|
|
37
|
+
"FieldValidator",
|
|
38
|
+
"Strict",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _unwrap_annotation(annotation: Any) -> tuple[Any, tuple[Any, ...]]:
|
|
44
|
+
"""Unwrap Optional/Union[..., None] and Annotated layers.
|
|
45
|
+
|
|
46
|
+
Returns the concrete annotation and any metadata collected from Annotated
|
|
47
|
+
wrappers (used when Pydantic leaves field.metadata empty for
|
|
48
|
+
``Annotated[T, ...] | None``).
|
|
49
|
+
"""
|
|
50
|
+
collected: list[Any] = []
|
|
51
|
+
current = annotation
|
|
52
|
+
|
|
53
|
+
while True:
|
|
54
|
+
origin = get_origin(current)
|
|
55
|
+
if origin is Annotated:
|
|
56
|
+
args = get_args(current)
|
|
57
|
+
if not args:
|
|
58
|
+
break
|
|
59
|
+
current = args[0]
|
|
60
|
+
collected.extend(args[1:])
|
|
61
|
+
continue
|
|
62
|
+
if origin is Union or origin is UnionType:
|
|
63
|
+
non_none = [arg for arg in get_args(current) if arg is not type(None)]
|
|
64
|
+
if len(non_none) == 1:
|
|
65
|
+
current = non_none[0]
|
|
66
|
+
continue
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
return current, tuple(collected)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _iter_metadata(metadata: Any) -> Any:
|
|
73
|
+
for item in metadata:
|
|
74
|
+
if item is None:
|
|
75
|
+
continue
|
|
76
|
+
if isinstance(item, GroupedMetadata):
|
|
77
|
+
yield from _iter_metadata(item)
|
|
78
|
+
elif isinstance(item, FieldInfo):
|
|
79
|
+
# Annotated[..., Field(...)] | None leaves FieldInfo in the annotation
|
|
80
|
+
# metadata when field.metadata is empty.
|
|
81
|
+
yield from _iter_metadata(item.metadata)
|
|
82
|
+
else:
|
|
83
|
+
yield item
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _field_metadata(field: FieldDescriptor) -> tuple[Any, ...]:
|
|
87
|
+
if field.metadata:
|
|
88
|
+
return field.metadata
|
|
89
|
+
_, annotated_metadata = _unwrap_annotation(field.annotation)
|
|
90
|
+
return annotated_metadata
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _concrete_type(field: FieldDescriptor) -> Any:
|
|
94
|
+
annotation, _ = _unwrap_annotation(field.annotation)
|
|
95
|
+
return annotation
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def ensure_supported_type(field: FieldDescriptor) -> None:
|
|
99
|
+
"""Raise when a field annotation is outside the v0.1 support matrix."""
|
|
100
|
+
annotation = _concrete_type(field)
|
|
101
|
+
origin = get_origin(annotation)
|
|
102
|
+
|
|
103
|
+
if origin is Literal:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if origin in _UNSUPPORTED_ORIGINS:
|
|
107
|
+
raise UnsupportedConstraintError(
|
|
108
|
+
field=field.name,
|
|
109
|
+
operator=getattr(origin, "__name__", str(origin)),
|
|
110
|
+
value=annotation,
|
|
111
|
+
suggestion=(
|
|
112
|
+
"Remove the field or wait for a future SQLRules release with container support."
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if annotation in _SUPPORTED_TYPES:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if annotation in _UNSUPPORTED_TYPES or (
|
|
123
|
+
isinstance(annotation, type) and annotation in _UNSUPPORTED_TYPES
|
|
124
|
+
):
|
|
125
|
+
type_name = getattr(annotation, "__name__", str(annotation))
|
|
126
|
+
raise UnsupportedConstraintError(
|
|
127
|
+
field=field.name,
|
|
128
|
+
operator=type_name,
|
|
129
|
+
value=annotation,
|
|
130
|
+
suggestion=f"Type {type_name!r} is not supported in SQLRules 0.1.",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Unions with multiple non-None members and other unknown annotations.
|
|
134
|
+
type_name = getattr(annotation, "__name__", repr(annotation))
|
|
135
|
+
raise UnsupportedConstraintError(
|
|
136
|
+
field=field.name,
|
|
137
|
+
operator="type",
|
|
138
|
+
value=annotation,
|
|
139
|
+
suggestion=f"Annotation {type_name} is outside the SQLRules 0.1 type support matrix.",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _unsupported_constraints(field_name: str, item: Any) -> list[Constraint]:
|
|
144
|
+
if isinstance(item, Predicate):
|
|
145
|
+
return [Constraint(field_name, "predicate", item.func)]
|
|
146
|
+
|
|
147
|
+
type_name = type(item).__name__
|
|
148
|
+
if type_name in _VALIDATOR_TYPE_NAMES:
|
|
149
|
+
return [
|
|
150
|
+
Constraint(
|
|
151
|
+
field_name,
|
|
152
|
+
type_name,
|
|
153
|
+
getattr(item, "func", getattr(item, "strict", item)),
|
|
154
|
+
)
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
data = getattr(item, "__dict__", None)
|
|
158
|
+
if isinstance(data, dict) and data:
|
|
159
|
+
return [
|
|
160
|
+
Constraint(field_name, key, value) for key, value in data.items() if value is not None
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
return [Constraint(field_name, type_name, item)]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _reject_type_operator_mismatch(field: FieldDescriptor, constraint: Constraint) -> None:
|
|
167
|
+
annotation = _concrete_type(field)
|
|
168
|
+
origin = get_origin(annotation)
|
|
169
|
+
|
|
170
|
+
if origin is Literal:
|
|
171
|
+
if (
|
|
172
|
+
constraint.operator not in {"literal", "enum"}
|
|
173
|
+
and constraint.operator in _LENGTH_OPERATORS | _NUMERIC_OPERATORS
|
|
174
|
+
):
|
|
175
|
+
raise UnsupportedConstraintError(
|
|
176
|
+
field=field.name,
|
|
177
|
+
operator=constraint.operator,
|
|
178
|
+
value=constraint.value,
|
|
179
|
+
suggestion=(f"Constraint {constraint.operator!r} is not valid for Literal fields."),
|
|
180
|
+
)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
184
|
+
if constraint.operator in _LENGTH_OPERATORS | _NUMERIC_OPERATORS:
|
|
185
|
+
raise UnsupportedConstraintError(
|
|
186
|
+
field=field.name,
|
|
187
|
+
operator=constraint.operator,
|
|
188
|
+
value=constraint.value,
|
|
189
|
+
suggestion=f"Constraint {constraint.operator!r} is not valid for Enum fields.",
|
|
190
|
+
)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
if annotation is bool and constraint.operator in _LENGTH_OPERATORS | _NUMERIC_OPERATORS:
|
|
194
|
+
raise UnsupportedConstraintError(
|
|
195
|
+
field=field.name,
|
|
196
|
+
operator=constraint.operator,
|
|
197
|
+
value=constraint.value,
|
|
198
|
+
suggestion=(
|
|
199
|
+
"Bool fields only support Literal/equality-style constraints in SQLRules 0.1."
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if constraint.operator in _LENGTH_OPERATORS and annotation is not str:
|
|
204
|
+
raise UnsupportedConstraintError(
|
|
205
|
+
field=field.name,
|
|
206
|
+
operator=constraint.operator,
|
|
207
|
+
value=constraint.value,
|
|
208
|
+
suggestion="Length constraints require a str field annotation.",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if constraint.operator in _NUMERIC_OPERATORS and annotation is str:
|
|
212
|
+
raise UnsupportedConstraintError(
|
|
213
|
+
field=field.name,
|
|
214
|
+
operator=constraint.operator,
|
|
215
|
+
value=constraint.value,
|
|
216
|
+
suggestion="Numeric comparison constraints are not valid for str fields.",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def extract_constraints(field: FieldDescriptor) -> list[Constraint]:
|
|
221
|
+
constraints: list[Constraint] = []
|
|
222
|
+
|
|
223
|
+
for item in _iter_metadata(_field_metadata(field)):
|
|
224
|
+
if isinstance(item, Gt):
|
|
225
|
+
constraints.append(Constraint(field.name, "gt", item.gt))
|
|
226
|
+
elif isinstance(item, Ge):
|
|
227
|
+
constraints.append(Constraint(field.name, "ge", item.ge))
|
|
228
|
+
elif isinstance(item, Lt):
|
|
229
|
+
constraints.append(Constraint(field.name, "lt", item.lt))
|
|
230
|
+
elif isinstance(item, Le):
|
|
231
|
+
constraints.append(Constraint(field.name, "le", item.le))
|
|
232
|
+
elif isinstance(item, MultipleOf):
|
|
233
|
+
constraints.append(Constraint(field.name, "multiple_of", item.multiple_of))
|
|
234
|
+
elif isinstance(item, MinLen):
|
|
235
|
+
constraints.append(Constraint(field.name, "min_length", item.min_length))
|
|
236
|
+
elif isinstance(item, MaxLen):
|
|
237
|
+
constraints.append(Constraint(field.name, "max_length", item.max_length))
|
|
238
|
+
else:
|
|
239
|
+
constraints.extend(_unsupported_constraints(field.name, item))
|
|
240
|
+
|
|
241
|
+
annotation = _concrete_type(field)
|
|
242
|
+
origin = get_origin(annotation)
|
|
243
|
+
args = get_args(annotation)
|
|
244
|
+
|
|
245
|
+
if origin is Literal:
|
|
246
|
+
constraints.append(Constraint(field.name, "literal", args))
|
|
247
|
+
|
|
248
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
249
|
+
values = tuple(member.value for member in annotation)
|
|
250
|
+
constraints.append(Constraint(field.name, "enum", values))
|
|
251
|
+
|
|
252
|
+
for constraint in constraints:
|
|
253
|
+
if constraint.operator in _LENGTH_OPERATORS | _NUMERIC_OPERATORS:
|
|
254
|
+
_reject_type_operator_mismatch(field, constraint)
|
|
255
|
+
|
|
256
|
+
return constraints
|
sqlrules/errors.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SQLRulesError(Exception):
|
|
8
|
+
"""Base class for all public SQLRules exceptions."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class InvalidModelError(SQLRulesError):
|
|
13
|
+
model: Any
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
return (
|
|
17
|
+
f"Expected a Pydantic BaseModel subclass, got {self.model!r}. "
|
|
18
|
+
"Pass a model class such as class UserFilter(BaseModel): ..."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class MissingColumnError(SQLRulesError):
|
|
24
|
+
field: str
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return (
|
|
28
|
+
f"No SQLAlchemy column found for field {self.field!r}. "
|
|
29
|
+
"Provide a matching table column, ORM attribute, or column_map entry."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class UnsupportedConstraintError(SQLRulesError):
|
|
35
|
+
field: str
|
|
36
|
+
operator: str
|
|
37
|
+
value: Any = None
|
|
38
|
+
suggestion: str | None = None
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
message = (
|
|
42
|
+
f"Field {self.field!r}: constraint {self.operator!r} is not supported by SQLRules 0.1."
|
|
43
|
+
)
|
|
44
|
+
if self.suggestion:
|
|
45
|
+
return f"{message} {self.suggestion}"
|
|
46
|
+
return f"{message} Remove the constraint, or set on_unsupported='warn'/'ignore'."
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(slots=True)
|
|
50
|
+
class TranslatorError(SQLRulesError):
|
|
51
|
+
field: str
|
|
52
|
+
operator: str
|
|
53
|
+
message: str
|
|
54
|
+
|
|
55
|
+
def __str__(self) -> str:
|
|
56
|
+
return (
|
|
57
|
+
f"Translator failed for field {self.field!r}, "
|
|
58
|
+
f"operator {self.operator!r}: {self.message}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(slots=True)
|
|
63
|
+
class InvalidTranslatorError(SQLRulesError):
|
|
64
|
+
operator: str
|
|
65
|
+
translator: Any
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
return (
|
|
69
|
+
f"Invalid translator for operator {self.operator!r}: {self.translator!r}. "
|
|
70
|
+
"Translators must be callables returning a SQLAlchemy boolean expression."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(slots=True)
|
|
75
|
+
class RegistryError(SQLRulesError):
|
|
76
|
+
message: str
|
|
77
|
+
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
return self.message
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(slots=True)
|
|
83
|
+
class ConfigurationError(SQLRulesError):
|
|
84
|
+
option: str
|
|
85
|
+
value: Any
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
return (
|
|
89
|
+
f"Invalid configuration value for {self.option!r}: {self.value!r}. "
|
|
90
|
+
"Use one of: 'raise', 'warn', 'ignore'."
|
|
91
|
+
if self.option == "on_unsupported"
|
|
92
|
+
else f"Invalid configuration value for {self.option!r}: {self.value!r}."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class InternalCompilerError(SQLRulesError):
|
|
97
|
+
"""Unexpected internal compiler failure."""
|
sqlrules/inspectors.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import AliasChoices, AliasPath, BaseModel
|
|
6
|
+
|
|
7
|
+
from sqlrules.errors import InvalidModelError
|
|
8
|
+
from sqlrules.ir import FieldDescriptor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _string_alias(value: Any) -> str | None:
|
|
12
|
+
if isinstance(value, str):
|
|
13
|
+
return value
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _field_aliases(field: Any) -> tuple[str | None, tuple[str, ...]]:
|
|
18
|
+
"""Return primary alias and extra string alias candidates for column binding."""
|
|
19
|
+
aliases: list[str] = []
|
|
20
|
+
|
|
21
|
+
primary = _string_alias(field.alias)
|
|
22
|
+
if primary:
|
|
23
|
+
aliases.append(primary)
|
|
24
|
+
|
|
25
|
+
for attr in ("validation_alias", "serialization_alias"):
|
|
26
|
+
value = getattr(field, attr, None)
|
|
27
|
+
# AliasPath / AliasChoices are not used for column binding in 0.1.
|
|
28
|
+
if isinstance(value, (AliasPath, AliasChoices)):
|
|
29
|
+
continue
|
|
30
|
+
candidate = _string_alias(value)
|
|
31
|
+
if candidate and candidate not in aliases:
|
|
32
|
+
aliases.append(candidate)
|
|
33
|
+
|
|
34
|
+
primary_alias = aliases[0] if aliases else None
|
|
35
|
+
return primary_alias, tuple(aliases)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def inspect_model(model: type[BaseModel]) -> list[FieldDescriptor]:
|
|
39
|
+
if not isinstance(model, type) or not issubclass(model, BaseModel):
|
|
40
|
+
raise InvalidModelError(model=model)
|
|
41
|
+
|
|
42
|
+
descriptors: list[FieldDescriptor] = []
|
|
43
|
+
for name, field in model.model_fields.items():
|
|
44
|
+
primary_alias, aliases = _field_aliases(field)
|
|
45
|
+
descriptors.append(
|
|
46
|
+
FieldDescriptor(
|
|
47
|
+
name=name,
|
|
48
|
+
alias=primary_alias,
|
|
49
|
+
aliases=aliases,
|
|
50
|
+
annotation=field.annotation,
|
|
51
|
+
metadata=tuple(field.metadata),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return descriptors
|
sqlrules/ir.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
OnUnsupported = Literal["raise", "warn", "ignore"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class Constraint:
|
|
11
|
+
field: str
|
|
12
|
+
operator: str
|
|
13
|
+
value: Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class FieldDescriptor:
|
|
18
|
+
name: str
|
|
19
|
+
alias: str | None
|
|
20
|
+
annotation: Any
|
|
21
|
+
metadata: tuple[Any, ...] = ()
|
|
22
|
+
aliases: tuple[str, ...] = ()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class CompilationContext:
|
|
27
|
+
on_unsupported: OnUnsupported = "raise"
|
|
28
|
+
diagnostics: tuple[str, ...] = field(default_factory=tuple)
|
sqlrules/py.typed
ADDED
|
File without changes
|
sqlrules/translators.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import operator
|
|
4
|
+
import warnings
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import func
|
|
9
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
10
|
+
|
|
11
|
+
from sqlrules.errors import RegistryError, TranslatorError, UnsupportedConstraintError
|
|
12
|
+
from sqlrules.ir import CompilationContext, Constraint
|
|
13
|
+
|
|
14
|
+
Translator = Callable[[Constraint, ColumnElement[Any], CompilationContext], ColumnElement[bool]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SQLRulesWarning(UserWarning):
|
|
18
|
+
"""Warning emitted when an unsupported constraint is skipped."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _binary(op: Callable[[Any, Any], Any]) -> Translator:
|
|
22
|
+
def translate(
|
|
23
|
+
constraint: Constraint,
|
|
24
|
+
column: ColumnElement[Any],
|
|
25
|
+
context: CompilationContext,
|
|
26
|
+
) -> ColumnElement[bool]:
|
|
27
|
+
return cast(ColumnElement[bool], op(column, constraint.value))
|
|
28
|
+
|
|
29
|
+
return translate
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _multiple_of(
|
|
33
|
+
constraint: Constraint,
|
|
34
|
+
column: ColumnElement[Any],
|
|
35
|
+
context: CompilationContext,
|
|
36
|
+
) -> ColumnElement[bool]:
|
|
37
|
+
value = constraint.value
|
|
38
|
+
try:
|
|
39
|
+
non_positive = value <= 0
|
|
40
|
+
except TypeError as exc:
|
|
41
|
+
raise UnsupportedConstraintError(
|
|
42
|
+
field=constraint.field,
|
|
43
|
+
operator="multiple_of",
|
|
44
|
+
value=value,
|
|
45
|
+
suggestion="multiple_of requires a positive numeric value.",
|
|
46
|
+
) from exc
|
|
47
|
+
if non_positive:
|
|
48
|
+
raise UnsupportedConstraintError(
|
|
49
|
+
field=constraint.field,
|
|
50
|
+
operator="multiple_of",
|
|
51
|
+
value=value,
|
|
52
|
+
suggestion="multiple_of requires a positive numeric value.",
|
|
53
|
+
)
|
|
54
|
+
return cast(ColumnElement[bool], (column % value) == 0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _min_length(
|
|
58
|
+
constraint: Constraint,
|
|
59
|
+
column: ColumnElement[Any],
|
|
60
|
+
context: CompilationContext,
|
|
61
|
+
) -> ColumnElement[bool]:
|
|
62
|
+
return cast(ColumnElement[bool], func.length(column) >= constraint.value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _max_length(
|
|
66
|
+
constraint: Constraint,
|
|
67
|
+
column: ColumnElement[Any],
|
|
68
|
+
context: CompilationContext,
|
|
69
|
+
) -> ColumnElement[bool]:
|
|
70
|
+
return cast(ColumnElement[bool], func.length(column) <= constraint.value)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _in_values(
|
|
74
|
+
constraint: Constraint,
|
|
75
|
+
column: ColumnElement[Any],
|
|
76
|
+
context: CompilationContext,
|
|
77
|
+
) -> ColumnElement[bool]:
|
|
78
|
+
return cast(ColumnElement[bool], column.in_(constraint.value))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TranslatorRegistry:
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
self._translators: dict[str, Translator] = {}
|
|
84
|
+
|
|
85
|
+
def register(
|
|
86
|
+
self,
|
|
87
|
+
operator_name: str,
|
|
88
|
+
translator: Translator,
|
|
89
|
+
*,
|
|
90
|
+
replace: bool = False,
|
|
91
|
+
) -> None:
|
|
92
|
+
if not replace and operator_name in self._translators:
|
|
93
|
+
raise RegistryError(f"Translator for {operator_name!r} is already registered.")
|
|
94
|
+
self._translators[operator_name] = translator
|
|
95
|
+
|
|
96
|
+
def lookup(self, operator_name: str) -> Translator | None:
|
|
97
|
+
return self._translators.get(operator_name)
|
|
98
|
+
|
|
99
|
+
def translate(
|
|
100
|
+
self,
|
|
101
|
+
constraint: Constraint,
|
|
102
|
+
column: ColumnElement[Any],
|
|
103
|
+
context: CompilationContext,
|
|
104
|
+
) -> ColumnElement[bool] | None:
|
|
105
|
+
translator = self.lookup(constraint.operator)
|
|
106
|
+
if translator is None:
|
|
107
|
+
if context.on_unsupported == "raise":
|
|
108
|
+
raise UnsupportedConstraintError(
|
|
109
|
+
field=constraint.field,
|
|
110
|
+
operator=constraint.operator,
|
|
111
|
+
value=constraint.value,
|
|
112
|
+
suggestion=("Remove the constraint, or set on_unsupported='warn'/'ignore'."),
|
|
113
|
+
)
|
|
114
|
+
if context.on_unsupported == "warn":
|
|
115
|
+
# sqlrules.compile → Compiler.compile → translate → warn
|
|
116
|
+
# stacklevel=4 attributes the warning to the caller's code.
|
|
117
|
+
warnings.warn(
|
|
118
|
+
(
|
|
119
|
+
f"Field {constraint.field!r}: constraint {constraint.operator!r} "
|
|
120
|
+
"is not supported and will be skipped."
|
|
121
|
+
),
|
|
122
|
+
SQLRulesWarning,
|
|
123
|
+
stacklevel=4,
|
|
124
|
+
)
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
return translator(constraint, column, context)
|
|
129
|
+
except UnsupportedConstraintError:
|
|
130
|
+
raise
|
|
131
|
+
except Exception as exc: # pragma: no cover - defensive wrapper
|
|
132
|
+
raise TranslatorError(
|
|
133
|
+
field=constraint.field,
|
|
134
|
+
operator=constraint.operator,
|
|
135
|
+
message=str(exc),
|
|
136
|
+
) from exc
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def default_registry() -> TranslatorRegistry:
|
|
140
|
+
registry = TranslatorRegistry()
|
|
141
|
+
registry.register("gt", _binary(operator.gt))
|
|
142
|
+
registry.register("ge", _binary(operator.ge))
|
|
143
|
+
registry.register("lt", _binary(operator.lt))
|
|
144
|
+
registry.register("le", _binary(operator.le))
|
|
145
|
+
registry.register("multiple_of", _multiple_of)
|
|
146
|
+
registry.register("min_length", _min_length)
|
|
147
|
+
registry.register("max_length", _max_length)
|
|
148
|
+
registry.register("literal", _in_values)
|
|
149
|
+
registry.register("enum", _in_values)
|
|
150
|
+
return registry
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlrules
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compile constrained Pydantic models into SQLAlchemy WHERE-rule dictionaries.
|
|
5
|
+
Project-URL: Homepage, https://github.com/eddiethedean/sqlrules
|
|
6
|
+
Project-URL: Documentation, https://github.com/eddiethedean/sqlrules#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/eddiethedean/sqlrules
|
|
8
|
+
Project-URL: Changelog, https://github.com/eddiethedean/sqlrules/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/eddiethedean/sqlrules/issues
|
|
10
|
+
Author: SQLRules Contributors
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: compiler,constraints,filters,pydantic,sqlalchemy,where
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Database
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: annotated-types>=0.6
|
|
29
|
+
Requires-Dist: pydantic>=2.0
|
|
30
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
33
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# SQLRules
|
|
40
|
+
|
|
41
|
+
**Compile constrained Pydantic models into SQLAlchemy WHERE-rule dictionaries.**
|
|
42
|
+
|
|
43
|
+
One package. One job. Deterministic output. Zero database dependency.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from typing import Annotated
|
|
47
|
+
from pydantic import BaseModel, Field
|
|
48
|
+
from sqlalchemy import Table, Column, Integer, String, MetaData
|
|
49
|
+
|
|
50
|
+
import sqlrules
|
|
51
|
+
|
|
52
|
+
metadata = MetaData()
|
|
53
|
+
|
|
54
|
+
users = Table(
|
|
55
|
+
"users",
|
|
56
|
+
metadata,
|
|
57
|
+
Column("age", Integer),
|
|
58
|
+
Column("name", String),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
class UserFilter(BaseModel):
|
|
62
|
+
age: Annotated[int, Field(ge=18, le=65)]
|
|
63
|
+
name: Annotated[str, Field(min_length=2)]
|
|
64
|
+
|
|
65
|
+
rules = sqlrules.compile(UserFilter, users)
|
|
66
|
+
# {
|
|
67
|
+
# "age": [users.c.age >= 18, users.c.age <= 65],
|
|
68
|
+
# "name": [func.length(users.c.name) >= 2],
|
|
69
|
+
# }
|
|
70
|
+
|
|
71
|
+
stmt = users.select().where(*sqlrules.where(rules))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install sqlrules
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
81
|
+
|
|
82
|
+
## Supported constraints (0.1)
|
|
83
|
+
|
|
84
|
+
| Constraint | SQLAlchemy expression |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `gt` / `ge` / `lt` / `le` | `column > / >= / < / <= value` |
|
|
87
|
+
| `multiple_of` | `column % value == 0` |
|
|
88
|
+
| `min_length` / `max_length` | `func.length(column) >= / <= value` |
|
|
89
|
+
| `Literal[...]` | `column.in_(...)` |
|
|
90
|
+
| `Enum` | `column.in_(...)` |
|
|
91
|
+
|
|
92
|
+
Unsupported constraints raise `UnsupportedConstraintError` by default.
|
|
93
|
+
Use `on_unsupported="warn"` or `"ignore"` to change that policy for unknown
|
|
94
|
+
constraint operators (unsupported types always raise).
|
|
95
|
+
|
|
96
|
+
## Public API
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise")
|
|
100
|
+
sqlrules.where(rules) # flatten all expressions
|
|
101
|
+
sqlrules.flatten(rules) # alias of where()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Non-goals
|
|
105
|
+
|
|
106
|
+
SQLRules is not an ORM, validator, query builder, SQL string generator,
|
|
107
|
+
migration tool, or database client. It only compiles supported Pydantic
|
|
108
|
+
constraints into SQLAlchemy expressions.
|
|
109
|
+
|
|
110
|
+
## Documentation
|
|
111
|
+
|
|
112
|
+
See [`docs/index.md`](docs/index.md) for the full documentation set,
|
|
113
|
+
including the [spec](docs/SPEC.md), [API](docs/API.md),
|
|
114
|
+
[architecture](docs/ARCHITECTURE.md), and [roadmap](docs/ROADMAP.md).
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install -e ".[dev]"
|
|
120
|
+
pytest
|
|
121
|
+
ruff check .
|
|
122
|
+
mypy src/sqlrules
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
sqlrules/__init__.py,sha256=51kBzKMVXNdUUgWNYzKcSFJfPA3ZlX2OiFUfZ7aljss,875
|
|
2
|
+
sqlrules/columns.py,sha256=AKLzYvYkHC-jIMPXMDLj37kUUPU3hIHCDoZYhwa9064,2373
|
|
3
|
+
sqlrules/compiler.py,sha256=iIPNGq5TN2cQ21HtrG6uRaYqksDfdvKE9h5KrnjHZu4,2529
|
|
4
|
+
sqlrules/constraints.py,sha256=oSgNzzstRxyskeKddQTNo6Cbgc_tXnx8A4KYY7_a4po,8921
|
|
5
|
+
sqlrules/errors.py,sha256=5R-tj_TROF_aWBcB-zB-e2crmAri3hhEH7bZlslyN5w,2537
|
|
6
|
+
sqlrules/inspectors.py,sha256=h6TsUVyLTyNie0up3ZM1QLReYtKBlHBcwDbJxbS8gGs,1707
|
|
7
|
+
sqlrules/ir.py,sha256=gdz9u2pnVM8WTjdUCTq1MdKA3U_NV3paVwqZqG1pc8Q,623
|
|
8
|
+
sqlrules/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
sqlrules/translators.py,sha256=mKTcSPMZewqpjVRf58iGyZJlATTyYFZmklLz3qXNIjs,4955
|
|
10
|
+
sqlrules-0.1.0.dist-info/METADATA,sha256=d9QCDHGEdUA_mr_mSFReh8wGcdaGV-3-Lj1Wdk7doZo,3788
|
|
11
|
+
sqlrules-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
12
|
+
sqlrules-0.1.0.dist-info/licenses/LICENSE,sha256=cLMlkCH6RhjWAkXBWR7LazV98TOG0WkPtCKB5zqqkAs,1078
|
|
13
|
+
sqlrules-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SQLRules Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|