pyrsql 0.0.4__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyrsql-0.0.4 → pyrsql-0.0.5}/PKG-INFO +14 -11
- {pyrsql-0.0.4 → pyrsql-0.0.5}/README.md +8 -4
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/explanation/architecture.md +1 -1
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/index.md +1 -1
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/quickstart.md +1 -1
- pyrsql-0.0.5/docs/reference/operators.md +51 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/reference/options.md +4 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/query.md +9 -5
- pyrsql-0.0.5/pyproject.toml +233 -0
- pyrsql-0.0.4/pyproject.toml → pyrsql-0.0.5/pyproject.toml.orig +21 -19
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/json/options.py +2 -2
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/orm.py +1 -1
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/translator.py +2 -2
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/type_inference.py +9 -8
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/lexer.py +6 -6
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/integration/sqlalchemy/test_async_pipeline.py +16 -2
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/integration/sqlalchemy/test_query_pipeline.py +58 -16
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/adapters/fastapi_adapter/conftest.py +2 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/test_conversion.py +1 -1
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/integrations/fastapi_sqlalchemy/conftest.py +4 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/orms/sqlalchemy/test_json_path_builder.py +1 -1
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/orms/sqlalchemy/test_resolver.py +1 -1
- pyrsql-0.0.5/tests/unit/orms/sqlalchemy/test_type_inference.py +43 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/parsing/test_lexer.py +37 -0
- pyrsql-0.0.4/docs/reference/operators.md +0 -37
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/contributing.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/explanation/extensibility.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/reference/api.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/testing.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/async.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/custom-predicates.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/errors.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/fastapi-sqlalchemy.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/fastapi.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/field-policies.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/join-hints.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/json.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/page.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/sort.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/docs/usage/value-conversion.md +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/adapters/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/adapters/fastapi/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/adapters/fastapi/config.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/adapters/fastapi/criteria.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/adapters/fastapi/dependency.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/adapters/fastapi/errors.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/api.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/binding_policy.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/compiler.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/conversion.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/custom.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/field_policy.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/joins.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/json/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/json/path.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/json/query.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/json/values.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/options.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/page.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/procedure_policy.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/query.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/sort.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/source_issues.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/core/validation.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/integrations/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/integrations/fastapi/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/integrations/fastapi/sqlalchemy/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/integrations/fastapi/sqlalchemy/examples.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/integrations/fastapi/sqlalchemy/helpers.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/integrations/fastapi/sqlalchemy/integration.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/integrations/fastapi/sqlalchemy/payloads.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/integrations/fastapi/sqlalchemy/resource.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/base.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/coercion.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/compiled_page.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/compiled_query.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/compiled_sort.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/custom.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/errors.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/introspection.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/json_path.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/resolver.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/sorter.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/statement.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/orms/sqlalchemy/types.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/ast.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/errors.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/limits.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/operators.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/parser.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/source.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/parsing/tokens.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/py.typed +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/selector/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/selector/ast.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/selector/parser.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/semantic/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/semantic/binder.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/semantic/diagnostics.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/semantic/errors.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/sorting/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/sorting/ast.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/sorting/binder.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/sorting/diagnostics.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/sorting/errors.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/sorting/limits.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/src/pyrsql/sorting/parser.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/conftest.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/fastapi_adapter/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/fastapi_adapter/test_async_dependency.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/fastapi_adapter/test_dependency.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/fastapi_sqlalchemy/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/fastapi_sqlalchemy/conftest.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/fastapi_sqlalchemy/test_async_integration.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/fastapi_sqlalchemy/test_integration.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/functional/test_public_api.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/integration/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/integration/sqlalchemy/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/integration/sqlalchemy/conftest.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/integration/sqlalchemy/test_page_pipeline.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/integration/sqlalchemy/test_sort_pipeline.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/conftest.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/test_conversion_bench.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/test_json_bench.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/test_json_path_builder_bench.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/test_parser_bench.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/test_path_resolver_bench.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/test_selector_bench.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/test_semantic_bench.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/performance/test_sqlalchemy_translation_bench.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/conftest.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_configuration_hardening.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_fastapi_sqlalchemy_error_sanitization.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_fastapi_sqlalchemy_hpp.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_fastapi_sqlalchemy_injection.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_fastapi_sqlalchemy_limits.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_fastapi_sqlalchemy_policies.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_fastapi_sqlalchemy_resource_security.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_sqlalchemy_injection.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/security/test_sqlalchemy_jsonpath_security.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/adapters/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/adapters/fastapi_adapter/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/adapters/fastapi_adapter/test_config.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/adapters/fastapi_adapter/test_criteria.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/adapters/fastapi_adapter/test_errors.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/conftest.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/conftest.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/json/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/json/test_options.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/json/test_path.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/json/test_query.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/json/test_values.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/test_custom.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/test_options.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/test_page.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/test_query.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/core/test_sort.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/integrations/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/integrations/fastapi/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/integrations/fastapi_sqlalchemy/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/integrations/fastapi_sqlalchemy/test_examples.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/integrations/fastapi_sqlalchemy/test_integration.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/orms/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/orms/sqlalchemy/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/orms/sqlalchemy/conftest.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/orms/sqlalchemy/test_coercion.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/orms/sqlalchemy/test_orm.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/orms/sqlalchemy/test_statement.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/parsing/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/parsing/test_ast.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/parsing/test_parser.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/parsing/test_source.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/selector/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/selector/test_parser.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/semantic/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/semantic/test_binder.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/sorting/__init__.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/sorting/test_ast.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/sorting/test_binder.py +0 -0
- {pyrsql-0.0.4 → pyrsql-0.0.5}/tests/unit/sorting/test_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyrsql
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Compiler-oriented RSQL query engine for safe, typed, and extensible Python ORM-backed APIs.
|
|
5
5
|
Keywords: rsql,orm,filter,pagination,sort,sqlalchemy,fastapi,rest
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,7 +9,6 @@ Classifier: Environment :: Web Environment
|
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
10
10
|
Classifier: Intended Audience :: Information Technology
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -20,11 +19,11 @@ Classifier: Topic :: Database
|
|
|
20
19
|
Classifier: Topic :: Software Development :: Compilers
|
|
21
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
21
|
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Dist: msgspec>=0.
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Python: >=3.
|
|
22
|
+
Requires-Dist: msgspec>=0.22.0,<1.0
|
|
23
|
+
Requires-Dist: fastapi>=0.150.0,<1.0 ; extra == 'fastapi'
|
|
24
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.1,<3.0 ; extra == 'sqlalchemy'
|
|
25
|
+
Requires-Dist: typing-extensions>=4.10.0 ; extra == 'sqlalchemy'
|
|
26
|
+
Requires-Python: >=3.11
|
|
28
27
|
Project-URL: Homepage, https://github.com/wskr00/pyrsql
|
|
29
28
|
Project-URL: Repository, https://github.com/wskr00/pyrsql
|
|
30
29
|
Project-URL: Issues, https://github.com/wskr00/pyrsql/issues
|
|
@@ -38,7 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
38
37
|
A compiler-oriented RSQL query engine for safe, typed, and extensible
|
|
39
38
|
filtering, sorting, and pagination in Python APIs.
|
|
40
39
|
|
|
41
|
-
[](https://pypi.org/project/pyrsql/)
|
|
42
41
|
[](LICENSE)
|
|
43
42
|
|
|
44
43
|
pyrsql compiles RSQL query strings into ORM-specific statement objects
|
|
@@ -51,9 +50,9 @@ For FastAPI applications using SQLAlchemy, the recommended entry point is
|
|
|
51
50
|
declarative resources; the lower-level query, adapter, and ORM APIs remain
|
|
52
51
|
available when more control is needed.
|
|
53
52
|
|
|
54
|
-
**Current backends:** SQLAlchemy 2.
|
|
55
|
-
**Current framework adapters:** FastAPI
|
|
56
|
-
**Planned:** Django ORM, SQLModel, Flask
|
|
53
|
+
- **Current backends:** SQLAlchemy 2.1+
|
|
54
|
+
- **Current framework adapters:** FastAPI
|
|
55
|
+
- **Planned:** Django ORM, SQLModel, Flask
|
|
57
56
|
|
|
58
57
|
## Why pyrsql?
|
|
59
58
|
|
|
@@ -276,6 +275,10 @@ Full documentation at **[wskr00.github.io/pyrsql](https://wskr00.github.io/pyrsq
|
|
|
276
275
|
- Strong typing
|
|
277
276
|
- ORM-neutral public API
|
|
278
277
|
|
|
278
|
+
## Acknowledgements
|
|
279
|
+
|
|
280
|
+
`pyrsql` was strongly inspired by [Jirutka's `rsql-parser`](https://github.com/jirutka/rsql-parser) and [PerplexHub's `rsql-jpa-specification`](https://github.com/perplexhub/rsql-jpa-specification), especially their RSQL syntax and filtering behavior. I am grateful to their maintainers and contributors for their work.
|
|
281
|
+
|
|
279
282
|
## License
|
|
280
283
|
|
|
281
284
|
[MIT](LICENSE).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A compiler-oriented RSQL query engine for safe, typed, and extensible
|
|
4
4
|
filtering, sorting, and pagination in Python APIs.
|
|
5
5
|
|
|
6
|
-
[](https://pypi.org/project/pyrsql/)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
9
|
pyrsql compiles RSQL query strings into ORM-specific statement objects
|
|
@@ -16,9 +16,9 @@ For FastAPI applications using SQLAlchemy, the recommended entry point is
|
|
|
16
16
|
declarative resources; the lower-level query, adapter, and ORM APIs remain
|
|
17
17
|
available when more control is needed.
|
|
18
18
|
|
|
19
|
-
**Current backends:** SQLAlchemy 2.
|
|
20
|
-
**Current framework adapters:** FastAPI
|
|
21
|
-
**Planned:** Django ORM, SQLModel, Flask
|
|
19
|
+
- **Current backends:** SQLAlchemy 2.1+
|
|
20
|
+
- **Current framework adapters:** FastAPI
|
|
21
|
+
- **Planned:** Django ORM, SQLModel, Flask
|
|
22
22
|
|
|
23
23
|
## Why pyrsql?
|
|
24
24
|
|
|
@@ -241,6 +241,10 @@ Full documentation at **[wskr00.github.io/pyrsql](https://wskr00.github.io/pyrsq
|
|
|
241
241
|
- Strong typing
|
|
242
242
|
- ORM-neutral public API
|
|
243
243
|
|
|
244
|
+
## Acknowledgements
|
|
245
|
+
|
|
246
|
+
`pyrsql` was strongly inspired by [Jirutka's `rsql-parser`](https://github.com/jirutka/rsql-parser) and [PerplexHub's `rsql-jpa-specification`](https://github.com/perplexhub/rsql-jpa-specification), especially their RSQL syntax and filtering behavior. I am grateful to their maintainers and contributors for their work.
|
|
247
|
+
|
|
244
248
|
## License
|
|
245
249
|
|
|
246
250
|
[MIT](LICENSE).
|
|
@@ -28,7 +28,7 @@ The project is designed around four major responsibilities:
|
|
|
28
28
|
- backend lowering: turn validated query objects into ORM-specific statements
|
|
29
29
|
- optional framework adapters and integrations: expose the core in HTTP stacks
|
|
30
30
|
|
|
31
|
-
The current production backend is SQLAlchemy 2.
|
|
31
|
+
The current production backend is SQLAlchemy 2.1+. The current framework
|
|
32
32
|
adapter is FastAPI.
|
|
33
33
|
|
|
34
34
|
## Current Pipeline
|
|
@@ -45,7 +45,7 @@ I/O.
|
|
|
45
45
|
## Key features
|
|
46
46
|
|
|
47
47
|
- **ORM-neutral core** - `Query`, `Sort`, `PageRequest` have zero ORM dependencies
|
|
48
|
-
- **Pluggable backends** - SQLAlchemy 2.
|
|
48
|
+
- **Pluggable backends** - SQLAlchemy 2.1+ today, Django ORM / SQLModel planned
|
|
49
49
|
- **Pluggable adapters** - FastAPI today, Flask planned
|
|
50
50
|
- **20+ built-in operators** - `==`, `=like=`, `=in=`, `=bt=`, `=nn=`, etc.
|
|
51
51
|
- **Custom operators** - define your own RSQL operators with per-ORM lowering
|
|
@@ -55,7 +55,7 @@ from pyrsql import Query
|
|
|
55
55
|
Query.parse("name==demo") # equal
|
|
56
56
|
Query.parse("age=gt=18") # greater than
|
|
57
57
|
Query.parse("id=in=(1,2,3)") # IN
|
|
58
|
-
Query.parse("name=like=demo
|
|
58
|
+
Query.parse("name=like=demo") # contains: LIKE '%demo%'
|
|
59
59
|
Query.parse("score=bt=(10,20)") # BETWEEN
|
|
60
60
|
Query.parse("name==demo;age=gt=18") # AND
|
|
61
61
|
Query.parse("name==demo,age=gt=18") # OR
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Operators
|
|
2
|
+
|
|
3
|
+
Pyrsql supports the base RSQL comparison operators and the extensions used by
|
|
4
|
+
PerplexHub's JPA specification.
|
|
5
|
+
|
|
6
|
+
## Comparison operators
|
|
7
|
+
|
|
8
|
+
| Operator | Aliases | Arity | Description |
|
|
9
|
+
|----------|---------|-------|-------------|
|
|
10
|
+
| `==` | | 1 | Equal |
|
|
11
|
+
| `!=` | | 1 | Not equal |
|
|
12
|
+
| `=gt=` | `>` | 1 | Greater than |
|
|
13
|
+
| `=ge=` | `>=` | 1 | Greater or equal |
|
|
14
|
+
| `=lt=` | `<` | 1 | Less than |
|
|
15
|
+
| `=le=` | `<=` | 1 | Less or equal |
|
|
16
|
+
| `=in=` | | 1..100 | IN (comma-separated args) |
|
|
17
|
+
| `=out=` | | 1..100 | NOT IN |
|
|
18
|
+
| `=like=` | `=ke=` | 1 | Contains match (`LIKE '%value%'`) |
|
|
19
|
+
| `=notlike=` | `=nk=` | 1 | Negated contains match |
|
|
20
|
+
| `=ilike=` | `=ik=` | 1 | Case-insensitive contains match |
|
|
21
|
+
| `=inotlike=` | `=ni=` | 1 | Negated case-insensitive contains match |
|
|
22
|
+
| `=ic=` | `=icase=` | 1 | Case-insensitive equality |
|
|
23
|
+
| `=bt=` | `=between=` | 2 | BETWEEN (exactly 2 args) |
|
|
24
|
+
| `=nb=` | `=notbetween=` | 2 | NOT BETWEEN (exactly 2 args) |
|
|
25
|
+
| `=na=` | `=isnull=`, `=null=` | 0..1 | IS NULL |
|
|
26
|
+
| `=nn=` | `=notnull=`, `=isnotnull=` | 0..1 | IS NOT NULL |
|
|
27
|
+
|
|
28
|
+
For `=like=`, `=notlike=`, `=ilike=`, and `=inotlike=`, pyrsql surrounds the
|
|
29
|
+
argument with `%` to match anywhere in the value. These operators use SQL
|
|
30
|
+
`LIKE` pattern rules: `%` matches any number of characters, `_` matches one,
|
|
31
|
+
and `*` is literal. The `like_escape_character` option controls SQL pattern
|
|
32
|
+
escaping.
|
|
33
|
+
|
|
34
|
+
For string-column `==` and `!=`, `*` is the wildcard and any `^` marker
|
|
35
|
+
requests case-insensitive matching. Quoting an argument does not disable these
|
|
36
|
+
markers. Use `=in=(*)` to match a literal asterisk.
|
|
37
|
+
|
|
38
|
+
## Logical operators
|
|
39
|
+
|
|
40
|
+
| Operator | Aliases | Description |
|
|
41
|
+
|----------|---------|-------------|
|
|
42
|
+
| `;` | `and` | AND (higher precedence) |
|
|
43
|
+
| `,` | `or` | OR (lower precedence) |
|
|
44
|
+
|
|
45
|
+
## Sort modifiers
|
|
46
|
+
|
|
47
|
+
| Modifier | Example | Description |
|
|
48
|
+
|----------|---------|-------------|
|
|
49
|
+
| `asc` | `name,asc` | Ascending (default) |
|
|
50
|
+
| `desc` | `name,desc` | Descending |
|
|
51
|
+
| `ic` | `name,desc,ic` | Ignore case |
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
|
|
27
27
|
Notes:
|
|
28
28
|
|
|
29
|
+
- `strict_equality=True` makes string `==` and `!=` compare markers such as
|
|
30
|
+
`*` and `^` literally; quoting alone does not disable them
|
|
31
|
+
- `like_escape_character` applies to LIKE-based operators, including equality
|
|
32
|
+
comparisons that contain `*`
|
|
29
33
|
- configuration containers are normalized into immutable views
|
|
30
34
|
- procedure allow/block rules are compiled as regex policies and evaluated with
|
|
31
35
|
`fullmatch`, not partial match
|
|
@@ -29,10 +29,10 @@ Query.parse("name=nn=") # IS NOT NULL
|
|
|
29
29
|
## Text matching
|
|
30
30
|
|
|
31
31
|
```python
|
|
32
|
-
Query.parse("name=like=demo
|
|
32
|
+
Query.parse("name=like=demo") # contains: LIKE '%demo%'
|
|
33
33
|
Query.parse("name=notlike=demo") # NOT LIKE
|
|
34
|
-
Query.parse("name=ic=
|
|
35
|
-
Query.parse("name=ilike=DEMO") #
|
|
34
|
+
Query.parse("name=ic=DEMO") # case-insensitive equality
|
|
35
|
+
Query.parse("name=ilike=DEMO") # case-insensitive contains
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Range
|
|
@@ -54,15 +54,19 @@ Query.parse("(name==demo;age=gt=18),status==active") # grouping
|
|
|
54
54
|
|
|
55
55
|
```python
|
|
56
56
|
Query.parse("name=='de\\'mo'") # quoted string with escape
|
|
57
|
-
Query.parse(
|
|
57
|
+
Query.parse('name=="Kill Bill"') # double-quoted argument
|
|
58
|
+
Query.parse("name=='*demo*'") # quotes do not disable equality wildcards
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
## Wildcard matching in equality
|
|
61
62
|
|
|
62
|
-
By default, `*` in equality expressions is treated as a `LIKE`
|
|
63
|
+
By default, `*` in string equality expressions is treated as a `LIKE`
|
|
64
|
+
wildcard, and `^` makes the equality case-insensitive:
|
|
63
65
|
|
|
64
66
|
```python
|
|
65
67
|
Query.parse("name==*demo*") # becomes LIKE '%demo%'
|
|
68
|
+
Query.parse("name==^*DEMO*") # case-insensitive LIKE '%DEMO%'
|
|
69
|
+
Query.parse("name=in=(*)") # exact match for a literal asterisk
|
|
66
70
|
```
|
|
67
71
|
|
|
68
72
|
Disable with strict mode:
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyrsql"
|
|
3
|
+
version = "0.0.5"
|
|
4
|
+
description = "Compiler-oriented RSQL query engine for safe, typed, and extensible Python ORM-backed APIs."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
keywords = [
|
|
9
|
+
"rsql",
|
|
10
|
+
"orm",
|
|
11
|
+
"filter",
|
|
12
|
+
"pagination",
|
|
13
|
+
"sort",
|
|
14
|
+
"sqlalchemy",
|
|
15
|
+
"fastapi",
|
|
16
|
+
"rest",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Environment :: Web Environment",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Information Technology",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Programming Language :: Python :: Free Threading",
|
|
29
|
+
"Framework :: FastAPI",
|
|
30
|
+
"Topic :: Database",
|
|
31
|
+
"Topic :: Software Development :: Compilers",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
|
+
"Typing :: Typed",
|
|
34
|
+
]
|
|
35
|
+
dependencies = ["msgspec>=0.22.0,<1.0"]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
sqlalchemy = [
|
|
39
|
+
"sqlalchemy[asyncio]>=2.1,<3.0",
|
|
40
|
+
"typing-extensions>=4.10.0",
|
|
41
|
+
]
|
|
42
|
+
fastapi = ["fastapi>=0.150.0,<1.0"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/wskr00/pyrsql"
|
|
46
|
+
Repository = "https://github.com/wskr00/pyrsql"
|
|
47
|
+
Issues = "https://github.com/wskr00/pyrsql/issues"
|
|
48
|
+
Documentation = "https://wskr00.github.io/pyrsql/"
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
lint = ["ruff>=0.15.21"]
|
|
52
|
+
test = [
|
|
53
|
+
"pytest>=9.1.1",
|
|
54
|
+
"httpx>=0.28.1",
|
|
55
|
+
"pytest-cov>=7.1.0",
|
|
56
|
+
"aiosqlite>=0.21.0,<0.22.0",
|
|
57
|
+
"sqlalchemy[asyncio]>=2.1,<3.0",
|
|
58
|
+
"typing-extensions>=4.10.0",
|
|
59
|
+
]
|
|
60
|
+
typing = ["mypy>=2.2.0"]
|
|
61
|
+
docs = [
|
|
62
|
+
"mkdocs-material>=9.7.6",
|
|
63
|
+
"mkdocstrings[python]>=1.0.5",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[[dependency-groups.dev]]
|
|
67
|
+
include-group = "lint"
|
|
68
|
+
|
|
69
|
+
[[dependency-groups.dev]]
|
|
70
|
+
include-group = "test"
|
|
71
|
+
|
|
72
|
+
[[dependency-groups.dev]]
|
|
73
|
+
include-group = "typing"
|
|
74
|
+
|
|
75
|
+
[[dependency-groups.dev]]
|
|
76
|
+
include-group = "docs"
|
|
77
|
+
|
|
78
|
+
[build-system]
|
|
79
|
+
requires = ["uv_build>=0.13.1,<0.14.0"]
|
|
80
|
+
build-backend = "uv_build"
|
|
81
|
+
|
|
82
|
+
[tool.uv]
|
|
83
|
+
managed = true
|
|
84
|
+
|
|
85
|
+
[tool.uv.build-backend]
|
|
86
|
+
source-include = [
|
|
87
|
+
"docs/**",
|
|
88
|
+
"tests/**",
|
|
89
|
+
"src/pyrsql/py.typed",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[tool.ruff]
|
|
93
|
+
line-length = 80
|
|
94
|
+
target-version = "py311"
|
|
95
|
+
src = [
|
|
96
|
+
"src",
|
|
97
|
+
"tests",
|
|
98
|
+
]
|
|
99
|
+
fix = true
|
|
100
|
+
|
|
101
|
+
[tool.ruff.lint]
|
|
102
|
+
select = ["ALL"]
|
|
103
|
+
preview = true
|
|
104
|
+
fixable = ["ALL"]
|
|
105
|
+
unfixable = ["ERA"]
|
|
106
|
+
ignore = [
|
|
107
|
+
"CPY001",
|
|
108
|
+
"EM101",
|
|
109
|
+
"EM102",
|
|
110
|
+
"TRY003",
|
|
111
|
+
"COM812",
|
|
112
|
+
"ISC001",
|
|
113
|
+
"C901",
|
|
114
|
+
"PLR0913",
|
|
115
|
+
"PLR0911",
|
|
116
|
+
"PLR0912",
|
|
117
|
+
]
|
|
118
|
+
future-annotations = true
|
|
119
|
+
|
|
120
|
+
[tool.ruff.lint.pydocstyle]
|
|
121
|
+
convention = "google"
|
|
122
|
+
|
|
123
|
+
[tool.ruff.lint.flake8-annotations]
|
|
124
|
+
mypy-init-return = true
|
|
125
|
+
suppress-dummy-args = true
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
128
|
+
extend-immutable-calls = [
|
|
129
|
+
"fastapi.Depends",
|
|
130
|
+
"fastapi.Query",
|
|
131
|
+
"fastapi.Path",
|
|
132
|
+
"fastapi.Header",
|
|
133
|
+
"fastapi.Cookie",
|
|
134
|
+
"fastapi.Body",
|
|
135
|
+
"fastapi.Form",
|
|
136
|
+
"fastapi.File",
|
|
137
|
+
"fastapi.Security",
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
[tool.ruff.lint.flake8-type-checking]
|
|
141
|
+
exempt-modules = [
|
|
142
|
+
"typing",
|
|
143
|
+
"typing_extensions",
|
|
144
|
+
]
|
|
145
|
+
strict = false
|
|
146
|
+
runtime-evaluated-base-classes = ["sqlalchemy.orm.DeclarativeBase"]
|
|
147
|
+
runtime-evaluated-decorators = [
|
|
148
|
+
"fastapi.FastAPI.get",
|
|
149
|
+
"fastapi.FastAPI.post",
|
|
150
|
+
"fastapi.FastAPI.put",
|
|
151
|
+
"fastapi.FastAPI.patch",
|
|
152
|
+
"fastapi.FastAPI.delete",
|
|
153
|
+
"fastapi.FastAPI.options",
|
|
154
|
+
"fastapi.FastAPI.head",
|
|
155
|
+
"fastapi.FastAPI.trace",
|
|
156
|
+
"fastapi.FastAPI.api_route",
|
|
157
|
+
"fastapi.APIRouter.get",
|
|
158
|
+
"fastapi.APIRouter.post",
|
|
159
|
+
"fastapi.APIRouter.put",
|
|
160
|
+
"fastapi.APIRouter.patch",
|
|
161
|
+
"fastapi.APIRouter.delete",
|
|
162
|
+
"fastapi.APIRouter.options",
|
|
163
|
+
"fastapi.APIRouter.head",
|
|
164
|
+
"fastapi.APIRouter.trace",
|
|
165
|
+
"fastapi.APIRouter.api_route",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
[tool.ruff.lint.isort]
|
|
169
|
+
known-first-party = ["pyrsql"]
|
|
170
|
+
combine-as-imports = true
|
|
171
|
+
force-sort-within-sections = true
|
|
172
|
+
|
|
173
|
+
[tool.ruff.lint.flake8-pytest-style]
|
|
174
|
+
fixture-parentheses = false
|
|
175
|
+
mark-parentheses = false
|
|
176
|
+
|
|
177
|
+
[tool.ruff.lint.per-file-ignores]
|
|
178
|
+
"tests/**/*.py" = [
|
|
179
|
+
"ANN",
|
|
180
|
+
"ARG",
|
|
181
|
+
"B008",
|
|
182
|
+
"DOC201",
|
|
183
|
+
"DOC501",
|
|
184
|
+
"DOC502",
|
|
185
|
+
"D",
|
|
186
|
+
"E402",
|
|
187
|
+
"FBT",
|
|
188
|
+
"PLR2004",
|
|
189
|
+
"S101",
|
|
190
|
+
"SLF001",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
[tool.ruff.format]
|
|
194
|
+
quote-style = "double"
|
|
195
|
+
indent-style = "space"
|
|
196
|
+
line-ending = "auto"
|
|
197
|
+
docstring-code-format = true
|
|
198
|
+
docstring-code-line-length = 80
|
|
199
|
+
|
|
200
|
+
[tool.pytest.ini_options]
|
|
201
|
+
testpaths = ["tests"]
|
|
202
|
+
pythonpath = ["src"]
|
|
203
|
+
addopts = ["--import-mode=importlib"]
|
|
204
|
+
markers = [
|
|
205
|
+
"unit: isolated tests",
|
|
206
|
+
"integration: interaction between components",
|
|
207
|
+
"functional: user-visible behavior",
|
|
208
|
+
"security: adversarial safety and injection-resistance tests",
|
|
209
|
+
"performance: benchmark tests",
|
|
210
|
+
"sqlalchemy: SQLAlchemy tests",
|
|
211
|
+
"fastapi: FastAPI tests",
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
[tool.mypy]
|
|
215
|
+
python_version = "3.11"
|
|
216
|
+
files = ["src"]
|
|
217
|
+
exclude = ["^tests/"]
|
|
218
|
+
strict = true
|
|
219
|
+
warn_unused_configs = true
|
|
220
|
+
show_error_codes = true
|
|
221
|
+
pretty = true
|
|
222
|
+
|
|
223
|
+
[tool.coverage.run]
|
|
224
|
+
source = ["pyrsql"]
|
|
225
|
+
branch = true
|
|
226
|
+
|
|
227
|
+
[tool.coverage.report]
|
|
228
|
+
show_missing = true
|
|
229
|
+
exclude_lines = [
|
|
230
|
+
"pragma: no cover",
|
|
231
|
+
"if TYPE_CHECKING:",
|
|
232
|
+
"raise NotImplementedError",
|
|
233
|
+
]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyrsql"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.5"
|
|
4
4
|
description = "Compiler-oriented RSQL query engine for safe, typed, and extensible Python ORM-backed APIs."
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
keywords = ["rsql", "orm", "filter", "pagination", "sort", "sqlalchemy", "fastapi", "rest"]
|
|
9
9
|
classifiers = [
|
|
@@ -12,7 +12,6 @@ classifiers = [
|
|
|
12
12
|
"Intended Audience :: Developers",
|
|
13
13
|
"Intended Audience :: Information Technology",
|
|
14
14
|
"Operating System :: OS Independent",
|
|
15
|
-
"Programming Language :: Python :: 3.10",
|
|
16
15
|
"Programming Language :: Python :: 3.11",
|
|
17
16
|
"Programming Language :: Python :: 3.12",
|
|
18
17
|
"Programming Language :: Python :: 3.13",
|
|
@@ -25,13 +24,15 @@ classifiers = [
|
|
|
25
24
|
"Typing :: Typed",
|
|
26
25
|
]
|
|
27
26
|
dependencies = [
|
|
28
|
-
"msgspec>=0.
|
|
29
|
-
"typing-extensions>=4.10.0; python_version < '3.11'",
|
|
27
|
+
"msgspec>=0.22.0,<1.0",
|
|
30
28
|
]
|
|
31
29
|
|
|
32
30
|
[project.optional-dependencies]
|
|
33
|
-
sqlalchemy = [
|
|
34
|
-
|
|
31
|
+
sqlalchemy = [
|
|
32
|
+
"sqlalchemy[asyncio]>=2.1,<3.0",
|
|
33
|
+
"typing-extensions>=4.10.0",
|
|
34
|
+
]
|
|
35
|
+
fastapi = ["fastapi>=0.150.0,<1.0"]
|
|
35
36
|
|
|
36
37
|
[project.urls]
|
|
37
38
|
Homepage = "https://github.com/wskr00/pyrsql"
|
|
@@ -47,23 +48,24 @@ dev = [
|
|
|
47
48
|
{ include-group = "docs" },
|
|
48
49
|
]
|
|
49
50
|
lint = [
|
|
50
|
-
"ruff>=0.
|
|
51
|
+
"ruff>=0.15.21",
|
|
51
52
|
]
|
|
52
53
|
test = [
|
|
53
|
-
"pytest>=
|
|
54
|
-
"httpx>=0.
|
|
55
|
-
"pytest-cov>=
|
|
56
|
-
"aiosqlite>=0.
|
|
57
|
-
"sqlalchemy[asyncio]>=2.
|
|
54
|
+
"pytest>=9.1.1",
|
|
55
|
+
"httpx>=0.28.1",
|
|
56
|
+
"pytest-cov>=7.1.0",
|
|
57
|
+
"aiosqlite>=0.21.0,<0.22.0",
|
|
58
|
+
"sqlalchemy[asyncio]>=2.1,<3.0",
|
|
59
|
+
"typing-extensions>=4.10.0",
|
|
58
60
|
]
|
|
59
|
-
typing = ["mypy>=2.
|
|
61
|
+
typing = ["mypy>=2.2.0"]
|
|
60
62
|
docs = [
|
|
61
|
-
"mkdocs-material>=9.
|
|
62
|
-
"mkdocstrings[python]>=0.
|
|
63
|
+
"mkdocs-material>=9.7.6",
|
|
64
|
+
"mkdocstrings[python]>=1.0.5",
|
|
63
65
|
]
|
|
64
66
|
|
|
65
67
|
[build-system]
|
|
66
|
-
requires = ["uv_build>=0.
|
|
68
|
+
requires = ["uv_build>=0.13.1,<0.14.0"]
|
|
67
69
|
build-backend = "uv_build"
|
|
68
70
|
|
|
69
71
|
[tool.uv]
|
|
@@ -71,7 +73,7 @@ managed = true
|
|
|
71
73
|
|
|
72
74
|
[tool.ruff]
|
|
73
75
|
line-length = 80
|
|
74
|
-
target-version = "
|
|
76
|
+
target-version = "py311"
|
|
75
77
|
src = ["src", "tests"]
|
|
76
78
|
fix = true
|
|
77
79
|
|
|
@@ -192,7 +194,7 @@ markers = [
|
|
|
192
194
|
]
|
|
193
195
|
|
|
194
196
|
[tool.mypy]
|
|
195
|
-
python_version = "3.
|
|
197
|
+
python_version = "3.11"
|
|
196
198
|
files = ["src"]
|
|
197
199
|
exclude = ["^tests/"]
|
|
198
200
|
strict = true
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
|
-
from enum import
|
|
6
|
+
from enum import StrEnum
|
|
7
7
|
import re
|
|
8
8
|
from types import MappingProxyType
|
|
9
9
|
from typing import TypeAlias
|
|
@@ -14,7 +14,7 @@ _SQL_IDENTIFIER_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
|
14
14
|
SortFieldTypeMap: TypeAlias = Mapping[str, "JSONSortScalarType"]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class JSONSortScalarType(
|
|
17
|
+
class JSONSortScalarType(StrEnum):
|
|
18
18
|
"""Supported scalar cast strategies for JSON sort expressions."""
|
|
19
19
|
|
|
20
20
|
TEXT = "text"
|
|
@@ -490,8 +490,8 @@ class SQLAlchemyExpressionTranslator:
|
|
|
490
490
|
if options.strict_equality:
|
|
491
491
|
return expression == value
|
|
492
492
|
|
|
493
|
-
ignore_case =
|
|
494
|
-
normalized_value = value
|
|
493
|
+
ignore_case = "^" in value
|
|
494
|
+
normalized_value = value.replace("^", "") if ignore_case else value
|
|
495
495
|
if "*" in normalized_value:
|
|
496
496
|
pattern = normalized_value.replace("*", "%")
|
|
497
497
|
return self._build_pattern_predicate(
|
|
@@ -22,18 +22,12 @@ def infer_sql_function_python_type(
|
|
|
22
22
|
) -> type[Any] | None:
|
|
23
23
|
"""Infers the Python type for one SQL function expression.
|
|
24
24
|
|
|
25
|
-
This helper
|
|
26
|
-
|
|
27
|
-
a small set of well-known functions.
|
|
25
|
+
This helper prefers explicit return types for known functions and argument
|
|
26
|
+
types for ``coalesce``, then asks SQLAlchemy for the expression type.
|
|
28
27
|
|
|
29
28
|
Returns:
|
|
30
29
|
The inferred Python type, or ``None`` when unknown.
|
|
31
30
|
"""
|
|
32
|
-
if function_expression is not None:
|
|
33
|
-
try:
|
|
34
|
-
return function_expression.type.python_type
|
|
35
|
-
except (AttributeError, NotImplementedError):
|
|
36
|
-
pass
|
|
37
31
|
normalized_name = function_name.lower()
|
|
38
32
|
known_return_type = _KNOWN_FUNCTION_RETURN_TYPES.get(normalized_name)
|
|
39
33
|
if known_return_type is not None:
|
|
@@ -42,6 +36,13 @@ def infer_sql_function_python_type(
|
|
|
42
36
|
for argument_type in argument_types:
|
|
43
37
|
if argument_type is not None:
|
|
44
38
|
return argument_type
|
|
39
|
+
if function_expression is not None:
|
|
40
|
+
try:
|
|
41
|
+
inferred_type = function_expression.type.python_type
|
|
42
|
+
except (AttributeError, NotImplementedError):
|
|
43
|
+
pass
|
|
44
|
+
else:
|
|
45
|
+
return None if inferred_type is object else inferred_type
|
|
45
46
|
return None
|
|
46
47
|
|
|
47
48
|
|
|
@@ -84,13 +84,13 @@ class Lexer:
|
|
|
84
84
|
return self._single_char_token(TokenKind.COMMA)
|
|
85
85
|
case ";":
|
|
86
86
|
return self._single_char_token(TokenKind.SEMICOLON)
|
|
87
|
-
case "'":
|
|
88
|
-
return self._consume_quoted_text()
|
|
87
|
+
case "'" | '"':
|
|
88
|
+
return self._consume_quoted_text(current_char)
|
|
89
89
|
case _:
|
|
90
90
|
return self._consume_unquoted_text()
|
|
91
91
|
|
|
92
|
-
def _consume_quoted_text(self) -> Token:
|
|
93
|
-
"""Consumes a single-quoted text token.
|
|
92
|
+
def _consume_quoted_text(self, quote_character: str) -> Token:
|
|
93
|
+
"""Consumes a single- or double-quoted text token.
|
|
94
94
|
|
|
95
95
|
Returns:
|
|
96
96
|
The consumed quoted token.
|
|
@@ -103,7 +103,7 @@ class Lexer:
|
|
|
103
103
|
characters: list[str] = []
|
|
104
104
|
while not self._is_at_end():
|
|
105
105
|
current_char = self._advance()
|
|
106
|
-
if current_char ==
|
|
106
|
+
if current_char == quote_character:
|
|
107
107
|
lexeme = "".join(characters)
|
|
108
108
|
self._enforce_argument_length(lexeme, start)
|
|
109
109
|
return self._make_token(TokenKind.QUOTED_TEXT, lexeme, start)
|
|
@@ -132,7 +132,7 @@ class Lexer:
|
|
|
132
132
|
current_char = self._peek()
|
|
133
133
|
if current_char.isspace():
|
|
134
134
|
break
|
|
135
|
-
if current_char in "(),;":
|
|
135
|
+
if current_char in "(),;'\"!~<>":
|
|
136
136
|
break
|
|
137
137
|
if self._match_operator_spelling(current_char) is not None:
|
|
138
138
|
break
|