pypaginate 0.2.0__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.
- pypaginate-0.2.0/.gitignore +95 -0
- pypaginate-0.2.0/CHANGELOG.md +148 -0
- pypaginate-0.2.0/LICENSE +21 -0
- pypaginate-0.2.0/PKG-INFO +416 -0
- pypaginate-0.2.0/README.md +368 -0
- pypaginate-0.2.0/docs/ARCHITECTURE.md +301 -0
- pypaginate-0.2.0/docs/CODE_OF_CONDUCT.md +65 -0
- pypaginate-0.2.0/docs/FEATURE_GAP_ANALYSIS.md +527 -0
- pypaginate-0.2.0/docs/OPTIMIZATION_AUDIT.md +516 -0
- pypaginate-0.2.0/docs/README.md +71 -0
- pypaginate-0.2.0/docs/TESTING.md +318 -0
- pypaginate-0.2.0/docs/_static/custom.css +122 -0
- pypaginate-0.2.0/docs/_static/custom.js +33 -0
- pypaginate-0.2.0/docs/_static/favicon.svg +21 -0
- pypaginate-0.2.0/docs/_static/logo.svg +23 -0
- pypaginate-0.2.0/docs/_templates/versions.html +66 -0
- pypaginate-0.2.0/docs/api/overview.md +265 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/fastapi/dependencies/index.rst +52 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/fastapi/filters/index.rst +76 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/fastapi/index.rst +153 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/fastapi/search/index.rst +52 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/fastapi/sorting/index.rst +52 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/index.rst +22 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/memory/backend/index.rst +62 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/memory/filters/index.rst +42 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/memory/index.rst +126 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/memory/search/index.rst +44 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/memory/sorting/index.rst +44 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/backend/index.rst +78 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/columns/index.rst +40 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/cursor/index.rst +79 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/cursor_codec/index.rst +46 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/filters/index.rst +43 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/index.rst +208 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/keyset/index.rst +87 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/search/index.rst +46 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/sorting/index.rst +44 -0
- pypaginate-0.2.0/docs/api/pypaginate/adapters/sqlalchemy/types/index.rst +27 -0
- pypaginate-0.2.0/docs/api/pypaginate/domain/enums/index.rst +78 -0
- pypaginate-0.2.0/docs/api/pypaginate/domain/exceptions/index.rst +96 -0
- pypaginate-0.2.0/docs/api/pypaginate/domain/fast_pages/index.rst +80 -0
- pypaginate-0.2.0/docs/api/pypaginate/domain/index.rst +26 -0
- pypaginate-0.2.0/docs/api/pypaginate/domain/pages/index.rst +93 -0
- pypaginate-0.2.0/docs/api/pypaginate/domain/params/index.rst +96 -0
- pypaginate-0.2.0/docs/api/pypaginate/domain/protocols/index.rst +137 -0
- pypaginate-0.2.0/docs/api/pypaginate/domain/specs/index.rst +143 -0
- pypaginate-0.2.0/docs/api/pypaginate/engine/cursor/index.rst +45 -0
- pypaginate-0.2.0/docs/api/pypaginate/engine/index.rst +22 -0
- pypaginate-0.2.0/docs/api/pypaginate/engine/paginator/index.rst +65 -0
- pypaginate-0.2.0/docs/api/pypaginate/engine/pipeline/index.rst +72 -0
- pypaginate-0.2.0/docs/api/pypaginate/filtering/accessor/index.rst +45 -0
- pypaginate-0.2.0/docs/api/pypaginate/filtering/engine/index.rst +43 -0
- pypaginate-0.2.0/docs/api/pypaginate/filtering/index.rst +96 -0
- pypaginate-0.2.0/docs/api/pypaginate/filtering/like/index.rst +53 -0
- pypaginate-0.2.0/docs/api/pypaginate/filtering/operators/index.rst +195 -0
- pypaginate-0.2.0/docs/api/pypaginate/filtering/regex/index.rst +36 -0
- pypaginate-0.2.0/docs/api/pypaginate/filtering/registry/index.rst +66 -0
- pypaginate-0.2.0/docs/api/pypaginate/index.rst +401 -0
- pypaginate-0.2.0/docs/api/pypaginate/search/engine/index.rst +36 -0
- pypaginate-0.2.0/docs/api/pypaginate/search/index.rst +44 -0
- pypaginate-0.2.0/docs/api/pypaginate/search/matching/index.rst +46 -0
- pypaginate-0.2.0/docs/api/pypaginate/search/parser/index.rst +53 -0
- pypaginate-0.2.0/docs/api/pypaginate/sorting/engine/index.rst +47 -0
- pypaginate-0.2.0/docs/api/pypaginate/sorting/index.rst +53 -0
- pypaginate-0.2.0/docs/api/pypaginate/sorting/keys/index.rst +41 -0
- pypaginate-0.2.0/docs/api/pypaginate/text/index.rst +20 -0
- pypaginate-0.2.0/docs/api/pypaginate/text/normalize/index.rst +50 -0
- pypaginate-0.2.0/docs/comparison.md +199 -0
- pypaginate-0.2.0/docs/concepts/architecture.md +320 -0
- pypaginate-0.2.0/docs/concepts/cursor-encoding.md +145 -0
- pypaginate-0.2.0/docs/concepts/filter-expressions.md +249 -0
- pypaginate-0.2.0/docs/concepts/index.md +107 -0
- pypaginate-0.2.0/docs/concepts/pagination-strategies.md +205 -0
- pypaginate-0.2.0/docs/concepts/search-relevance.md +239 -0
- pypaginate-0.2.0/docs/conf.py +423 -0
- pypaginate-0.2.0/docs/contributing/architecture.md +190 -0
- pypaginate-0.2.0/docs/contributing/code-style.md +270 -0
- pypaginate-0.2.0/docs/contributing/development.md +213 -0
- pypaginate-0.2.0/docs/contributing/index.md +130 -0
- pypaginate-0.2.0/docs/contributing/roadmap.md +158 -0
- pypaginate-0.2.0/docs/contributing/testing.md +220 -0
- pypaginate-0.2.0/docs/examples/basic-pagination.md +228 -0
- pypaginate-0.2.0/docs/examples/fastapi.md +315 -0
- pypaginate-0.2.0/docs/examples/filtering.md +181 -0
- pypaginate-0.2.0/docs/examples/index.md +54 -0
- pypaginate-0.2.0/docs/examples/keyset.md +254 -0
- pypaginate-0.2.0/docs/filtering/basic.md +242 -0
- pypaginate-0.2.0/docs/filtering/index.md +98 -0
- pypaginate-0.2.0/docs/filtering/json-logic.md +205 -0
- pypaginate-0.2.0/docs/filtering/operators.md +264 -0
- pypaginate-0.2.0/docs/getting-started/first-steps.md +160 -0
- pypaginate-0.2.0/docs/getting-started/index.md +64 -0
- pypaginate-0.2.0/docs/getting-started/installation.md +231 -0
- pypaginate-0.2.0/docs/getting-started/quickstart.md +118 -0
- pypaginate-0.2.0/docs/index.md +315 -0
- pypaginate-0.2.0/docs/integrations/fastapi.md +371 -0
- pypaginate-0.2.0/docs/integrations/index.md +90 -0
- pypaginate-0.2.0/docs/integrations/sqlalchemy.md +392 -0
- pypaginate-0.2.0/docs/pagination/index.md +164 -0
- pypaginate-0.2.0/docs/pagination/keyset.md +243 -0
- pypaginate-0.2.0/docs/pagination/memory.md +204 -0
- pypaginate-0.2.0/docs/pagination/offset.md +281 -0
- pypaginate-0.2.0/docs/poly.py +276 -0
- pypaginate-0.2.0/docs/search/fuzzy.md +273 -0
- pypaginate-0.2.0/docs/search/index.md +104 -0
- pypaginate-0.2.0/docs/search/text-search.md +238 -0
- pypaginate-0.2.0/docs/sorting/basic.md +186 -0
- pypaginate-0.2.0/docs/sorting/index.md +93 -0
- pypaginate-0.2.0/docs/sorting/multi-column.md +228 -0
- pypaginate-0.2.0/examples/README.md +56 -0
- pypaginate-0.2.0/examples/basic_pagination.py +46 -0
- pypaginate-0.2.0/examples/fastapi_integration.py +113 -0
- pypaginate-0.2.0/examples/filtering.py +67 -0
- pypaginate-0.2.0/examples/keyset_pagination.py +99 -0
- pypaginate-0.2.0/pyproject.toml +504 -0
- pypaginate-0.2.0/src/pypaginate/__init__.py +71 -0
- pypaginate-0.2.0/src/pypaginate/_cli/__init__.py +96 -0
- pypaginate-0.2.0/src/pypaginate/_cli/build.py +96 -0
- pypaginate-0.2.0/src/pypaginate/_cli/commands.py +171 -0
- pypaginate-0.2.0/src/pypaginate/_cli/output.py +91 -0
- pypaginate-0.2.0/src/pypaginate/_cli/runner.py +50 -0
- pypaginate-0.2.0/src/pypaginate/_detection.py +51 -0
- pypaginate-0.2.0/src/pypaginate/_dispatch.py +208 -0
- pypaginate-0.2.0/src/pypaginate/adapters/__init__.py +3 -0
- pypaginate-0.2.0/src/pypaginate/adapters/fastapi/__init__.py +25 -0
- pypaginate-0.2.0/src/pypaginate/adapters/fastapi/dependencies.py +67 -0
- pypaginate-0.2.0/src/pypaginate/adapters/fastapi/filters.py +75 -0
- pypaginate-0.2.0/src/pypaginate/adapters/fastapi/search.py +41 -0
- pypaginate-0.2.0/src/pypaginate/adapters/fastapi/sorting.py +48 -0
- pypaginate-0.2.0/src/pypaginate/adapters/memory/__init__.py +16 -0
- pypaginate-0.2.0/src/pypaginate/adapters/memory/backend.py +69 -0
- pypaginate-0.2.0/src/pypaginate/adapters/memory/filters.py +118 -0
- pypaginate-0.2.0/src/pypaginate/adapters/memory/search.py +171 -0
- pypaginate-0.2.0/src/pypaginate/adapters/memory/sorting.py +84 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/__init__.py +26 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/backend.py +144 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/columns.py +85 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/cursor.py +305 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/filters.py +192 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/keyset.py +187 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/search.py +132 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/sorting.py +94 -0
- pypaginate-0.2.0/src/pypaginate/adapters/sqlalchemy/types.py +21 -0
- pypaginate-0.2.0/src/pypaginate/domain/__init__.py +3 -0
- pypaginate-0.2.0/src/pypaginate/domain/enums.py +63 -0
- pypaginate-0.2.0/src/pypaginate/domain/exceptions.py +102 -0
- pypaginate-0.2.0/src/pypaginate/domain/fast_pages.py +93 -0
- pypaginate-0.2.0/src/pypaginate/domain/pages.py +152 -0
- pypaginate-0.2.0/src/pypaginate/domain/params.py +114 -0
- pypaginate-0.2.0/src/pypaginate/domain/protocols.py +120 -0
- pypaginate-0.2.0/src/pypaginate/domain/specs.py +171 -0
- pypaginate-0.2.0/src/pypaginate/engine/__init__.py +3 -0
- pypaginate-0.2.0/src/pypaginate/engine/cursor.py +54 -0
- pypaginate-0.2.0/src/pypaginate/engine/cursor_codec.py +108 -0
- pypaginate-0.2.0/src/pypaginate/engine/paginator.py +109 -0
- pypaginate-0.2.0/src/pypaginate/engine/pipeline.py +167 -0
- pypaginate-0.2.0/src/pypaginate/filtering/__init__.py +9 -0
- pypaginate-0.2.0/src/pypaginate/filtering/accessor.py +130 -0
- pypaginate-0.2.0/src/pypaginate/filtering/engine.py +208 -0
- pypaginate-0.2.0/src/pypaginate/filtering/like.py +70 -0
- pypaginate-0.2.0/src/pypaginate/filtering/operators.py +228 -0
- pypaginate-0.2.0/src/pypaginate/filtering/regex.py +44 -0
- pypaginate-0.2.0/src/pypaginate/filtering/registry.py +112 -0
- pypaginate-0.2.0/src/pypaginate/py.typed +0 -0
- pypaginate-0.2.0/src/pypaginate/search/__init__.py +8 -0
- pypaginate-0.2.0/src/pypaginate/search/engine.py +217 -0
- pypaginate-0.2.0/src/pypaginate/search/matching.py +78 -0
- pypaginate-0.2.0/src/pypaginate/search/parser.py +64 -0
- pypaginate-0.2.0/src/pypaginate/sorting/__init__.py +8 -0
- pypaginate-0.2.0/src/pypaginate/sorting/engine.py +78 -0
- pypaginate-0.2.0/src/pypaginate/sorting/keys.py +70 -0
- pypaginate-0.2.0/src/pypaginate/text/__init__.py +3 -0
- pypaginate-0.2.0/src/pypaginate/text/normalize.py +64 -0
- pypaginate-0.2.0/tests/__init__.py +1 -0
- pypaginate-0.2.0/tests/architecture/__init__.py +0 -0
- pypaginate-0.2.0/tests/architecture/test_file_limits.py +61 -0
- pypaginate-0.2.0/tests/architecture/test_imports.py +86 -0
- pypaginate-0.2.0/tests/architecture/test_protocols.py +56 -0
- pypaginate-0.2.0/tests/conftest.py +171 -0
- pypaginate-0.2.0/tests/e2e/__init__.py +3 -0
- pypaginate-0.2.0/tests/e2e/conftest.py +6 -0
- pypaginate-0.2.0/tests/e2e/test_combined_flows.py +76 -0
- pypaginate-0.2.0/tests/e2e/test_completeness.py +63 -0
- pypaginate-0.2.0/tests/e2e/test_fastapi_flows.py +382 -0
- pypaginate-0.2.0/tests/e2e/test_filter_flows.py +72 -0
- pypaginate-0.2.0/tests/e2e/test_offset_flows.py +66 -0
- pypaginate-0.2.0/tests/e2e/test_sort_flows.py +55 -0
- pypaginate-0.2.0/tests/factories/__init__.py +32 -0
- pypaginate-0.2.0/tests/factories/data.py +56 -0
- pypaginate-0.2.0/tests/factories/domain.py +115 -0
- pypaginate-0.2.0/tests/fixtures/__init__.py +9 -0
- pypaginate-0.2.0/tests/fixtures/backends.py +326 -0
- pypaginate-0.2.0/tests/fixtures/data.py +115 -0
- pypaginate-0.2.0/tests/fixtures/database.py +104 -0
- pypaginate-0.2.0/tests/fixtures/helpers.py +30 -0
- pypaginate-0.2.0/tests/fixtures/models.py +107 -0
- pypaginate-0.2.0/tests/integration/__init__.py +3 -0
- pypaginate-0.2.0/tests/integration/conftest.py +6 -0
- pypaginate-0.2.0/tests/integration/test_custom_backend.py +64 -0
- pypaginate-0.2.0/tests/integration/test_fastapi.py +471 -0
- pypaginate-0.2.0/tests/integration/test_filter_groups.py +113 -0
- pypaginate-0.2.0/tests/integration/test_filtering.py +111 -0
- pypaginate-0.2.0/tests/integration/test_pagination.py +94 -0
- pypaginate-0.2.0/tests/integration/test_pipeline.py +58 -0
- pypaginate-0.2.0/tests/integration/test_postgresql.py +184 -0
- pypaginate-0.2.0/tests/integration/test_search.py +52 -0
- pypaginate-0.2.0/tests/integration/test_smoke.py +33 -0
- pypaginate-0.2.0/tests/integration/test_sorting.py +83 -0
- pypaginate-0.2.0/tests/perf/__init__.py +1 -0
- pypaginate-0.2.0/tests/perf/conftest.py +257 -0
- pypaginate-0.2.0/tests/perf/test_boundary.py +179 -0
- pypaginate-0.2.0/tests/perf/test_comparison.py +197 -0
- pypaginate-0.2.0/tests/perf/test_competitor_scaling.py +206 -0
- pypaginate-0.2.0/tests/perf/test_competitor_scaling_sa.py +599 -0
- pypaginate-0.2.0/tests/perf/test_competitors.py +635 -0
- pypaginate-0.2.0/tests/perf/test_error_handling.py +323 -0
- pypaginate-0.2.0/tests/perf/test_fastapi_perf.py +1124 -0
- pypaginate-0.2.0/tests/perf/test_fastapi_scaling.py +449 -0
- pypaginate-0.2.0/tests/perf/test_filtering.py +168 -0
- pypaginate-0.2.0/tests/perf/test_overhead.py +383 -0
- pypaginate-0.2.0/tests/perf/test_pagination.py +199 -0
- pypaginate-0.2.0/tests/perf/test_pipeline.py +215 -0
- pypaginate-0.2.0/tests/perf/test_scaling.py +310 -0
- pypaginate-0.2.0/tests/perf/test_search.py +142 -0
- pypaginate-0.2.0/tests/perf/test_serialization.py +391 -0
- pypaginate-0.2.0/tests/perf/test_sorting.py +143 -0
- pypaginate-0.2.0/tests/property/__init__.py +1 -0
- pypaginate-0.2.0/tests/property/conftest.py +53 -0
- pypaginate-0.2.0/tests/property/strategies.py +80 -0
- pypaginate-0.2.0/tests/property/test_filtering.py +72 -0
- pypaginate-0.2.0/tests/property/test_pagination.py +94 -0
- pypaginate-0.2.0/tests/property/test_search.py +92 -0
- pypaginate-0.2.0/tests/property/test_sorting.py +78 -0
- pypaginate-0.2.0/tests/unit/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/adapters/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/adapters/fastapi/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/adapters/fastapi/test_dependencies.py +200 -0
- pypaginate-0.2.0/tests/unit/adapters/fastapi/test_filter_dep.py +71 -0
- pypaginate-0.2.0/tests/unit/adapters/fastapi/test_search_dep.py +38 -0
- pypaginate-0.2.0/tests/unit/adapters/fastapi/test_sort_dep.py +50 -0
- pypaginate-0.2.0/tests/unit/adapters/memory/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/adapters/memory/test_backend.py +92 -0
- pypaginate-0.2.0/tests/unit/adapters/memory/test_filters.py +84 -0
- pypaginate-0.2.0/tests/unit/adapters/memory/test_search.py +111 -0
- pypaginate-0.2.0/tests/unit/adapters/memory/test_search_advanced.py +74 -0
- pypaginate-0.2.0/tests/unit/adapters/memory/test_sorting.py +63 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/conftest.py +115 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_backend.py +136 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_columns.py +51 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_cursor.py +233 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_cursor_codec.py +121 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_filters.py +222 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_filters_db.py +106 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_keyset.py +196 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_search.py +216 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_sorting.py +179 -0
- pypaginate-0.2.0/tests/unit/adapters/sqlalchemy/test_types.py +15 -0
- pypaginate-0.2.0/tests/unit/conftest.py +43 -0
- pypaginate-0.2.0/tests/unit/domain/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/domain/test_enums.py +59 -0
- pypaginate-0.2.0/tests/unit/domain/test_exceptions.py +114 -0
- pypaginate-0.2.0/tests/unit/domain/test_fast_pages.py +306 -0
- pypaginate-0.2.0/tests/unit/domain/test_models.py +86 -0
- pypaginate-0.2.0/tests/unit/domain/test_pages.py +203 -0
- pypaginate-0.2.0/tests/unit/domain/test_params.py +146 -0
- pypaginate-0.2.0/tests/unit/domain/test_protocols.py +124 -0
- pypaginate-0.2.0/tests/unit/domain/test_specs.py +136 -0
- pypaginate-0.2.0/tests/unit/engine/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/engine/test_cursor.py +144 -0
- pypaginate-0.2.0/tests/unit/engine/test_dispatch.py +213 -0
- pypaginate-0.2.0/tests/unit/engine/test_paginator.py +302 -0
- pypaginate-0.2.0/tests/unit/engine/test_pipeline.py +285 -0
- pypaginate-0.2.0/tests/unit/filtering/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/filtering/test_accessor.py +105 -0
- pypaginate-0.2.0/tests/unit/filtering/test_engine.py +193 -0
- pypaginate-0.2.0/tests/unit/filtering/test_engine_like.py +179 -0
- pypaginate-0.2.0/tests/unit/filtering/test_groups.py +151 -0
- pypaginate-0.2.0/tests/unit/filtering/test_like.py +179 -0
- pypaginate-0.2.0/tests/unit/filtering/test_operators.py +212 -0
- pypaginate-0.2.0/tests/unit/filtering/test_registry.py +93 -0
- pypaginate-0.2.0/tests/unit/search/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/search/test_engine.py +187 -0
- pypaginate-0.2.0/tests/unit/search/test_matching.py +137 -0
- pypaginate-0.2.0/tests/unit/search/test_parser.py +98 -0
- pypaginate-0.2.0/tests/unit/sorting/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/sorting/test_engine.py +185 -0
- pypaginate-0.2.0/tests/unit/sorting/test_keys.py +90 -0
- pypaginate-0.2.0/tests/unit/test_public_api.py +17 -0
- pypaginate-0.2.0/tests/unit/text/__init__.py +0 -0
- pypaginate-0.2.0/tests/unit/text/test_normalize.py +88 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# UV
|
|
29
|
+
.uv/
|
|
30
|
+
|
|
31
|
+
# IDE
|
|
32
|
+
.idea/
|
|
33
|
+
.vscode/
|
|
34
|
+
*.swp
|
|
35
|
+
*.swo
|
|
36
|
+
*~
|
|
37
|
+
.project
|
|
38
|
+
.pydevproject
|
|
39
|
+
.settings/
|
|
40
|
+
|
|
41
|
+
# Testing
|
|
42
|
+
.pytest_cache/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
htmlcov/
|
|
46
|
+
.tox/
|
|
47
|
+
.nox/
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
|
|
53
|
+
# Type checking
|
|
54
|
+
.mypy_cache/
|
|
55
|
+
.dmypy.json
|
|
56
|
+
dmypy.json
|
|
57
|
+
|
|
58
|
+
# Linting
|
|
59
|
+
.ruff_cache/
|
|
60
|
+
|
|
61
|
+
# Documentation
|
|
62
|
+
docs/_build/
|
|
63
|
+
docs-sphinx/_build/
|
|
64
|
+
site/
|
|
65
|
+
v1docs/
|
|
66
|
+
|
|
67
|
+
# Jupyter
|
|
68
|
+
.ipynb_checkpoints/
|
|
69
|
+
|
|
70
|
+
# OS
|
|
71
|
+
.DS_Store
|
|
72
|
+
Thumbs.db
|
|
73
|
+
|
|
74
|
+
# Local configuration
|
|
75
|
+
.env
|
|
76
|
+
.env.local
|
|
77
|
+
*.local
|
|
78
|
+
|
|
79
|
+
# Node.js (MCP servers)
|
|
80
|
+
node_modules/
|
|
81
|
+
|
|
82
|
+
# Build artifacts
|
|
83
|
+
*.manifest
|
|
84
|
+
*.spec
|
|
85
|
+
|
|
86
|
+
# Session artifacts (AI-generated planning docs)
|
|
87
|
+
REFACTORING_PLAN.md
|
|
88
|
+
REFACTORING_ROADMAP.md
|
|
89
|
+
AUDIT_IMPLEMENTATION_PLAN.md
|
|
90
|
+
AUDIT_R2_IMPLEMENTATION_PLAN.md
|
|
91
|
+
DOCS_REFACTOR_PLAN.md
|
|
92
|
+
|
|
93
|
+
# Installer logs
|
|
94
|
+
pip-log.txt
|
|
95
|
+
pip-delete-this-directory.txt
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Planned for v0.3.0
|
|
11
|
+
- JSON Logic dict-to-FilterGroup parser for frontend integration
|
|
12
|
+
- Django `__` filter format parser
|
|
13
|
+
- `add_pagination(app)` zero-config FastAPI middleware
|
|
14
|
+
- HATEOAS link generation
|
|
15
|
+
- Additional ORM support (Beanie, Tortoise)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2025-XX-XX
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
#### Architecture
|
|
24
|
+
- Hexagonal architecture with domain/engine/adapter layers
|
|
25
|
+
- Protocol-based backends (`PaginationBackend`, `CursorBackend`, `FilterBackend`, `SortBackend`, `SearchBackend`)
|
|
26
|
+
- Universal `paginate()` entry point with Elysia-style type inference
|
|
27
|
+
- `SyncPipeline` and `AsyncPipeline` for composing filter + sort + search + paginate
|
|
28
|
+
|
|
29
|
+
#### Pagination
|
|
30
|
+
- `OffsetParams` / `OffsetPage` (replaces `PageParams` / `Page`)
|
|
31
|
+
- `CursorParams` / `CursorPage` for cursor/keyset pagination
|
|
32
|
+
- `OverflowStrategy` (EMPTY or CLAMP) for out-of-range pages
|
|
33
|
+
- Custom count query via `SQLAlchemyBackend(session, count_query=...)`
|
|
34
|
+
- Row deduplication via `SQLAlchemyBackend(session, unique=True)`
|
|
35
|
+
- `SyncSQLAlchemyBackend` and `SyncSQLAlchemyCursorBackend` for sync sessions
|
|
36
|
+
- Fast in-memory path (no backend allocation for list + OffsetParams)
|
|
37
|
+
|
|
38
|
+
#### Filtering
|
|
39
|
+
- 20 operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, starts_with, ends_with, like, ilike, between, is_null, is_not_null, regex, empty, not_empty, exists
|
|
40
|
+
- `FilterGroup` with `And()` / `Or()` builders for nested boolean groups (up to 5 levels)
|
|
41
|
+
- Compiled predicate closures (compile once, evaluate N times)
|
|
42
|
+
- `SQLAlchemyFilterBackend` for SQL WHERE clause generation
|
|
43
|
+
- `OperatorRegistry` for extensible operator lookup
|
|
44
|
+
|
|
45
|
+
#### Search
|
|
46
|
+
- `SearchSpec` with `weights`, `fuzzy`, `threshold`, `min_length`, `max_results`
|
|
47
|
+
- `FuzzyMode.EXACT`, `FuzzyMode.FUZZY` (partial_ratio), `FuzzyMode.TOKEN_SORT` (token_sort_ratio)
|
|
48
|
+
- `SearchFieldMode.EXACT`, `PREFIX`, `CONTAINS`
|
|
49
|
+
- Unicode normalization with accent removal
|
|
50
|
+
- `SQLAlchemySearchBackend` for SQL LIKE/ILIKE search
|
|
51
|
+
|
|
52
|
+
#### Sorting
|
|
53
|
+
- `SortSpec` with `direction` and `nulls` (NullsPosition.FIRST / LAST)
|
|
54
|
+
- Multi-key stable sorting
|
|
55
|
+
- `SQLAlchemySortBackend` for SQL ORDER BY
|
|
56
|
+
|
|
57
|
+
#### FastAPI Integration
|
|
58
|
+
- `OffsetDep`, `CursorDep` (Annotated dependency types)
|
|
59
|
+
- `FilterDep` with `FilterField()` for declarative filters
|
|
60
|
+
- `SortDep` for `?sort=name,-age` query parsing
|
|
61
|
+
- `SearchDep` for `?q=alice&search_fields=name,email` query parsing
|
|
62
|
+
|
|
63
|
+
#### Performance
|
|
64
|
+
- msgspec fast page construction (`pypaginate[fast]`)
|
|
65
|
+
- Compiled field accessors, pre-normalized search tokens
|
|
66
|
+
- `__slots__` on all stateful classes
|
|
67
|
+
- Optional google-re2 for ReDoS safety (`pypaginate[security]`)
|
|
68
|
+
- LIKE pattern string method dispatch (bypasses fnmatch/regex for common patterns)
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
- Renamed `PageParams` to `OffsetParams`, `Page` to `OffsetPage`
|
|
72
|
+
- Replaced `paginate_entities()` / `paginate_rows()` with universal `paginate()`
|
|
73
|
+
- Replaced JSON Logic dict filters with typed `FilterSpec` / `FilterGroup`
|
|
74
|
+
- Replaced `SearchOptions` with `SearchSpec`
|
|
75
|
+
- Moved `FilterEngine` to `pypaginate.filtering.engine`
|
|
76
|
+
- Moved `SortEngine` to `pypaginate.sorting.engine`
|
|
77
|
+
- FastAPI deps use `Annotated` types instead of function-based `Depends`
|
|
78
|
+
|
|
79
|
+
### Removed
|
|
80
|
+
- `SqlPaginator`, `MemoryPaginator` (replaced by protocol backends)
|
|
81
|
+
- `PaginationSnapshot` (direct page return)
|
|
82
|
+
- `MemorySearchService`, `SqlSearchService` (replaced by `SearchEngine` + backends)
|
|
83
|
+
- `SqlSortAdapter`, `SqlFilterAdapter` (replaced by SA backends)
|
|
84
|
+
- `get_pagination_params()`, `PagedResponse` (replaced by `OffsetDep`, `OffsetPage`)
|
|
85
|
+
- JSON Logic dict format (replaced by typed `FilterGroup`)
|
|
86
|
+
- JMESPath array access (replaced by dot notation)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## [0.1.0] - 2025-01-30
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
#### Core Pagination
|
|
95
|
+
- `Page[T]` generic response model with metadata (total, page, limit, pages)
|
|
96
|
+
- `PageParams` dataclass for pagination parameters
|
|
97
|
+
- Offset-based pagination with configurable page size
|
|
98
|
+
- Keyset (cursor-based) pagination for large datasets using `sqlakeyset`
|
|
99
|
+
|
|
100
|
+
#### Pagination Engines
|
|
101
|
+
- `SqlPaginator` - SQLAlchemy-based pagination engine
|
|
102
|
+
- `MemoryPaginator` - In-memory pagination for Python collections
|
|
103
|
+
- `paginate_entities()` - High-level async pagination API
|
|
104
|
+
|
|
105
|
+
#### Filtering
|
|
106
|
+
- `FilterEngine` with JSON Logic support for complex queries
|
|
107
|
+
- Predicate-based filtering system
|
|
108
|
+
- Support for operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `like`, `ilike`, `is_null`, `startswith`, `endswith`
|
|
109
|
+
- Logical operators: `and`, `or`, `not`
|
|
110
|
+
|
|
111
|
+
#### Search
|
|
112
|
+
- `SqlSearchService` for full-text search
|
|
113
|
+
- Fuzzy matching with RapidFuzz integration
|
|
114
|
+
- Configurable similarity thresholds
|
|
115
|
+
- Accent-insensitive search option
|
|
116
|
+
- Multi-field search support
|
|
117
|
+
|
|
118
|
+
#### Sorting
|
|
119
|
+
- `SortEngine` for sort operations
|
|
120
|
+
- `SqlSortAdapter` for SQLAlchemy integration
|
|
121
|
+
- Multi-column sorting support
|
|
122
|
+
- Ascending/descending order
|
|
123
|
+
|
|
124
|
+
#### FastAPI Integration
|
|
125
|
+
- `get_pagination_params()` dependency for FastAPI
|
|
126
|
+
- `PagedResponse` Pydantic model for OpenAPI documentation
|
|
127
|
+
- Type-safe parameter extraction from query strings
|
|
128
|
+
|
|
129
|
+
#### Developer Experience
|
|
130
|
+
- Full type hints with mypy --strict compatibility
|
|
131
|
+
- Comprehensive docstrings
|
|
132
|
+
- Async/await support throughout
|
|
133
|
+
|
|
134
|
+
### Technical Details
|
|
135
|
+
- Python 3.11+ required
|
|
136
|
+
- SQLAlchemy 2.0+ for database operations
|
|
137
|
+
- Pydantic v2 for data validation
|
|
138
|
+
- Optional RapidFuzz for fuzzy search
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Future Releases
|
|
143
|
+
|
|
144
|
+
See the [Roadmap](https://pypaginate.readthedocs.io/contributing/roadmap/) for detailed planning of future versions.
|
|
145
|
+
|
|
146
|
+
[Unreleased]: https://github.com/CybLow/pypaginate/compare/v0.2.0...HEAD
|
|
147
|
+
[0.2.0]: https://github.com/CybLow/pypaginate/compare/v0.1.0...v0.2.0
|
|
148
|
+
[0.1.0]: https://github.com/CybLow/pypaginate/releases/tag/v0.1.0
|
pypaginate-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 pypaginate 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.
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypaginate
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Universal pagination toolkit for Python — one function, any backend, auto-detects sync/async
|
|
5
|
+
Project-URL: Homepage, https://github.com/CybLow/pypaginate
|
|
6
|
+
Project-URL: Documentation, https://pypaginate.readthedocs.io
|
|
7
|
+
Project-URL: Repository, https://github.com/CybLow/pypaginate
|
|
8
|
+
Project-URL: Issues, https://github.com/CybLow/pypaginate/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/CybLow/pypaginate/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: CybLow <cyber_lolow@protonmail.com>
|
|
11
|
+
Maintainer-email: CybLow <cyber_lolow@protonmail.com>
|
|
12
|
+
License: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: async,cursor,database,fastapi,filtering,jsonlogic,keyset,orm,pagination,search,sqlalchemy
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Environment :: Web Environment
|
|
17
|
+
Classifier: Framework :: FastAPI
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Database
|
|
27
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.11
|
|
31
|
+
Requires-Dist: pydantic>=2.0.0
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: fastapi>=0.95.0; extra == 'all'
|
|
34
|
+
Requires-Dist: msgspec>=0.18.0; extra == 'all'
|
|
35
|
+
Requires-Dist: rapidfuzz>=3.0.0; extra == 'all'
|
|
36
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == 'all'
|
|
37
|
+
Provides-Extra: fast
|
|
38
|
+
Requires-Dist: msgspec>=0.18.0; extra == 'fast'
|
|
39
|
+
Provides-Extra: fastapi
|
|
40
|
+
Requires-Dist: fastapi>=0.95.0; extra == 'fastapi'
|
|
41
|
+
Provides-Extra: search
|
|
42
|
+
Requires-Dist: rapidfuzz>=3.0.0; extra == 'search'
|
|
43
|
+
Provides-Extra: security
|
|
44
|
+
Requires-Dist: google-re2>=1.0; extra == 'security'
|
|
45
|
+
Provides-Extra: sqlalchemy
|
|
46
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == 'sqlalchemy'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# pypaginate
|
|
50
|
+
|
|
51
|
+
**Universal pagination toolkit for Python -- one function, any backend, auto-detects sync/async.**
|
|
52
|
+
|
|
53
|
+
[](https://github.com/CybLow/pypaginate/actions/workflows/ci.yml)
|
|
54
|
+
[](https://pypi.org/project/pypaginate/)
|
|
55
|
+
[](https://pypi.org/project/pypaginate/)
|
|
56
|
+
[](https://opensource.org/licenses/MIT)
|
|
57
|
+
[](https://codecov.io/gh/CybLow/pypaginate)
|
|
58
|
+
[](https://github.com/astral-sh/ruff)
|
|
59
|
+
[](https://github.com/astral-sh/uv)
|
|
60
|
+
|
|
61
|
+
pypaginate provides a single `paginate()` function that works with lists, SQLAlchemy queries (async and sync), and cursor-based pagination. The return type is automatically inferred from the params you pass in.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **One function** -- `paginate()` handles lists, SQLAlchemy queries, sync and async
|
|
66
|
+
- **Type-safe inference** -- `OffsetParams` returns `OffsetPage`, `CursorParams` returns `CursorPage`
|
|
67
|
+
- **Filtering** -- 20 operators (eq, gte, contains, between, regex, etc.)
|
|
68
|
+
- **Sorting** -- multi-column with direction and null placement control
|
|
69
|
+
- **Search** -- full-text with optional fuzzy matching (RapidFuzz)
|
|
70
|
+
- **FastAPI** -- `Annotated` dependencies for pagination, filtering, sorting, and search
|
|
71
|
+
- **Cursor pagination** -- keyset/cursor-based pagination via sqlakeyset
|
|
72
|
+
- **Pipeline** -- compose filter + sort + search + paginate in one call
|
|
73
|
+
- **100% typed** -- mypy strict mode, Pydantic v2 models
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Core (in-memory pagination only)
|
|
79
|
+
pip install pypaginate
|
|
80
|
+
|
|
81
|
+
# With SQLAlchemy support
|
|
82
|
+
pip install pypaginate[sqlalchemy]
|
|
83
|
+
|
|
84
|
+
# With FastAPI integration
|
|
85
|
+
pip install pypaginate[fastapi]
|
|
86
|
+
|
|
87
|
+
# With fuzzy search (RapidFuzz)
|
|
88
|
+
pip install pypaginate[search]
|
|
89
|
+
|
|
90
|
+
# Everything
|
|
91
|
+
pip install pypaginate[all]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
uv add pypaginate
|
|
98
|
+
uv add pypaginate[all]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Quick Start
|
|
102
|
+
|
|
103
|
+
Paginate a list in 3 lines:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from pypaginate import paginate, OffsetParams
|
|
107
|
+
|
|
108
|
+
page = paginate([1, 2, 3, 4, 5], OffsetParams(page=1, limit=2))
|
|
109
|
+
|
|
110
|
+
page.items # [1, 2]
|
|
111
|
+
page.total # 5
|
|
112
|
+
page.pages # 3
|
|
113
|
+
page.has_next # True
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## SQLAlchemy (Async)
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from sqlalchemy import select
|
|
120
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
121
|
+
from pypaginate import paginate, OffsetParams
|
|
122
|
+
from pypaginate.adapters.sqlalchemy import SQLAlchemyBackend
|
|
123
|
+
|
|
124
|
+
async def list_users(session: AsyncSession):
|
|
125
|
+
stmt = select(User).order_by(User.created_at.desc())
|
|
126
|
+
backend = SQLAlchemyBackend(session)
|
|
127
|
+
|
|
128
|
+
page = await paginate(stmt, OffsetParams(page=1, limit=20), backend=backend)
|
|
129
|
+
|
|
130
|
+
page.items # list[User]
|
|
131
|
+
page.total # int
|
|
132
|
+
page.has_next # bool
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
For sync sessions, use `SyncSQLAlchemyBackend`:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from sqlalchemy.orm import Session
|
|
139
|
+
from pypaginate.adapters.sqlalchemy import SyncSQLAlchemyBackend
|
|
140
|
+
|
|
141
|
+
def list_users(session: Session):
|
|
142
|
+
backend = SyncSQLAlchemyBackend(session)
|
|
143
|
+
page = paginate(select(User), OffsetParams(page=1, limit=20), backend=backend)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Cursor Pagination
|
|
147
|
+
|
|
148
|
+
For large datasets where offset-based pagination is inefficient:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from pypaginate import paginate, CursorParams
|
|
152
|
+
from pypaginate.adapters.sqlalchemy import SQLAlchemyCursorBackend
|
|
153
|
+
|
|
154
|
+
async def scroll_users(session: AsyncSession, cursor: str | None = None):
|
|
155
|
+
stmt = select(User).order_by(User.id)
|
|
156
|
+
backend = SQLAlchemyCursorBackend(session)
|
|
157
|
+
|
|
158
|
+
page = await paginate(stmt, CursorParams(limit=20, after=cursor), backend=backend)
|
|
159
|
+
|
|
160
|
+
page.items # list[User]
|
|
161
|
+
page.next_cursor # str | None -- pass to next request
|
|
162
|
+
page.previous_cursor # str | None
|
|
163
|
+
page.has_next # bool
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## FastAPI Integration
|
|
167
|
+
|
|
168
|
+
pypaginate provides `Annotated` dependency types for clean FastAPI integration:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from fastapi import FastAPI
|
|
172
|
+
from pypaginate import paginate, OffsetPage
|
|
173
|
+
from pypaginate.adapters.fastapi import OffsetDep
|
|
174
|
+
|
|
175
|
+
app = FastAPI()
|
|
176
|
+
|
|
177
|
+
@app.get("/users")
|
|
178
|
+
async def list_users(params: OffsetDep) -> OffsetPage[dict]:
|
|
179
|
+
users = [{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]
|
|
180
|
+
return paginate(users, params)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Available dependencies:
|
|
184
|
+
|
|
185
|
+
| Dependency | Query Params | Produces |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `OffsetDep` | `?page=1&limit=20` | `OffsetParams` |
|
|
188
|
+
| `CursorDep` | `?limit=20&after=abc` | `CursorParams` |
|
|
189
|
+
| `FilterDep` | (user-defined fields) | `list[FilterSpec]` |
|
|
190
|
+
| `SortDep` | `?sort=name,-age` | `list[SortSpec]` |
|
|
191
|
+
| `SearchDep` | `?q=alice&search_fields=name,email` | `SearchSpec` |
|
|
192
|
+
|
|
193
|
+
### Declarative Filters
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from typing import Annotated
|
|
197
|
+
from fastapi import Query
|
|
198
|
+
from pypaginate.adapters.fastapi import FilterDep, FilterField
|
|
199
|
+
|
|
200
|
+
class UserFilters(FilterDep):
|
|
201
|
+
name: str | None = FilterField(None, operator="contains")
|
|
202
|
+
age_min: int | None = FilterField(None, field="age", operator="gte")
|
|
203
|
+
status: str | None = FilterField(None, operator="eq")
|
|
204
|
+
|
|
205
|
+
@app.get("/users")
|
|
206
|
+
async def list_users(
|
|
207
|
+
params: OffsetDep,
|
|
208
|
+
filters: Annotated[UserFilters, Query()],
|
|
209
|
+
):
|
|
210
|
+
# filters.to_specs() returns list[FilterSpec] for non-None fields
|
|
211
|
+
...
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Sorting and Search
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from pypaginate.adapters.fastapi import OffsetDep, SortDep, SearchDep
|
|
218
|
+
|
|
219
|
+
@app.get("/users")
|
|
220
|
+
async def list_users(params: OffsetDep, sort: SortDep, search: SearchDep):
|
|
221
|
+
# sort: ?sort=name,-created_at (- prefix = descending)
|
|
222
|
+
# search: ?q=alice&search_fields=name,email
|
|
223
|
+
...
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Filtering
|
|
227
|
+
|
|
228
|
+
Use `FilterSpec` to define filter conditions:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from pypaginate import FilterSpec
|
|
232
|
+
from pypaginate.filtering import FilterEngine, create_default_registry
|
|
233
|
+
|
|
234
|
+
engine = FilterEngine(create_default_registry())
|
|
235
|
+
|
|
236
|
+
users = [
|
|
237
|
+
{"name": "Alice", "age": 30, "status": "active"},
|
|
238
|
+
{"name": "Bob", "age": 25, "status": "inactive"},
|
|
239
|
+
{"name": "Charlie", "age": 35, "status": "active"},
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
# Simple equality
|
|
243
|
+
active = engine.apply(users, [FilterSpec(field="status", value="active")])
|
|
244
|
+
# [Alice, Charlie]
|
|
245
|
+
|
|
246
|
+
# Multiple filters (AND by default)
|
|
247
|
+
result = engine.apply(users, [
|
|
248
|
+
FilterSpec(field="age", operator="gte", value=30),
|
|
249
|
+
FilterSpec(field="status", value="active"),
|
|
250
|
+
])
|
|
251
|
+
# [Alice, Charlie]
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Nested Filter Groups
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from pypaginate import And, Or, FilterSpec
|
|
258
|
+
|
|
259
|
+
# (status = active) AND (age >= 30 OR name contains "bob")
|
|
260
|
+
group = And(
|
|
261
|
+
FilterSpec(field="status", value="active"),
|
|
262
|
+
Or(
|
|
263
|
+
FilterSpec(field="age", operator="gte", value=30),
|
|
264
|
+
FilterSpec(field="name", operator="contains", value="bob"),
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
result = engine.apply(users, group)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Available Filter Operators
|
|
272
|
+
|
|
273
|
+
| Operator | Description | Example |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `eq`, `ne` | Equality / inequality | `FilterSpec(field="status", value="active")` |
|
|
276
|
+
| `gt`, `gte`, `lt`, `lte` | Comparisons | `FilterSpec(field="age", operator="gte", value=18)` |
|
|
277
|
+
| `in`, `not_in` | Membership | `FilterSpec(field="role", operator="in", value=["admin", "user"])` |
|
|
278
|
+
| `contains`, `starts_with`, `ends_with` | Text matching | `FilterSpec(field="name", operator="contains", value="ali")` |
|
|
279
|
+
| `like`, `ilike` | SQL-style patterns | `FilterSpec(field="email", operator="like", value="%@gmail.com")` |
|
|
280
|
+
| `between` | Range | `FilterSpec(field="price", operator="between", value=[10, 100])` |
|
|
281
|
+
| `is_null`, `is_not_null` | Null checks | `FilterSpec(field="notes", operator="is_null")` |
|
|
282
|
+
| `empty`, `not_empty` | Empty checks | `FilterSpec(field="tags", operator="not_empty")` |
|
|
283
|
+
| `exists` | Field existence | `FilterSpec(field="id", operator="exists")` |
|
|
284
|
+
| `regex` | Regex matching | `FilterSpec(field="code", operator="regex", value="^A\\d+")` |
|
|
285
|
+
|
|
286
|
+
## Sorting
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from pypaginate import SortSpec, SortDirection
|
|
290
|
+
|
|
291
|
+
from pypaginate.sorting import SortEngine
|
|
292
|
+
|
|
293
|
+
engine = SortEngine()
|
|
294
|
+
|
|
295
|
+
users = [
|
|
296
|
+
{"name": "Charlie", "age": 35},
|
|
297
|
+
{"name": "Alice", "age": 30},
|
|
298
|
+
{"name": "Bob", "age": 25},
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
sorted_users = engine.apply(users, [
|
|
302
|
+
SortSpec(field="age", direction=SortDirection.DESC),
|
|
303
|
+
])
|
|
304
|
+
# [Charlie (35), Alice (30), Bob (25)]
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Search
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from pypaginate import SearchSpec
|
|
311
|
+
|
|
312
|
+
from pypaginate.search import SearchEngine
|
|
313
|
+
|
|
314
|
+
engine = SearchEngine()
|
|
315
|
+
|
|
316
|
+
users = [
|
|
317
|
+
{"name": "Alice Smith", "email": "alice@example.com"},
|
|
318
|
+
{"name": "Bob Johnson", "email": "bob@example.com"},
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
results = engine.apply(users, SearchSpec(
|
|
322
|
+
query="alice",
|
|
323
|
+
fields=("name", "email"),
|
|
324
|
+
))
|
|
325
|
+
# [Alice Smith]
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Fuzzy search (requires `pypaginate[search]`):
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
from pypaginate import SearchSpec, FuzzyMode
|
|
332
|
+
|
|
333
|
+
results = engine.apply(users, SearchSpec(
|
|
334
|
+
query="alce",
|
|
335
|
+
fields=("name",),
|
|
336
|
+
fuzzy=FuzzyMode.FUZZY,
|
|
337
|
+
threshold=75,
|
|
338
|
+
))
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Pipeline (Filter + Sort + Search + Paginate)
|
|
342
|
+
|
|
343
|
+
Compose all operations in a single call:
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from pypaginate import OffsetParams, FilterSpec, SortSpec, SortDirection
|
|
347
|
+
from pypaginate.engine.pipeline import SyncPipeline
|
|
348
|
+
from pypaginate.engine.paginator import Paginator
|
|
349
|
+
from pypaginate.adapters.memory import (
|
|
350
|
+
MemoryBackend,
|
|
351
|
+
MemoryFilterBackend,
|
|
352
|
+
MemorySortBackend,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
pipeline = SyncPipeline(
|
|
356
|
+
Paginator(MemoryBackend()),
|
|
357
|
+
filter_backend=MemoryFilterBackend(),
|
|
358
|
+
sort_backend=MemorySortBackend(),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
page = pipeline.execute(
|
|
362
|
+
users,
|
|
363
|
+
OffsetParams(page=1, limit=10),
|
|
364
|
+
filters=[FilterSpec(field="status", value="active")],
|
|
365
|
+
sorting=[SortSpec(field="name", direction=SortDirection.ASC)],
|
|
366
|
+
)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
For async (e.g., SQLAlchemy), use `AsyncPipeline` with `AsyncPaginator`.
|
|
370
|
+
|
|
371
|
+
## Architecture
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
pypaginate/
|
|
375
|
+
├── domain/ # Models, specs, enums, protocols (no deps)
|
|
376
|
+
├── engine/ # Paginator, cursor paginator, pipeline
|
|
377
|
+
├── filtering/ # In-memory filter engine + operators
|
|
378
|
+
├── sorting/ # In-memory sort engine
|
|
379
|
+
├── search/ # In-memory search engine
|
|
380
|
+
└── adapters/
|
|
381
|
+
├── memory/ # In-memory backends (filter, sort, search)
|
|
382
|
+
├── sqlalchemy/ # SA backends (offset, cursor, filter, sort, search)
|
|
383
|
+
└── fastapi/ # Annotated dependencies (OffsetDep, FilterDep, etc.)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Development
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
git clone https://github.com/CybLow/pypaginate.git
|
|
390
|
+
cd pypaginate
|
|
391
|
+
uv sync
|
|
392
|
+
|
|
393
|
+
# Run all checks
|
|
394
|
+
uv run ruff format . && uv run ruff check --fix . && uv run mypy src/ && uv run pytest
|
|
395
|
+
|
|
396
|
+
# Individual commands
|
|
397
|
+
uv run pytest # Tests
|
|
398
|
+
uv run pytest --cov # Coverage
|
|
399
|
+
uv run ruff format . # Format
|
|
400
|
+
uv run ruff check --fix . # Lint
|
|
401
|
+
uv run mypy src/ # Type check
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Contributing
|
|
405
|
+
|
|
406
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
407
|
+
|
|
408
|
+
1. Fork the repository
|
|
409
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
410
|
+
3. Run tests and quality checks (`uv run pytest && uv run ruff check .`)
|
|
411
|
+
4. Commit with conventional commits (`git commit -m 'feat: add amazing feature'`)
|
|
412
|
+
5. Open a Pull Request
|
|
413
|
+
|
|
414
|
+
## License
|
|
415
|
+
|
|
416
|
+
MIT -- see [LICENSE](LICENSE) for details.
|