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
@@ -0,0 +1,116 @@
1
+ """ORM-neutral semantic analysis."""
2
+
3
+ from pyrsql.core.options import QueryOptions
4
+ from pyrsql.parsing.ast import ComparisonNode, Expression, LogicalNode
5
+ from pyrsql.selector.analyzer import SelectorSemanticAnalyzer
6
+ from pyrsql.selector.ast import Selector
7
+ from pyrsql.selector.semantic import SemanticSelector
8
+ from pyrsql.semantic.ast import (
9
+ SemanticComparison,
10
+ SemanticExpression,
11
+ SemanticLogical,
12
+ )
13
+ from pyrsql.semantic.errors import (
14
+ FieldBlacklistedError,
15
+ FieldNotWhitelistedError,
16
+ FunctionBlacklistedError,
17
+ FunctionNotWhitelistedError,
18
+ )
19
+
20
+
21
+ class SemanticAnalyzer:
22
+ """Applies orm-neutral semantic rules to parsed expressions."""
23
+
24
+ def __init__(self, options: QueryOptions) -> None:
25
+ self._options = options
26
+ self._field_whitelist = options.field_whitelist
27
+ self._field_blacklist = options.field_blacklist
28
+ self._procedure_policy = options.procedure_policy
29
+ self._selector_analyzer = SelectorSemanticAnalyzer(
30
+ options.field_mapping
31
+ )
32
+
33
+ def analyze(self, expression: Expression) -> SemanticExpression:
34
+ """Analyzes a parsed AST into a semantic expression tree."""
35
+ if isinstance(expression, ComparisonNode):
36
+ return self._analyze_comparison(expression)
37
+ if not isinstance(expression, LogicalNode):
38
+ raise TypeError("Expected LogicalNode")
39
+ return SemanticLogical(
40
+ span=expression.span,
41
+ operator=expression.operator,
42
+ children=tuple(
43
+ self.analyze(child) for child in expression.children
44
+ ),
45
+ )
46
+
47
+ def _analyze_comparison(
48
+ self,
49
+ expression: ComparisonNode,
50
+ ) -> SemanticComparison:
51
+ """Analyzes a comparison node."""
52
+ return SemanticComparison(
53
+ span=expression.span,
54
+ selector=self._analyze_selector(
55
+ expression.selector,
56
+ expression=expression,
57
+ ),
58
+ operator=expression.operator,
59
+ arguments=expression.arguments,
60
+ )
61
+
62
+ def _analyze_selector(
63
+ self,
64
+ selector: Selector,
65
+ *,
66
+ expression: ComparisonNode,
67
+ ) -> SemanticSelector:
68
+ """Analyzes a parsed selector recursively."""
69
+ return self._selector_analyzer.analyze(
70
+ selector,
71
+ validate_field=lambda field_path: self._enforce_field_access_policy(
72
+ field_path,
73
+ expression,
74
+ ),
75
+ validate_function=(
76
+ lambda function_name: self._enforce_function_access_policy(
77
+ function_name,
78
+ expression=expression,
79
+ )
80
+ ),
81
+ )
82
+
83
+ def _enforce_field_access_policy(
84
+ self,
85
+ field_path: str,
86
+ expression: ComparisonNode,
87
+ ) -> None:
88
+ """Validates whitelist and blacklist rules."""
89
+ if self._field_whitelist and field_path not in self._field_whitelist:
90
+ raise FieldNotWhitelistedError(
91
+ message=f"Field {field_path!r} is not allowed",
92
+ span=expression.span,
93
+ )
94
+ if field_path in self._field_blacklist:
95
+ raise FieldBlacklistedError(
96
+ message=f"Field {field_path!r} is blocked",
97
+ span=expression.span,
98
+ )
99
+
100
+ def _enforce_function_access_policy(
101
+ self,
102
+ function_name: str,
103
+ *,
104
+ expression: ComparisonNode,
105
+ ) -> None:
106
+ """Validates whitelist and blacklist rules for functions."""
107
+ if not self._procedure_policy.is_whitelisted(function_name):
108
+ raise FunctionNotWhitelistedError(
109
+ message=f"Function {function_name!r} is not whitelisted",
110
+ span=expression.span,
111
+ )
112
+ if self._procedure_policy.is_blacklisted(function_name):
113
+ raise FunctionBlacklistedError(
114
+ message=f"Function {function_name!r} is blacklisted",
115
+ span=expression.span,
116
+ )
pyrsql/semantic/ast.py ADDED
@@ -0,0 +1,35 @@
1
+ """Semantic expression nodes."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pyrsql.parsing.ast import Argument, LogicalOperator
6
+ from pyrsql.parsing.operators import ComparisonOperator
7
+ from pyrsql.parsing.source import SourceSpan
8
+ from pyrsql.selector.semantic import SemanticSelector
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class SemanticNode:
13
+ """Base semantic node."""
14
+
15
+ span: SourceSpan
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class SemanticComparison(SemanticNode):
20
+ """Comparison expression after selector normalization."""
21
+
22
+ selector: SemanticSelector
23
+ operator: ComparisonOperator
24
+ arguments: tuple[Argument, ...]
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class SemanticLogical(SemanticNode):
29
+ """Logical semantic node."""
30
+
31
+ operator: LogicalOperator
32
+ children: tuple["SemanticExpression", ...]
33
+
34
+
35
+ SemanticExpression = SemanticComparison | SemanticLogical
@@ -0,0 +1,40 @@
1
+ """Semantic analysis exceptions."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pyrsql.parsing.source import SourceSpan
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class SemanticError(ValueError):
10
+ """Base exception for semantic analysis failures."""
11
+
12
+ message: str
13
+ span: SourceSpan
14
+
15
+ def __str__(self) -> str:
16
+ """Formats semantic errors with source position 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 FieldNotWhitelistedError(SemanticError):
25
+ """Raised when a selector is not allowed by the whitelist."""
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class FieldBlacklistedError(SemanticError):
30
+ """Raised when a selector is blocked by the blacklist."""
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class FunctionNotWhitelistedError(SemanticError):
35
+ """Raised when a function selector is not allowed by the whitelist."""
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class FunctionBlacklistedError(SemanticError):
40
+ """Raised when a function selector is blocked by the blacklist."""
@@ -0,0 +1,26 @@
1
+ """Sorting primitives for pyrsql."""
2
+
3
+ from pyrsql.sorting.ast import SortDirection, SortField
4
+ from pyrsql.sorting.errors import (
5
+ SortFieldBlacklistedError,
6
+ SortFieldNotWhitelistedError,
7
+ SortFunctionBlacklistedError,
8
+ SortFunctionNotWhitelistedError,
9
+ SortParseError,
10
+ )
11
+ from pyrsql.sorting.limits import SortLimits
12
+ from pyrsql.sorting.parser import SortParser
13
+ from pyrsql.sorting.semantic import SemanticSortField
14
+
15
+ __all__ = [
16
+ "SemanticSortField",
17
+ "SortDirection",
18
+ "SortField",
19
+ "SortFieldBlacklistedError",
20
+ "SortFieldNotWhitelistedError",
21
+ "SortFunctionBlacklistedError",
22
+ "SortFunctionNotWhitelistedError",
23
+ "SortLimits",
24
+ "SortParseError",
25
+ "SortParser",
26
+ ]
@@ -0,0 +1,74 @@
1
+ """Semantic analysis for sort expressions."""
2
+
3
+ from pyrsql.core.options import SortOptions
4
+ from pyrsql.selector.analyzer import SelectorSemanticAnalyzer
5
+ from pyrsql.selector.ast import Selector
6
+ from pyrsql.selector.semantic import SemanticSelector
7
+ from pyrsql.sorting.ast import SortField
8
+ from pyrsql.sorting.errors import (
9
+ SortFieldBlacklistedError,
10
+ SortFieldNotWhitelistedError,
11
+ SortFunctionBlacklistedError,
12
+ SortFunctionNotWhitelistedError,
13
+ )
14
+ from pyrsql.sorting.semantic import SemanticSortField
15
+
16
+
17
+ class SortAnalyzer:
18
+ """Applies selector mapping and access rules to parsed sort fields."""
19
+
20
+ def __init__(self, options: SortOptions) -> None:
21
+ self._field_whitelist = options.field_whitelist
22
+ self._field_blacklist = options.field_blacklist
23
+ self._procedure_policy = options.procedure_policy
24
+ self._selector_analyzer = SelectorSemanticAnalyzer(
25
+ options.field_mapping
26
+ )
27
+
28
+ def analyze(
29
+ self,
30
+ fields: tuple[SortField, ...],
31
+ ) -> tuple[SemanticSortField, ...]:
32
+ """Analyzes parsed sort fields into semantic sort fields."""
33
+ return tuple(self._analyze_field(field) for field in fields)
34
+
35
+ def _analyze_field(self, field: SortField) -> SemanticSortField:
36
+ """Analyzes a single parsed sort field."""
37
+ return SemanticSortField(
38
+ selector=self._analyze_selector(field.selector),
39
+ direction=field.direction,
40
+ ignore_case=field.ignore_case,
41
+ )
42
+
43
+ def _analyze_selector(
44
+ self,
45
+ selector: Selector,
46
+ ) -> SemanticSelector:
47
+ """Analyzes a parsed selector recursively."""
48
+ return self._selector_analyzer.analyze(
49
+ selector,
50
+ validate_field=self._validate_field_access,
51
+ validate_function=self._validate_function_access,
52
+ )
53
+
54
+ def _validate_field_access(self, field_path: str) -> None:
55
+ """Validates whitelist and blacklist rules for a sort field."""
56
+ if self._field_whitelist and field_path not in self._field_whitelist:
57
+ raise SortFieldNotWhitelistedError(
58
+ f"Field {field_path!r} is not allowed by the whitelist."
59
+ )
60
+ if field_path in self._field_blacklist:
61
+ raise SortFieldBlacklistedError(
62
+ f"Field {field_path!r} is blocked by the blacklist."
63
+ )
64
+
65
+ def _validate_function_access(self, function_name: str) -> None:
66
+ """Validates whitelist and blacklist rules for a sort function."""
67
+ if not self._procedure_policy.is_whitelisted(function_name):
68
+ raise SortFunctionNotWhitelistedError(
69
+ f"Function {function_name!r} is not whitelisted."
70
+ )
71
+ if self._procedure_policy.is_blacklisted(function_name):
72
+ raise SortFunctionBlacklistedError(
73
+ f"Function {function_name!r} is blacklisted."
74
+ )
pyrsql/sorting/ast.py ADDED
@@ -0,0 +1,46 @@
1
+ """AST nodes for pyrsql sort expressions."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ from pyrsql.selector.ast import Selector
7
+
8
+
9
+ class SortDirection(Enum):
10
+ """Supported sort directions."""
11
+
12
+ ASCENDING = "asc"
13
+ DESCENDING = "desc"
14
+
15
+ @classmethod
16
+ def from_raw(cls, raw_direction: str) -> "SortDirection | None":
17
+ """Matches a raw token to a supported sort direction.
18
+
19
+ Args:
20
+ raw_direction: Raw direction token from the sort expression.
21
+
22
+ Returns:
23
+ The matching direction, or None when the token is unsupported.
24
+ """
25
+ match raw_direction.lower():
26
+ case "asc":
27
+ return cls.ASCENDING
28
+ case "desc":
29
+ return cls.DESCENDING
30
+ case _:
31
+ return None
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class SortField:
36
+ """Single parsed sort field.
37
+
38
+ Attributes:
39
+ selector: Selector to sort by.
40
+ direction: Sort direction to apply.
41
+ ignore_case: Whether case should be ignored when sorting.
42
+ """
43
+
44
+ selector: Selector
45
+ direction: SortDirection
46
+ ignore_case: bool
@@ -0,0 +1,21 @@
1
+ """Errors raised while parsing or analyzing sort expressions."""
2
+
3
+
4
+ class SortParseError(ValueError):
5
+ """Raised when a sort expression is malformed."""
6
+
7
+
8
+ class SortFieldNotWhitelistedError(ValueError):
9
+ """Raised when a sort selector is not allowed by the whitelist."""
10
+
11
+
12
+ class SortFieldBlacklistedError(ValueError):
13
+ """Raised when a sort selector is blocked by the blacklist."""
14
+
15
+
16
+ class SortFunctionNotWhitelistedError(ValueError):
17
+ """Raised when a sort function is not allowed by the whitelist."""
18
+
19
+
20
+ class SortFunctionBlacklistedError(ValueError):
21
+ """Raised when a sort function is blocked by the blacklist."""
@@ -0,0 +1,15 @@
1
+ """Security and complexity limits for sort parsing."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class SortLimits:
8
+ """Configurable limits for sort parsing."""
9
+
10
+ max_sort_length: int = 4096
11
+ max_fields: int = 32
12
+ max_field_path_length: int = 256
13
+
14
+
15
+ DEFAULT_SORT_LIMITS = SortLimits()
@@ -0,0 +1,150 @@
1
+ """Parser for pyrsql sort expressions."""
2
+
3
+ from typing import Final
4
+
5
+ from pyrsql.selector.ast import Selector
6
+ from pyrsql.selector.parser import DEFAULT_SELECTOR_PARSER, SelectorParseError
7
+ from pyrsql.sorting.ast import SortDirection, SortField
8
+ from pyrsql.sorting.errors import SortParseError
9
+ from pyrsql.sorting.limits import DEFAULT_SORT_LIMITS, SortLimits
10
+
11
+ _EMPTY_FIELDS: Final[tuple[SortField, ...]] = ()
12
+
13
+
14
+ class SortParser:
15
+ """Parses sort expressions into orm-neutral field descriptors."""
16
+
17
+ def __init__(
18
+ self,
19
+ source: str | None,
20
+ *,
21
+ limits: SortLimits | None = None,
22
+ ) -> None:
23
+ self._source = (source or "").strip()
24
+ self._limits = limits or DEFAULT_SORT_LIMITS
25
+ self._selector_parser = DEFAULT_SELECTOR_PARSER
26
+
27
+ def parse(self) -> tuple[SortField, ...]:
28
+ """Parses the configured sort expression."""
29
+ if not self._source:
30
+ return _EMPTY_FIELDS
31
+
32
+ max_sort_length = self._limits.max_sort_length
33
+ max_fields = self._limits.max_fields
34
+ if len(self._source) > max_sort_length:
35
+ raise SortParseError(
36
+ "Sort expression exceeds the maximum supported length of "
37
+ f"{max_sort_length}."
38
+ )
39
+
40
+ fields: list[SortField] = []
41
+ for clause_index, clause in enumerate(self._source.split(";"), start=1):
42
+ parsed_field = self._parse_clause(clause, clause_index=clause_index)
43
+ if parsed_field is None:
44
+ continue
45
+ fields.append(parsed_field)
46
+ if len(fields) > max_fields:
47
+ raise SortParseError(
48
+ "Sort expression exceeds the maximum supported field "
49
+ f"count of {max_fields}."
50
+ )
51
+ return tuple(fields)
52
+
53
+ def _parse_clause(
54
+ self,
55
+ clause: str,
56
+ *,
57
+ clause_index: int,
58
+ ) -> SortField | None:
59
+ """Parses a single semicolon-delimited sort clause."""
60
+ try:
61
+ parts = list(
62
+ self._selector_parser.split_top_level(
63
+ clause,
64
+ delimiter=",",
65
+ )
66
+ )
67
+ except SelectorParseError as error:
68
+ raise SortParseError(str(error)) from error
69
+ if not parts:
70
+ return None
71
+ if len(parts) > 3:
72
+ raise SortParseError(
73
+ "Sort clause "
74
+ f"#{clause_index} {clause!r} has too many comma-separated "
75
+ "parts."
76
+ )
77
+
78
+ selector = self._parse_selector(parts[0], clause_index=clause_index)
79
+
80
+ direction = SortDirection.ASCENDING
81
+ if len(parts) > 1:
82
+ direction = self._parse_direction(
83
+ parts[1],
84
+ clause,
85
+ clause_index=clause_index,
86
+ )
87
+
88
+ ignore_case = False
89
+ if len(parts) > 2:
90
+ ignore_case = self._parse_ignore_case(
91
+ parts[2],
92
+ clause,
93
+ clause_index=clause_index,
94
+ )
95
+
96
+ return SortField(
97
+ selector=selector,
98
+ direction=direction,
99
+ ignore_case=ignore_case,
100
+ )
101
+
102
+ def _parse_direction(
103
+ self,
104
+ raw_direction: str,
105
+ clause: str,
106
+ *,
107
+ clause_index: int,
108
+ ) -> SortDirection:
109
+ """Parses the direction token for a sort clause."""
110
+ direction = SortDirection.from_raw(raw_direction)
111
+ if direction is not None:
112
+ return direction
113
+ raise SortParseError(
114
+ f"Sort clause #{clause_index} {clause!r} has unsupported direction "
115
+ f"{raw_direction!r}."
116
+ )
117
+
118
+ def _parse_ignore_case(
119
+ self,
120
+ raw_flag: str,
121
+ clause: str,
122
+ *,
123
+ clause_index: int,
124
+ ) -> bool:
125
+ """Parses the ignore-case modifier for a sort clause."""
126
+ match raw_flag.lower():
127
+ case "ic":
128
+ return True
129
+ case _:
130
+ raise SortParseError(
131
+ f"Sort clause #{clause_index} {clause!r} has unsupported "
132
+ "modifier "
133
+ f"{raw_flag!r}."
134
+ )
135
+
136
+ def _parse_selector(
137
+ self,
138
+ raw_selector: str,
139
+ *,
140
+ clause_index: int,
141
+ ) -> Selector:
142
+ """Parses a sort selector recursively."""
143
+ try:
144
+ return self._selector_parser.parse(
145
+ raw_selector,
146
+ max_length=self._limits.max_field_path_length,
147
+ context=f"Sort selector in clause #{clause_index}",
148
+ )
149
+ except SelectorParseError as error:
150
+ raise SortParseError(str(error)) from error
@@ -0,0 +1,15 @@
1
+ """Semantic sort nodes."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pyrsql.selector.semantic import SemanticSelector
6
+ from pyrsql.sorting.ast import SortDirection
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class SemanticSortField:
11
+ """Single sort field after selector normalization."""
12
+
13
+ selector: SemanticSelector
14
+ direction: SortDirection
15
+ ignore_case: bool
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrsql
3
+ Version: 0.0.1
4
+ Summary: ORM-oriented RSQL support for Python applications.
5
+ Keywords: rsql,orm,sqlalchemy,fastapi,query-language,rest
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Topic :: Database
16
+ Requires-Dist: ciso8601>=2.3,<3.0
17
+ Requires-Dist: msgspec>=0.18,<1.0
18
+ Requires-Dist: typing-extensions>=4.10.0 ; python_full_version < '3.11'
19
+ Requires-Dist: fastapi>=0.110,<1.0 ; extra == 'fastapi'
20
+ Requires-Dist: sqlalchemy>=2.0,<3.0 ; extra == 'sqlalchemy'
21
+ Requires-Python: >=3.10
22
+ Project-URL: Homepage, https://github.com/wskr00/pyrsql
23
+ Project-URL: Repository, https://github.com/wskr00/pyrsql
24
+ Project-URL: Issues, https://github.com/wskr00/pyrsql/issues
25
+ Project-URL: Documentation, https://github.com/wskr00/pyrsql/blob/main/README.md
26
+ Provides-Extra: fastapi
27
+ Provides-Extra: sqlalchemy
28
+ Description-Content-Type: text/markdown
29
+
30
+ ## pyrsql
31
+
32
+ `pyrsql` is an ORM-oriented RSQL library for Python.
33
+
34
+ The project is being designed to support multiple ORMs, starting with
35
+ `SQLAlchemy 2.0`. The core package remains ORM-neutral, while ORM
36
+ implementations live in `orms`, and framework-specific integration lives
37
+ in `adapters`.
38
+
39
+ Current `SQLAlchemy` support includes:
40
+
41
+ - filters
42
+ - sorting
43
+ - pagination
44
+ - `distinct`
45
+ - `join_hints`
46
+ - PostgreSQL-style JSON / JSONB path support
47
+ - temporal JSON path semantics via `JSONOptions(use_datetime=True)`
48
+
49
+ Current adapter support includes:
50
+
51
+ - `FastAPI` request extraction via dependencies
52
+ - configurable `filter`, `sort`, `page`, and `size` parameters
53
+ - class-based and factory-based FastAPI dependencies
54
+ - `HTTP 422` translation for pyrsql parse and semantic failures
55
+
56
+ ## Development Principles
57
+
58
+ - Object-oriented design
59
+ - SOLID principles
60
+ - Google Python Style Guide
61
+ - Strong typing
62
+ - ORM-neutral public API
63
+
64
+ ## Documentation
65
+
66
+ - [Architecture](docs/architecture.md)
67
+ - [Usage](docs/usage.md)
68
+ - [Reference](docs/reference.md)
69
+ - [FastAPI Adapter Usage](docs/usage.md#fastapi-adapter)
70
+ - [Google Python Style Guide](docs/pyguide.md)
71
+ - [Quality and Tooling](docs/quality.md)
72
+ - [Testing](docs/testing.md)