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.
- pyrsql/__init__.py +63 -0
- pyrsql/adapters/__init__.py +3 -0
- pyrsql/adapters/fastapi/__init__.py +15 -0
- pyrsql/adapters/fastapi/config.py +76 -0
- pyrsql/adapters/fastapi/criteria.py +56 -0
- pyrsql/adapters/fastapi/dependency.py +217 -0
- pyrsql/adapters/fastapi/errors.py +107 -0
- pyrsql/api.py +76 -0
- pyrsql/core/__init__.py +45 -0
- pyrsql/core/compiler.py +81 -0
- pyrsql/core/conversion.py +206 -0
- pyrsql/core/custom.py +14 -0
- pyrsql/core/field_policy.py +97 -0
- pyrsql/core/joins.py +11 -0
- pyrsql/core/json/__init__.py +19 -0
- pyrsql/core/json/options.py +31 -0
- pyrsql/core/json/path.py +35 -0
- pyrsql/core/json/query.py +39 -0
- pyrsql/core/json/values.py +72 -0
- pyrsql/core/options.py +416 -0
- pyrsql/core/page.py +115 -0
- pyrsql/core/procedure_policy.py +61 -0
- pyrsql/core/query.py +160 -0
- pyrsql/core/sort.py +153 -0
- pyrsql/integrations/__init__.py +8 -0
- pyrsql/integrations/fastapi/__init__.py +8 -0
- pyrsql/integrations/fastapi/sqlalchemy.py +251 -0
- pyrsql/orms/__init__.py +15 -0
- pyrsql/orms/base.py +99 -0
- pyrsql/orms/sqlalchemy/__init__.py +39 -0
- pyrsql/orms/sqlalchemy/coercion.py +57 -0
- pyrsql/orms/sqlalchemy/compiled.py +56 -0
- pyrsql/orms/sqlalchemy/compiled_page.py +40 -0
- pyrsql/orms/sqlalchemy/compiled_sort.py +56 -0
- pyrsql/orms/sqlalchemy/custom.py +25 -0
- pyrsql/orms/sqlalchemy/errors.py +17 -0
- pyrsql/orms/sqlalchemy/introspection.py +107 -0
- pyrsql/orms/sqlalchemy/json_path.py +351 -0
- pyrsql/orms/sqlalchemy/orm.py +65 -0
- pyrsql/orms/sqlalchemy/resolver.py +202 -0
- pyrsql/orms/sqlalchemy/sorter.py +173 -0
- pyrsql/orms/sqlalchemy/statement.py +40 -0
- pyrsql/orms/sqlalchemy/translator.py +560 -0
- pyrsql/orms/sqlalchemy/type_inference.py +27 -0
- pyrsql/orms/sqlalchemy/types.py +64 -0
- pyrsql/parsing/__init__.py +32 -0
- pyrsql/parsing/ast.py +61 -0
- pyrsql/parsing/errors.py +25 -0
- pyrsql/parsing/lexer.py +264 -0
- pyrsql/parsing/limits.py +15 -0
- pyrsql/parsing/operators.py +201 -0
- pyrsql/parsing/parser.py +292 -0
- pyrsql/parsing/source.py +48 -0
- pyrsql/parsing/tokens.py +30 -0
- pyrsql/py.typed +0 -0
- pyrsql/selector/__init__.py +30 -0
- pyrsql/selector/analyzer.py +58 -0
- pyrsql/selector/ast.py +31 -0
- pyrsql/selector/parser.py +130 -0
- pyrsql/selector/semantic.py +34 -0
- pyrsql/semantic/__init__.py +23 -0
- pyrsql/semantic/analyzer.py +116 -0
- pyrsql/semantic/ast.py +35 -0
- pyrsql/semantic/errors.py +40 -0
- pyrsql/sorting/__init__.py +26 -0
- pyrsql/sorting/analyzer.py +74 -0
- pyrsql/sorting/ast.py +46 -0
- pyrsql/sorting/errors.py +21 -0
- pyrsql/sorting/limits.py +15 -0
- pyrsql/sorting/parser.py +150 -0
- pyrsql/sorting/semantic.py +15 -0
- pyrsql-0.0.1.dist-info/METADATA +72 -0
- pyrsql-0.0.1.dist-info/RECORD +74 -0
- pyrsql-0.0.1.dist-info/WHEEL +4 -0
pyrsql/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Public package interface for pyrsql."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=redefined-builtin
|
|
4
|
+
|
|
5
|
+
import pyrsql.api as api
|
|
6
|
+
from pyrsql.core.compiler import (
|
|
7
|
+
CompilationResult,
|
|
8
|
+
PageCompilationResult,
|
|
9
|
+
SortCompilationResult,
|
|
10
|
+
)
|
|
11
|
+
from pyrsql.core.conversion import (
|
|
12
|
+
FieldValueConverterSet,
|
|
13
|
+
ValueConverter,
|
|
14
|
+
ValueConverterRegistry,
|
|
15
|
+
)
|
|
16
|
+
from pyrsql.core.custom import CustomPredicateDefinition
|
|
17
|
+
from pyrsql.core.field_policy import FieldPolicySet
|
|
18
|
+
from pyrsql.core.json.options import JSONOptions
|
|
19
|
+
from pyrsql.core.json.path import JSONPath
|
|
20
|
+
from pyrsql.core.json.query import JSONPathComparison
|
|
21
|
+
from pyrsql.core.json.values import JSONScalarNormalizer, JSONScalarValue
|
|
22
|
+
from pyrsql.core.options import QueryOptions, SortOptions
|
|
23
|
+
from pyrsql.core.page import PageRequest
|
|
24
|
+
from pyrsql.core.procedure_policy import ProcedureAccessPolicy
|
|
25
|
+
from pyrsql.core.query import Query
|
|
26
|
+
from pyrsql.core.sort import Sort
|
|
27
|
+
from pyrsql.integrations import (
|
|
28
|
+
FastAPISQLAlchemyIntegration,
|
|
29
|
+
SQLAlchemyPaginatedSelect,
|
|
30
|
+
)
|
|
31
|
+
from pyrsql.orms.base import ORM
|
|
32
|
+
|
|
33
|
+
parse = api.parse
|
|
34
|
+
compile = api.compile
|
|
35
|
+
apply = api.apply
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ORM",
|
|
39
|
+
"CompilationResult",
|
|
40
|
+
"CustomPredicateDefinition",
|
|
41
|
+
"FieldPolicySet",
|
|
42
|
+
"FieldValueConverterSet",
|
|
43
|
+
"FastAPISQLAlchemyIntegration",
|
|
44
|
+
"JSONOptions",
|
|
45
|
+
"JSONPath",
|
|
46
|
+
"JSONPathComparison",
|
|
47
|
+
"JSONScalarNormalizer",
|
|
48
|
+
"JSONScalarValue",
|
|
49
|
+
"PageCompilationResult",
|
|
50
|
+
"PageRequest",
|
|
51
|
+
"ProcedureAccessPolicy",
|
|
52
|
+
"Query",
|
|
53
|
+
"QueryOptions",
|
|
54
|
+
"Sort",
|
|
55
|
+
"SortCompilationResult",
|
|
56
|
+
"SortOptions",
|
|
57
|
+
"SQLAlchemyPaginatedSelect",
|
|
58
|
+
"ValueConverter",
|
|
59
|
+
"ValueConverterRegistry",
|
|
60
|
+
"apply",
|
|
61
|
+
"compile",
|
|
62
|
+
"parse",
|
|
63
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""FastAPI adapter for pyrsql request extraction and error translation."""
|
|
2
|
+
|
|
3
|
+
from pyrsql.adapters.fastapi.config import FastAPICriteriaConfig
|
|
4
|
+
from pyrsql.adapters.fastapi.criteria import RequestCriteria
|
|
5
|
+
from pyrsql.adapters.fastapi.dependency import (
|
|
6
|
+
CriteriaDependency,
|
|
7
|
+
criteria_dependency,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CriteriaDependency",
|
|
12
|
+
"FastAPICriteriaConfig",
|
|
13
|
+
"RequestCriteria",
|
|
14
|
+
"criteria_dependency",
|
|
15
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Configuration objects for the FastAPI adapter."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from pyrsql.core.options import QueryOptions, SortOptions
|
|
6
|
+
|
|
7
|
+
_DEFAULT_FILTER_PARAMETER = "filter"
|
|
8
|
+
_DEFAULT_SORT_PARAMETER = "sort"
|
|
9
|
+
_DEFAULT_PAGE_PARAMETER = "page"
|
|
10
|
+
_DEFAULT_SIZE_PARAMETER = "size"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class FastAPICriteriaConfig:
|
|
15
|
+
"""Configures FastAPI query parameter extraction for pyrsql.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
filter_parameter: Query parameter used for filtering expressions.
|
|
19
|
+
sort_parameter: Query parameter used for sort expressions.
|
|
20
|
+
page_parameter: Query parameter used for page numbers.
|
|
21
|
+
size_parameter: Query parameter used for page size.
|
|
22
|
+
default_page_size: Default size used when only a page number is given.
|
|
23
|
+
max_page_size: Maximum accepted page size.
|
|
24
|
+
one_based_paging: Whether page numbers start at 1 instead of 0.
|
|
25
|
+
query_options: Query parsing and semantic configuration.
|
|
26
|
+
sort_options: Sort parsing and semantic configuration.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
filter_parameter: str = _DEFAULT_FILTER_PARAMETER
|
|
30
|
+
sort_parameter: str = _DEFAULT_SORT_PARAMETER
|
|
31
|
+
page_parameter: str = _DEFAULT_PAGE_PARAMETER
|
|
32
|
+
size_parameter: str = _DEFAULT_SIZE_PARAMETER
|
|
33
|
+
default_page_size: int | None = None
|
|
34
|
+
max_page_size: int | None = None
|
|
35
|
+
one_based_paging: bool = False
|
|
36
|
+
query_options: QueryOptions = field(default_factory=QueryOptions)
|
|
37
|
+
sort_options: SortOptions = field(default_factory=SortOptions)
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
"""Validates adapter configuration invariants.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If parameter names are empty, duplicated, or paging
|
|
44
|
+
limits are inconsistent.
|
|
45
|
+
"""
|
|
46
|
+
parameter_names = (
|
|
47
|
+
self.filter_parameter,
|
|
48
|
+
self.sort_parameter,
|
|
49
|
+
self.page_parameter,
|
|
50
|
+
self.size_parameter,
|
|
51
|
+
)
|
|
52
|
+
normalized_names = tuple(name.strip() for name in parameter_names)
|
|
53
|
+
if any(not name for name in normalized_names):
|
|
54
|
+
raise ValueError("FastAPI parameter names must not be empty.")
|
|
55
|
+
if len(set(normalized_names)) != len(normalized_names):
|
|
56
|
+
raise ValueError("FastAPI parameter names must be unique.")
|
|
57
|
+
if self.default_page_size is not None and self.default_page_size <= 0:
|
|
58
|
+
raise ValueError("default_page_size must be greater than 0.")
|
|
59
|
+
if self.max_page_size is not None and self.max_page_size <= 0:
|
|
60
|
+
raise ValueError("max_page_size must be greater than 0.")
|
|
61
|
+
if (
|
|
62
|
+
self.default_page_size is not None
|
|
63
|
+
and self.max_page_size is not None
|
|
64
|
+
and self.default_page_size > self.max_page_size
|
|
65
|
+
):
|
|
66
|
+
raise ValueError("default_page_size must not exceed max_page_size.")
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def minimum_page_number(self) -> int:
|
|
70
|
+
"""The minimum accepted page number for the adapter."""
|
|
71
|
+
return 1 if self.one_based_paging else 0
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def default_page_number(self) -> int:
|
|
75
|
+
"""The page number used when only size is provided."""
|
|
76
|
+
return 1 if self.one_based_paging else 0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""FastAPI request criteria objects."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pyrsql.core.page import PageRequest
|
|
7
|
+
from pyrsql.core.query import Query
|
|
8
|
+
from pyrsql.core.sort import Sort
|
|
9
|
+
from pyrsql.orms.base import ORM
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class RequestCriteria:
|
|
14
|
+
"""Represents request-derived pyrsql criteria for a FastAPI endpoint."""
|
|
15
|
+
|
|
16
|
+
query: Query | None = None
|
|
17
|
+
sort: Sort | None = None
|
|
18
|
+
page_request: PageRequest | None = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def is_empty(self) -> bool:
|
|
22
|
+
"""Indicates whether no query components are present."""
|
|
23
|
+
return (
|
|
24
|
+
self.query is None
|
|
25
|
+
and self.sort is None
|
|
26
|
+
and self.page_request is None
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def apply(
|
|
30
|
+
self,
|
|
31
|
+
target: Any,
|
|
32
|
+
model: type[Any],
|
|
33
|
+
*,
|
|
34
|
+
orm: ORM,
|
|
35
|
+
) -> Any:
|
|
36
|
+
"""Applies the criteria to an ORM-specific target."""
|
|
37
|
+
current_target = target
|
|
38
|
+
if self.query is not None:
|
|
39
|
+
current_target = self.query.apply(
|
|
40
|
+
current_target,
|
|
41
|
+
model,
|
|
42
|
+
orm=orm,
|
|
43
|
+
)
|
|
44
|
+
if self.sort is not None:
|
|
45
|
+
current_target = self.sort.apply(
|
|
46
|
+
current_target,
|
|
47
|
+
model,
|
|
48
|
+
orm=orm,
|
|
49
|
+
)
|
|
50
|
+
if self.page_request is not None:
|
|
51
|
+
current_target = self.page_request.apply(
|
|
52
|
+
current_target,
|
|
53
|
+
model,
|
|
54
|
+
orm=orm,
|
|
55
|
+
)
|
|
56
|
+
return current_target
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Dependency factories for FastAPI request integration."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from inspect import Signature, signature
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from fastapi import HTTPException, Query
|
|
9
|
+
except ImportError as error: # pragma: no cover - import guard
|
|
10
|
+
raise ImportError(
|
|
11
|
+
"FastAPI support requires installing the 'fastapi' extra: "
|
|
12
|
+
"pip install pyrsql[fastapi]"
|
|
13
|
+
) from error
|
|
14
|
+
|
|
15
|
+
from pyrsql.adapters.fastapi.config import FastAPICriteriaConfig
|
|
16
|
+
from pyrsql.adapters.fastapi.criteria import RequestCriteria
|
|
17
|
+
from pyrsql.adapters.fastapi.errors import (
|
|
18
|
+
FastAPIAdapterErrorPayload,
|
|
19
|
+
build_page_error_payload,
|
|
20
|
+
build_query_error_payload,
|
|
21
|
+
build_sort_error_payload,
|
|
22
|
+
)
|
|
23
|
+
from pyrsql.core.page import PageRequest
|
|
24
|
+
from pyrsql.core.query import Query as PyrsqlQuery
|
|
25
|
+
from pyrsql.core.sort import Sort as PyrsqlSort
|
|
26
|
+
from pyrsql.parsing.errors import ParseError
|
|
27
|
+
from pyrsql.semantic.errors import SemanticError
|
|
28
|
+
from pyrsql.sorting.errors import (
|
|
29
|
+
SortFieldBlacklistedError,
|
|
30
|
+
SortFieldNotWhitelistedError,
|
|
31
|
+
SortFunctionBlacklistedError,
|
|
32
|
+
SortFunctionNotWhitelistedError,
|
|
33
|
+
SortParseError,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_QUERY_ERROR_TYPES = (ParseError, SemanticError)
|
|
37
|
+
_SORT_ERROR_TYPES = (
|
|
38
|
+
SortParseError,
|
|
39
|
+
SortFieldNotWhitelistedError,
|
|
40
|
+
SortFieldBlacklistedError,
|
|
41
|
+
SortFunctionNotWhitelistedError,
|
|
42
|
+
SortFunctionBlacklistedError,
|
|
43
|
+
)
|
|
44
|
+
_DEFAULT_FASTAPI_CRITERIA_CONFIG = FastAPICriteriaConfig()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _raise_http_error(payload: FastAPIAdapterErrorPayload) -> None:
|
|
48
|
+
"""Raises a standardized FastAPI HTTP exception for adapter failures."""
|
|
49
|
+
raise HTTPException(
|
|
50
|
+
status_code=422,
|
|
51
|
+
detail={
|
|
52
|
+
"parameter": payload.parameter,
|
|
53
|
+
"type": payload.error_type,
|
|
54
|
+
"message": payload.message,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_page_request(
|
|
60
|
+
*,
|
|
61
|
+
config: FastAPICriteriaConfig,
|
|
62
|
+
page_value: int | None,
|
|
63
|
+
size_value: int | None,
|
|
64
|
+
) -> PageRequest | None:
|
|
65
|
+
"""Builds a page request from validated FastAPI query params."""
|
|
66
|
+
resolved_page_size = size_value or config.default_page_size
|
|
67
|
+
|
|
68
|
+
if page_value is None:
|
|
69
|
+
if resolved_page_size is None:
|
|
70
|
+
return None
|
|
71
|
+
resolved_page_number = config.default_page_number
|
|
72
|
+
else:
|
|
73
|
+
resolved_page_number = page_value
|
|
74
|
+
|
|
75
|
+
if resolved_page_size is None:
|
|
76
|
+
_raise_http_error(
|
|
77
|
+
build_page_error_payload(
|
|
78
|
+
config.size_parameter,
|
|
79
|
+
error_type="page_configuration_error",
|
|
80
|
+
message=(
|
|
81
|
+
f"'{config.size_parameter}' is required when "
|
|
82
|
+
f"'{config.page_parameter}' is provided."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if resolved_page_size is None:
|
|
88
|
+
raise ValueError("resolved_page_size cannot be None")
|
|
89
|
+
|
|
90
|
+
if config.one_based_paging:
|
|
91
|
+
if resolved_page_number <= 0:
|
|
92
|
+
_raise_http_error(
|
|
93
|
+
build_page_error_payload(
|
|
94
|
+
config.page_parameter,
|
|
95
|
+
error_type="page_validation_error",
|
|
96
|
+
message=(
|
|
97
|
+
f"'{config.page_parameter}' must be greater than 0 "
|
|
98
|
+
"when one_based_paging is enabled."
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
resolved_page_number -= 1
|
|
103
|
+
|
|
104
|
+
return PageRequest.of(resolved_page_number, resolved_page_size)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _build_criteria_callable(
|
|
108
|
+
config: FastAPICriteriaConfig,
|
|
109
|
+
) -> Callable[..., RequestCriteria]:
|
|
110
|
+
"""Builds the concrete dependency callable for a fixed configuration."""
|
|
111
|
+
|
|
112
|
+
def dependency(
|
|
113
|
+
filter_value: Annotated[
|
|
114
|
+
str | None,
|
|
115
|
+
Query(alias=config.filter_parameter),
|
|
116
|
+
] = None,
|
|
117
|
+
sort_value: Annotated[
|
|
118
|
+
str | None,
|
|
119
|
+
Query(alias=config.sort_parameter),
|
|
120
|
+
] = None,
|
|
121
|
+
page_value: Annotated[
|
|
122
|
+
int | None,
|
|
123
|
+
Query(
|
|
124
|
+
alias=config.page_parameter,
|
|
125
|
+
ge=config.minimum_page_number,
|
|
126
|
+
),
|
|
127
|
+
] = None,
|
|
128
|
+
size_value: Annotated[
|
|
129
|
+
int | None,
|
|
130
|
+
Query(
|
|
131
|
+
alias=config.size_parameter,
|
|
132
|
+
gt=0,
|
|
133
|
+
le=config.max_page_size,
|
|
134
|
+
),
|
|
135
|
+
] = None,
|
|
136
|
+
) -> RequestCriteria:
|
|
137
|
+
query = None
|
|
138
|
+
sort = None
|
|
139
|
+
|
|
140
|
+
if filter_value:
|
|
141
|
+
try:
|
|
142
|
+
query = PyrsqlQuery.parse(
|
|
143
|
+
filter_value,
|
|
144
|
+
options=config.query_options,
|
|
145
|
+
)
|
|
146
|
+
except _QUERY_ERROR_TYPES as error:
|
|
147
|
+
_raise_http_error(
|
|
148
|
+
build_query_error_payload(config.filter_parameter, error)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if sort_value:
|
|
152
|
+
try:
|
|
153
|
+
sort = PyrsqlSort.parse(
|
|
154
|
+
sort_value,
|
|
155
|
+
options=config.sort_options,
|
|
156
|
+
)
|
|
157
|
+
except _SORT_ERROR_TYPES as error:
|
|
158
|
+
_raise_http_error(
|
|
159
|
+
build_sort_error_payload(config.sort_parameter, error)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
page_request = _build_page_request(
|
|
163
|
+
config=config,
|
|
164
|
+
page_value=page_value,
|
|
165
|
+
size_value=size_value,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return RequestCriteria(
|
|
169
|
+
query=query,
|
|
170
|
+
sort=sort,
|
|
171
|
+
page_request=page_request,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return dependency
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CriteriaDependency:
|
|
178
|
+
"""Callable FastAPI dependency object that resolves request criteria.
|
|
179
|
+
|
|
180
|
+
The generated signature exposes FastAPI query parameters and returns a
|
|
181
|
+
parsed RequestCriteria object.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
config: FastAPICriteriaConfig | None = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Creates a dependency object for the provided FastAPI config.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
config: Optional FastAPI criteria configuration.
|
|
192
|
+
"""
|
|
193
|
+
self.config = config or _DEFAULT_FASTAPI_CRITERIA_CONFIG
|
|
194
|
+
self._dependency = _build_criteria_callable(self.config)
|
|
195
|
+
self.__signature__: Signature = signature(self._dependency)
|
|
196
|
+
|
|
197
|
+
def __call__(self, *args: Any, **kwargs: Any) -> RequestCriteria:
|
|
198
|
+
"""Delegates FastAPI dependency resolution to the generated callable.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Parsed request criteria for the incoming FastAPI query params.
|
|
202
|
+
"""
|
|
203
|
+
return self._dependency(*args, **kwargs)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def criteria_dependency(
|
|
207
|
+
config: FastAPICriteriaConfig | None = None,
|
|
208
|
+
) -> CriteriaDependency:
|
|
209
|
+
"""Builds a FastAPI dependency object that returns parsed criteria.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
config: Optional FastAPI criteria configuration.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
A callable FastAPI dependency object.
|
|
216
|
+
"""
|
|
217
|
+
return CriteriaDependency(config)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""FastAPI adapter error helpers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pyrsql.parsing.errors import ParseError
|
|
6
|
+
from pyrsql.semantic.errors import SemanticError
|
|
7
|
+
from pyrsql.sorting.errors import (
|
|
8
|
+
SortFieldBlacklistedError,
|
|
9
|
+
SortFieldNotWhitelistedError,
|
|
10
|
+
SortFunctionBlacklistedError,
|
|
11
|
+
SortFunctionNotWhitelistedError,
|
|
12
|
+
SortParseError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class FastAPIAdapterErrorPayload:
|
|
18
|
+
"""Structured adapter error payload for HTTP translation.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
parameter: Name of the query parameter associated with the failure.
|
|
22
|
+
error_type: Stable machine-readable error category.
|
|
23
|
+
message: Human-readable error message.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
parameter: str
|
|
27
|
+
error_type: str
|
|
28
|
+
message: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_query_error_payload(
|
|
32
|
+
parameter_name: str,
|
|
33
|
+
error: ParseError | SemanticError,
|
|
34
|
+
) -> FastAPIAdapterErrorPayload:
|
|
35
|
+
"""Builds a payload for a filter/query parsing or semantic failure.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
parameter_name: Query parameter name associated with the failure.
|
|
39
|
+
error: Parsing or semantic error raised by pyrsql.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A normalized FastAPI error payload.
|
|
43
|
+
"""
|
|
44
|
+
error_type = "query_parse_error"
|
|
45
|
+
if isinstance(error, SemanticError):
|
|
46
|
+
error_type = "query_semantic_error"
|
|
47
|
+
return FastAPIAdapterErrorPayload(
|
|
48
|
+
parameter=parameter_name,
|
|
49
|
+
error_type=error_type,
|
|
50
|
+
message=str(error),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_sort_error_payload(
|
|
55
|
+
parameter_name: str,
|
|
56
|
+
error: Exception,
|
|
57
|
+
) -> FastAPIAdapterErrorPayload:
|
|
58
|
+
"""Builds a payload for a sort parsing or semantic failure.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
parameter_name: Query parameter name associated with the failure.
|
|
62
|
+
error: Sorting error raised by pyrsql.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A normalized FastAPI error payload.
|
|
66
|
+
"""
|
|
67
|
+
error_type = "sort_error"
|
|
68
|
+
if isinstance(error, SortParseError):
|
|
69
|
+
error_type = "sort_parse_error"
|
|
70
|
+
elif isinstance(
|
|
71
|
+
error,
|
|
72
|
+
(
|
|
73
|
+
SortFieldNotWhitelistedError,
|
|
74
|
+
SortFieldBlacklistedError,
|
|
75
|
+
SortFunctionNotWhitelistedError,
|
|
76
|
+
SortFunctionBlacklistedError,
|
|
77
|
+
),
|
|
78
|
+
):
|
|
79
|
+
error_type = "sort_semantic_error"
|
|
80
|
+
return FastAPIAdapterErrorPayload(
|
|
81
|
+
parameter=parameter_name,
|
|
82
|
+
error_type=error_type,
|
|
83
|
+
message=str(error),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_page_error_payload(
|
|
88
|
+
parameter_name: str,
|
|
89
|
+
*,
|
|
90
|
+
error_type: str,
|
|
91
|
+
message: str,
|
|
92
|
+
) -> FastAPIAdapterErrorPayload:
|
|
93
|
+
"""Builds a payload for page-related adapter validation failures.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
parameter_name: Query parameter name associated with the failure.
|
|
97
|
+
error_type: Machine-readable error category.
|
|
98
|
+
message: Human-readable error message.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A normalized FastAPI error payload.
|
|
102
|
+
"""
|
|
103
|
+
return FastAPIAdapterErrorPayload(
|
|
104
|
+
parameter=parameter_name,
|
|
105
|
+
error_type=error_type,
|
|
106
|
+
message=message,
|
|
107
|
+
)
|
pyrsql/api.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""ORM-neutral high-level API for pyrsql."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=redefined-builtin
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pyrsql.core.compiler import CompilationResult
|
|
8
|
+
from pyrsql.core.options import QueryOptions
|
|
9
|
+
from pyrsql.core.query import Query
|
|
10
|
+
from pyrsql.orms.base import ORM
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse(query_text: str, *, options: QueryOptions | None = None) -> Query:
|
|
14
|
+
"""Parses raw RSQL text into a query object.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
query_text: Raw RSQL text to parse.
|
|
18
|
+
options: Optional query configuration.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
A parsed query object.
|
|
22
|
+
"""
|
|
23
|
+
return Query.parse(query_text, options=options)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def compile(
|
|
27
|
+
query_text: str,
|
|
28
|
+
*,
|
|
29
|
+
orm: ORM,
|
|
30
|
+
options: QueryOptions | None = None,
|
|
31
|
+
) -> CompilationResult:
|
|
32
|
+
"""Compiles raw RSQL text using the provided ORM.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
query_text: Raw RSQL text to compile.
|
|
36
|
+
orm: ORM adapter used to compile the query.
|
|
37
|
+
options: Optional query configuration.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The ORM-specific compilation result.
|
|
41
|
+
"""
|
|
42
|
+
return parse(query_text, options=options).compile(orm=orm)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def apply(
|
|
46
|
+
target: Any,
|
|
47
|
+
model: type[Any],
|
|
48
|
+
query_text: str,
|
|
49
|
+
*,
|
|
50
|
+
orm: ORM,
|
|
51
|
+
options: QueryOptions | None = None,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Applies raw RSQL text to an ORM-specific target.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
target: ORM-specific target to mutate.
|
|
57
|
+
model: ORM model class used to resolve the query.
|
|
58
|
+
query_text: Raw RSQL text to apply.
|
|
59
|
+
orm: ORM adapter used to compile the query.
|
|
60
|
+
options: Optional query configuration.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The value returned by the ORM-specific apply operation.
|
|
64
|
+
"""
|
|
65
|
+
return compile(
|
|
66
|
+
query_text,
|
|
67
|
+
orm=orm,
|
|
68
|
+
options=options,
|
|
69
|
+
).apply(target=target, model=model)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"apply",
|
|
74
|
+
"compile",
|
|
75
|
+
"parse",
|
|
76
|
+
]
|
pyrsql/core/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Core ORM-agnostic pyrsql types."""
|
|
2
|
+
|
|
3
|
+
from pyrsql.core.compiler import (
|
|
4
|
+
CompilationResult,
|
|
5
|
+
PageCompilationResult,
|
|
6
|
+
SortCompilationResult,
|
|
7
|
+
)
|
|
8
|
+
from pyrsql.core.conversion import (
|
|
9
|
+
FieldValueConverterSet,
|
|
10
|
+
ValueConverter,
|
|
11
|
+
ValueConverterRegistry,
|
|
12
|
+
)
|
|
13
|
+
from pyrsql.core.custom import CustomPredicateDefinition
|
|
14
|
+
from pyrsql.core.field_policy import FieldPolicySet
|
|
15
|
+
from pyrsql.core.json.options import JSONOptions
|
|
16
|
+
from pyrsql.core.json.path import JSONPath
|
|
17
|
+
from pyrsql.core.json.query import JSONPathComparison
|
|
18
|
+
from pyrsql.core.json.values import JSONScalarNormalizer, JSONScalarValue
|
|
19
|
+
from pyrsql.core.options import QueryOptions, SortOptions
|
|
20
|
+
from pyrsql.core.page import PageRequest
|
|
21
|
+
from pyrsql.core.procedure_policy import ProcedureAccessPolicy
|
|
22
|
+
from pyrsql.core.query import Query
|
|
23
|
+
from pyrsql.core.sort import Sort
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"CompilationResult",
|
|
27
|
+
"CustomPredicateDefinition",
|
|
28
|
+
"FieldPolicySet",
|
|
29
|
+
"FieldValueConverterSet",
|
|
30
|
+
"JSONOptions",
|
|
31
|
+
"JSONPath",
|
|
32
|
+
"JSONPathComparison",
|
|
33
|
+
"JSONScalarNormalizer",
|
|
34
|
+
"JSONScalarValue",
|
|
35
|
+
"PageCompilationResult",
|
|
36
|
+
"PageRequest",
|
|
37
|
+
"ProcedureAccessPolicy",
|
|
38
|
+
"Query",
|
|
39
|
+
"QueryOptions",
|
|
40
|
+
"Sort",
|
|
41
|
+
"SortCompilationResult",
|
|
42
|
+
"SortOptions",
|
|
43
|
+
"ValueConverter",
|
|
44
|
+
"ValueConverterRegistry",
|
|
45
|
+
]
|