querysource 4.1.10__tar.gz → 4.1.12__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.
- querysource-4.1.12/CHANGES.rst +54 -0
- {querysource-4.1.10 → querysource-4.1.12}/Makefile +9 -3
- {querysource-4.1.10/querysource.egg-info → querysource-4.1.12}/PKG-INFO +2 -2
- {querysource-4.1.10 → querysource-4.1.12}/pyproject.toml +10 -1
- {querysource-4.1.10 → querysource-4.1.12}/querysource/_version.py +3 -3
- querysource-4.1.12/querysource/handlers/_pagination.py +413 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/manager.py +145 -28
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/connections.py +14 -11
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/__init__.py +30 -16
- {querysource-4.1.10 → querysource-4.1.12}/querysource/version.py +1 -1
- {querysource-4.1.10 → querysource-4.1.12/querysource.egg-info}/PKG-INFO +2 -2
- {querysource-4.1.10 → querysource-4.1.12}/querysource.egg-info/SOURCES.txt +5 -1
- {querysource-4.1.10 → querysource-4.1.12}/querysource.egg-info/requires.txt +1 -1
- querysource-4.1.12/tests/handlers/__init__.py +0 -0
- querysource-4.1.12/tests/handlers/conftest.py +173 -0
- querysource-4.1.12/tests/handlers/test_querymanager_pagination.py +560 -0
- querysource-4.1.10/CHANGES.rst +0 -16
- {querysource-4.1.10 → querysource-4.1.12}/.bumpversion.cfg +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/agents/code-reviewer.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/agents/sdd-worker.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/pr-review.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-brainstorm.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-codereview.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-done.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-fromjira.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-next.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-proposal.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-spec.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-start.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-status.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-task.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/commands/sdd-tojira.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/aws-cost-optimization.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/code-reviewer.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/cython-development.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/python-development.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/rust-development.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/using-git-worktrees.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/worktree-pr-and-clean.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/worktree-start-feature.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.claude/rules/worktree-status.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.github/dependabot.yml +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.github/workflows/codeql-analysis.yml +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.github/workflows/release.yml +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.isort.cfg +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.jupyter/jupyter_notebook_config.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/.pylintrc +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/CODE_OF_CONDUCT.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/CONTRIBUTING.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/INSTALL +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/LICENSE +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/MANIFEST.in +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/README.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/app.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/bin/README.md +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/gunicorn_config.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/mypy.ini +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/nav.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/pytest.ini +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/__cli__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/cache/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/cache/backends/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/cache/backends/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/cache/backends/memcache.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/cache/backends/redis.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/cache/base.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/conf.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/connections.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/arangodb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/athena.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/bigquery.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/cassandra.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/clickhouse.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/cockroachdb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/couchbase.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/couchdb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/countries.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/delta.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/documentdb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/dynamodb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/elastic.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/ga.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/gcalc.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/hazel.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/iceberg.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/influx.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/jdbc.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/jira.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/mariadb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/memcached.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/mongo.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/mysql.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/odbc.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/openweather.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/oracle.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/pg.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/postgres.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/py.typed +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/qs.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/redis.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/rest.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/rethink.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/sa.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/salesforce.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/scylladb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/sqlalchemy.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/sqlite.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/sqlserver.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/upc.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/drivers/zipcodeapi.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/handlers/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/handlers/datasource.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/handlers/utils.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/datasources/models.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/events/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/exceptions.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/executor.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/log.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/multi.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/outputs/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/outputs/tableOutput/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/outputs/tableOutput/postgres.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/outputs/tableOutput/table.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/service.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/handlers/variables.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/credentials.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/databases/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/databases/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/databases/bigquery.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/databases/db.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/databases/mongo.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/databases/rethink.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/http.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/playwright_service.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/queries.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/interfaces/selenium_service.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/libs/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/libs/encoders.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/libs/functions/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/libs/py.typed +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/models.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/arrow.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/dt.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/factory.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/iter.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/modin.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/pandas.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/dt/polars.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/output.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/bigquery.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/documentdb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/mongodb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/mysql.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/postgres.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/rethink.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/sa.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/TableOutput/table.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/tables/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/bokeh.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/clustering.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/csv.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/describe.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/eda.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/excel.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/html.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/json.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/pdf.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/pickle.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/plotly.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/profiling.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/report.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/table.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/tsv.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/txt.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/outputs/writers/xml.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/abstract.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/abstract.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/abstract.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/arangodb.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/arangodb.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/arangodb.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/bigquery.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/bigquery.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/bigquery.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/cql.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/cql.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/cql.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/deltatbl.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/deltatbl.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/deltatbl.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/elastic.cpp +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/elastic.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/elastic.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/iceberg.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/iceberg.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/iceberg.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/influx.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/influx.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/influx.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/mongo.cpp +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/mongo.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/mongo.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/parser.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/parser.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/parser.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/pgsql.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/pgsql.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/pgsql.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/rethink.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/rethink.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/rethink.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sosql.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sosql.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sosql.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sql.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sql.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sql.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sqlserver.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sqlserver.pxd +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/parsers/sqlserver.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/plugins/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/plugins/importer.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/plugins/sources/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/arangodb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/bigquery.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/cassandra.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/db.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/default.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/deltatbl.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/documentdb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/dummy.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/elastic.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/external.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/http.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/iceberg.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/influx.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/mysql.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/pg.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/py.typed +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/rest.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/rethink.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/salesforce.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/scylladb.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/amazon.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/countries.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/ga.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/geofcc.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/gmaps.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/graphcountries.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/graphql.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/http.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/hubspot.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/openweather.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/parsers/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/parsers/amproduct.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/parsers/xpath.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/pokemon.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/populartimes.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/py.typed +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/rest.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/retailnext.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/rssapp.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/salesforce.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/scrapper.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/shoppertrack.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/swop.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/uap.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/upc.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/wm_stores.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/zammad.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sources/zipcodeapi.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sql.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/providers/sqlserver.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/py.typed +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/qs_parsers/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/base.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/executor.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/models.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/components/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/Concat.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/GroupBy.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/Info.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/Join.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/Melt.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/Merge.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/filter/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/operators/filter/flt.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/sources/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/sources/file.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/sources/query.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/Forecast.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/Map.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/abstract.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/correlation.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/crosstab.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/google/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/google/maps.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/pivot.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/tOrder.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/multi/transformations/tPandas.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/obj.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/queries/qs.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/scheduler/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/scheduler/jobs.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/scheduler/notifications.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/scheduler/scheduler.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/services.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/template/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/template/parser.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/converters.cpp +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/converters.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/dt/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/dt/filters.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/dt/transforms.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/py.typed +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/typedefs.c +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/typedefs.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/validators.cpp +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/types/validators.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/cache_serialization.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/events.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/fn.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/functions.cpp +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/functions.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/getfunc.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/handlers.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/parseqs.cpp +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/parseqs.pyx +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource/utils/validators.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource.egg-info/dependency_links.txt +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource.egg-info/entry_points.txt +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource.egg-info/not-zip-safe +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/querysource.egg-info/top_level.txt +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/run.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/Cargo.lock +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/Cargo.toml +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/pyproject.toml +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/arangodb_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/bigquery_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/cql_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/elastic_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/filter_common.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/flux_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/lib.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/mongo_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/mssql_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/parseqs.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/pgsql_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/rethink_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/safe_dict.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/soql_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/sql_parser.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/rust/src/validators.rs +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/setup.cfg +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/setup.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/static/notebook/bundle.js +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/templates/__init__.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/templates/base.html +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/templates/default.html +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/templates/default_table.html +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/templates/fontlist-v330.json +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/templates/fontlist-v390.json +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/templates/table_charts.html +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_api.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_arangodb_parser.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_column_filters.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_elastic_parser.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_eval.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_join_conditions.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_join_with_column_filter.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_rss.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_rust_parsers.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_scheduler_core.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_scheduler_integration.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_scheduler_jobs.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tests/test_scheduler_notifications.py +0 -0
- {querysource-4.1.10 → querysource-4.1.12}/tox.ini +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Unreleased
|
|
2
|
+
==========
|
|
3
|
+
|
|
4
|
+
FEAT-090 — Query Slug list pagination
|
|
5
|
+
-------------------------------------
|
|
6
|
+
|
|
7
|
+
**BREAKING**: ``GET /api/v1/management/queries`` now returns a paginated
|
|
8
|
+
envelope (``{"data": [...], "meta": {...}}``) instead of a bare JSON array.
|
|
9
|
+
The response is capped at 200 rows per request (default page size 50).
|
|
10
|
+
|
|
11
|
+
New query parameters on ``GET /api/v1/management/queries``:
|
|
12
|
+
|
|
13
|
+
- ``page`` (int, default ``1``)
|
|
14
|
+
- ``page_size`` (int, default ``50``, max ``200``)
|
|
15
|
+
- ``sort=<field>[:asc|desc]`` — allowlisted columns only
|
|
16
|
+
(``query_slug``, ``description``, ``program_slug``, ``provider``,
|
|
17
|
+
``is_cached``, ``created_at``, ``updated_at``)
|
|
18
|
+
- ``search=<term>`` — ``ILIKE '%term%'`` across ``query_slug``,
|
|
19
|
+
``description``, ``program_slug`` and ``source``
|
|
20
|
+
- ``fields=<csv>`` — same allowlist as before, now validated against
|
|
21
|
+
``QueryModel.columns`` (unknown columns rejected with ``400``)
|
|
22
|
+
|
|
23
|
+
Any remaining query-string key that matches a ``QueryModel`` column is
|
|
24
|
+
still accepted as an equality filter; unknown / unsafe keys are dropped
|
|
25
|
+
rather than forwarded to SQL.
|
|
26
|
+
|
|
27
|
+
New response headers: ``X-Total-Count``, ``X-Page``, ``X-Page-Size``,
|
|
28
|
+
``X-Total-Pages``. Empty results return ``204 No Content`` with
|
|
29
|
+
``X-Total-Count: 0`` (same semantics as the previous ``NoDataFound`` path).
|
|
30
|
+
|
|
31
|
+
Unchanged:
|
|
32
|
+
|
|
33
|
+
- ``GET /api/v1/management/queries/{slug}``
|
|
34
|
+
- ``GET /api/v1/management/queries:meta``
|
|
35
|
+
- ``GET /api/v1/management/queries/{slug}:insert``
|
|
36
|
+
- ``PUT`` / ``POST`` / ``PATCH`` / ``DELETE`` verbs
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
2.8.0 (2022-09-29)
|
|
40
|
+
==================
|
|
41
|
+
|
|
42
|
+
- new support for models based on datamodel
|
|
43
|
+
- upgraded version of asyncdb
|
|
44
|
+
- migrated parsers to cython
|
|
45
|
+
- added support for stored procedures in SQL Server driver.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
2.7.7 (2022-08-02)
|
|
49
|
+
==================
|
|
50
|
+
|
|
51
|
+
- Removing dependency of navigator.conf
|
|
52
|
+
- added new navigator-session dependency
|
|
53
|
+
- security fixes
|
|
54
|
+
- bump version packages.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# QuerySource Makefile
|
|
2
2
|
# This Makefile provides a set of commands to manage the QuerySource project.
|
|
3
3
|
|
|
4
|
-
.PHONY: venv install develop setup dev release format lint test clean distclean lock sync
|
|
4
|
+
.PHONY: venv install develop setup dev release format lint test clean distclean lock sync build-rust
|
|
5
5
|
|
|
6
6
|
# Python version to use
|
|
7
7
|
PYTHON_VERSION := 3.11
|
|
@@ -22,7 +22,7 @@ venv:
|
|
|
22
22
|
@echo 'run `source .venv/bin/activate` to start develop with QuerySource.'
|
|
23
23
|
|
|
24
24
|
# Install production dependencies using lock file
|
|
25
|
-
install:
|
|
25
|
+
install: build-rust
|
|
26
26
|
uv sync --frozen --no-dev --extra analytics --extra vectors
|
|
27
27
|
uv pip install navigator-api[uvloop,locale]
|
|
28
28
|
@echo "Production dependencies installed. Use 'make develop' for development setup."
|
|
@@ -36,9 +36,14 @@ else
|
|
|
36
36
|
endif
|
|
37
37
|
|
|
38
38
|
# Install all dependencies including dev dependencies
|
|
39
|
-
develop:
|
|
39
|
+
develop: build-rust
|
|
40
40
|
uv sync --frozen --extra analytics --extra dev
|
|
41
41
|
|
|
42
|
+
# Build the Rust-accelerated parser extension (querysource.qs_parsers._qs_parsers)
|
|
43
|
+
build-rust:
|
|
44
|
+
@command -v maturin >/dev/null 2>&1 || uv pip install maturin
|
|
45
|
+
maturin develop --release
|
|
46
|
+
|
|
42
47
|
# Alternative: install without lock file (faster for development)
|
|
43
48
|
develop-fast:
|
|
44
49
|
uv pip install -e .[analytics,dev]
|
|
@@ -187,5 +192,6 @@ help:
|
|
|
187
192
|
@echo " detect-tools - Show detected tools"
|
|
188
193
|
@echo " install-uv - Install uv"
|
|
189
194
|
@echo " build-inplace - Build Cython extensions in place"
|
|
195
|
+
@echo " build-rust - Build the Rust parser extension via maturin"
|
|
190
196
|
@echo ""
|
|
191
197
|
@echo "Current setup: $(TOOL_INFO)"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: querysource
|
|
3
|
-
Version: 4.1.
|
|
3
|
+
Version: 4.1.12
|
|
4
4
|
Summary: Aiohttp web service for querying several databases easily
|
|
5
5
|
Author-email: Jesus Lara <jesuslarag@gmail.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -55,7 +55,7 @@ Requires-Dist: webdriver-manager>=4.0.2
|
|
|
55
55
|
Requires-Dist: playwright==1.52.0
|
|
56
56
|
Requires-Dist: proxylists>=0.14.0
|
|
57
57
|
Requires-Dist: async-notify[all]>=1.4.0
|
|
58
|
-
Requires-Dist: navconfig[default,uvloop]
|
|
58
|
+
Requires-Dist: navconfig[default,uvloop]==2.1.3
|
|
59
59
|
Requires-Dist: navigator-api[locale,uvloop]>=2.12.22
|
|
60
60
|
Requires-Dist: jsonschema>=4.22.0
|
|
61
61
|
Requires-Dist: backoff==2.2.1
|
|
@@ -68,7 +68,7 @@ dependencies = [
|
|
|
68
68
|
# NAV libraries - using compatible versions
|
|
69
69
|
"proxylists>=0.14.0",
|
|
70
70
|
"async-notify[all]>=1.4.0",
|
|
71
|
-
"navconfig[uvloop
|
|
71
|
+
"navconfig[default,uvloop]==2.1.3",
|
|
72
72
|
"navigator-api[uvloop,locale]>=2.12.22",
|
|
73
73
|
"jsonschema>=4.22.0",
|
|
74
74
|
# Backoff Support
|
|
@@ -184,3 +184,12 @@ manifest-path = "rust/Cargo.toml"
|
|
|
184
184
|
module-name = "querysource.qs_parsers._qs_parsers"
|
|
185
185
|
bindings = "pyo3"
|
|
186
186
|
features = ["pyo3/extension-module"]
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# uv install settings
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Force copy mode: hardlink fallback has been observed to leave native
|
|
192
|
+
# extension packages (orjson, pytomlpp, …) without their compiled .so,
|
|
193
|
+
# producing ModuleNotFoundError: No module named '<pkg>.<native>' at import.
|
|
194
|
+
[tool.uv]
|
|
195
|
+
link-mode = "copy"
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '4.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (4, 1,
|
|
21
|
+
__version__ = version = '4.1.13.dev0+gd6d474e05.d20260424'
|
|
22
|
+
__version_tuple__ = version_tuple = (4, 1, 13, 'dev0', 'gd6d474e05.d20260424')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gd6d474e05'
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Pagination helpers for the Query Slug management list endpoint.
|
|
2
|
+
|
|
3
|
+
Pure functions + Pydantic models. No aiohttp, no DB calls — this module is
|
|
4
|
+
unit-testable in isolation.
|
|
5
|
+
|
|
6
|
+
This module provides:
|
|
7
|
+
* Constants (default / max page size, default sort field & direction).
|
|
8
|
+
* Allowlists derived from :class:`querysource.models.QueryModel` so that
|
|
9
|
+
column identifiers interpolated into raw SQL can never come from
|
|
10
|
+
user-controlled strings.
|
|
11
|
+
* Pydantic v2 models :class:`PaginationParams`, :class:`PaginationMeta`
|
|
12
|
+
and :class:`PaginatedResponse`.
|
|
13
|
+
* Pure SQL-builder helpers: :func:`build_where_clause`,
|
|
14
|
+
:func:`build_order_by`, :func:`build_count_sql`, :func:`build_page_sql`.
|
|
15
|
+
|
|
16
|
+
All builders validate every column identifier against a fixed allowlist and
|
|
17
|
+
route scalar values through :class:`querysource.types.validators.Entity`
|
|
18
|
+
(``toSQL`` + ``quoteString``) — the same helpers used by
|
|
19
|
+
``QueryManager.get_query_insert``. This matches the project's existing
|
|
20
|
+
SQL-safety pattern and provides defence-in-depth against injection.
|
|
21
|
+
|
|
22
|
+
See ``sdd/specs/querysource-slug-list-pagination.spec.md`` §3 Modules 1-2.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from typing import Any, Literal, Optional
|
|
28
|
+
|
|
29
|
+
from pydantic import BaseModel, Field, field_validator
|
|
30
|
+
|
|
31
|
+
from ..models import QueryModel
|
|
32
|
+
from ..types.validators import Entity
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
SortDirection = Literal["asc", "desc"]
|
|
39
|
+
|
|
40
|
+
DEFAULT_PAGE_SIZE: int = 50
|
|
41
|
+
MAX_PAGE_SIZE: int = 200
|
|
42
|
+
DEFAULT_SORT_FIELD: str = "updated_at"
|
|
43
|
+
DEFAULT_SORT_DIRECTION: SortDirection = "desc"
|
|
44
|
+
|
|
45
|
+
# Columns allowlisted for WHERE / filter kwargs — every key of the live
|
|
46
|
+
# ``QueryModel.columns()`` dict. Frozen so the set is hashable and immutable.
|
|
47
|
+
_MODEL_COLUMNS: dict = QueryModel.columns(QueryModel)
|
|
48
|
+
FILTERABLE_COLUMNS: frozenset[str] = frozenset(_MODEL_COLUMNS.keys())
|
|
49
|
+
|
|
50
|
+
# Scalar columns that the caller is allowed to sort on. jsonb / array columns
|
|
51
|
+
# are deliberately excluded (see spec §7 "Known Risks / Gotchas").
|
|
52
|
+
SORTABLE_COLUMNS: frozenset[str] = frozenset(
|
|
53
|
+
{
|
|
54
|
+
"query_slug",
|
|
55
|
+
"description",
|
|
56
|
+
"program_slug",
|
|
57
|
+
"provider",
|
|
58
|
+
"is_cached",
|
|
59
|
+
"created_at",
|
|
60
|
+
"updated_at",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Columns matched by the ``search`` query-string param with ``ILIKE '%term%'``.
|
|
65
|
+
SEARCHABLE_COLUMNS: tuple[str, ...] = (
|
|
66
|
+
"query_slug",
|
|
67
|
+
"description",
|
|
68
|
+
"program_slug",
|
|
69
|
+
"source",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PaginationParams(BaseModel):
|
|
74
|
+
"""Validated pagination / sort / search parameters for the slug list endpoint.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
page: 1-based page number (``>= 1``).
|
|
78
|
+
page_size: Rows per page. Clamped to ``[1, MAX_PAGE_SIZE]``.
|
|
79
|
+
sort_field: Column to sort on. Must be in :data:`SORTABLE_COLUMNS`.
|
|
80
|
+
sort_direction: ``"asc"`` or ``"desc"``.
|
|
81
|
+
search: Optional free-text search term (max 255 chars). Applied via
|
|
82
|
+
``ILIKE '%term%'`` across :data:`SEARCHABLE_COLUMNS`.
|
|
83
|
+
fields: Optional list of column names to project. Each element must be
|
|
84
|
+
in :data:`FILTERABLE_COLUMNS`.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
page: int = Field(default=1, ge=1)
|
|
88
|
+
page_size: int = Field(default=DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE)
|
|
89
|
+
sort_field: str = Field(default=DEFAULT_SORT_FIELD)
|
|
90
|
+
sort_direction: SortDirection = Field(default=DEFAULT_SORT_DIRECTION)
|
|
91
|
+
search: Optional[str] = Field(default=None, max_length=255)
|
|
92
|
+
fields: Optional[list[str]] = Field(default=None)
|
|
93
|
+
|
|
94
|
+
@field_validator("sort_field")
|
|
95
|
+
@classmethod
|
|
96
|
+
def _validate_sort_field(cls, v: str) -> str:
|
|
97
|
+
"""Reject sort fields that are not in :data:`SORTABLE_COLUMNS`."""
|
|
98
|
+
if v not in SORTABLE_COLUMNS:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"sort field not allowed: {v!r}. "
|
|
101
|
+
f"Allowed: {sorted(SORTABLE_COLUMNS)}"
|
|
102
|
+
)
|
|
103
|
+
return v
|
|
104
|
+
|
|
105
|
+
@field_validator("fields")
|
|
106
|
+
@classmethod
|
|
107
|
+
def _validate_fields(cls, v: Optional[list[str]]) -> Optional[list[str]]:
|
|
108
|
+
"""Reject any field not in :data:`FILTERABLE_COLUMNS`."""
|
|
109
|
+
if v is None:
|
|
110
|
+
return v
|
|
111
|
+
unknown = [c for c in v if c not in FILTERABLE_COLUMNS]
|
|
112
|
+
if unknown:
|
|
113
|
+
raise ValueError(f"unknown field(s): {unknown}")
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def offset(self) -> int:
|
|
118
|
+
"""SQL OFFSET computed from ``(page - 1) * page_size``."""
|
|
119
|
+
return (self.page - 1) * self.page_size
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def from_query_string(cls, qs: dict) -> "PaginationParams":
|
|
123
|
+
"""Parse a flat query-string dict into :class:`PaginationParams`.
|
|
124
|
+
|
|
125
|
+
Understood keys:
|
|
126
|
+
* ``page`` (int)
|
|
127
|
+
* ``page_size`` (int)
|
|
128
|
+
* ``sort`` — either ``"<field>"`` or ``"<field>:<asc|desc>"``
|
|
129
|
+
* ``search`` (str)
|
|
130
|
+
* ``fields`` — either a comma-separated string or a list of strings
|
|
131
|
+
|
|
132
|
+
Unknown keys are simply ignored here — they are handled as filter
|
|
133
|
+
kwargs by ``build_where_clause``.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
qs: Flat query-string mapping (string → string, typically).
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A validated :class:`PaginationParams` instance.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
pydantic.ValidationError: On unsafe or out-of-range values.
|
|
143
|
+
ValueError: On malformed ``sort`` direction.
|
|
144
|
+
"""
|
|
145
|
+
data: dict[str, Any] = {}
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
if "page" in qs:
|
|
149
|
+
data["page"] = int(qs["page"])
|
|
150
|
+
if "page_size" in qs:
|
|
151
|
+
data["page_size"] = int(qs["page_size"])
|
|
152
|
+
except (TypeError, ValueError) as exc:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"'page' and 'page_size' must be integers: {exc}"
|
|
155
|
+
) from exc
|
|
156
|
+
|
|
157
|
+
if "sort" in qs and qs["sort"]:
|
|
158
|
+
sort_value = str(qs["sort"]).strip()
|
|
159
|
+
if ":" in sort_value:
|
|
160
|
+
field_part, _, dir_part = sort_value.partition(":")
|
|
161
|
+
field_part = field_part.strip()
|
|
162
|
+
dir_part = dir_part.strip().lower()
|
|
163
|
+
if dir_part not in ("asc", "desc"):
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"invalid sort direction: {dir_part!r}; "
|
|
166
|
+
"expected 'asc' or 'desc'"
|
|
167
|
+
)
|
|
168
|
+
data["sort_field"] = field_part
|
|
169
|
+
data["sort_direction"] = dir_part
|
|
170
|
+
else:
|
|
171
|
+
data["sort_field"] = sort_value
|
|
172
|
+
|
|
173
|
+
if "search" in qs and qs["search"] not in (None, ""):
|
|
174
|
+
data["search"] = str(qs["search"])
|
|
175
|
+
|
|
176
|
+
if "fields" in qs and qs["fields"] not in (None, ""):
|
|
177
|
+
raw = qs["fields"]
|
|
178
|
+
if isinstance(raw, (list, tuple)):
|
|
179
|
+
data["fields"] = [str(x).strip() for x in raw if str(x).strip()]
|
|
180
|
+
else:
|
|
181
|
+
data["fields"] = [
|
|
182
|
+
s.strip() for s in str(raw).split(",") if s.strip()
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
return cls(**data)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class PaginationMeta(BaseModel):
|
|
189
|
+
"""Metadata block returned inside :class:`PaginatedResponse`."""
|
|
190
|
+
|
|
191
|
+
page: int
|
|
192
|
+
page_size: int
|
|
193
|
+
total: int
|
|
194
|
+
total_pages: int
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class PaginatedResponse(BaseModel):
|
|
198
|
+
"""Response envelope for paginated list endpoints."""
|
|
199
|
+
|
|
200
|
+
data: list[dict]
|
|
201
|
+
meta: PaginationMeta
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# SQL builders
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _quote_ident(name: str) -> str:
|
|
210
|
+
"""Return ``"<name>"`` after allowlist validation.
|
|
211
|
+
|
|
212
|
+
Only identifiers already checked against :data:`FILTERABLE_COLUMNS` reach
|
|
213
|
+
this point — this helper simply adds double quotes so column names that
|
|
214
|
+
happen to collide with Postgres reserved words are still safe.
|
|
215
|
+
"""
|
|
216
|
+
if name not in FILTERABLE_COLUMNS:
|
|
217
|
+
raise ValueError(f"column not in allowlist: {name!r}")
|
|
218
|
+
return f'"{name}"'
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _coerce_value(col_name: str, value: Any) -> str:
|
|
222
|
+
"""Run ``value`` through ``Entity.toSQL`` + ``Entity.quoteString``.
|
|
223
|
+
|
|
224
|
+
Mirrors the pattern used by ``QueryManager.get_query_insert`` at
|
|
225
|
+
``querysource/handlers/manager.py:40-49``. The column's datamodel field is
|
|
226
|
+
used to look up the Python type + db_type before conversion.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
col_name: Name of the column (already allowlisted).
|
|
230
|
+
value: Raw value to coerce.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
A SQL literal (quoted where appropriate) safe to splice into a WHERE
|
|
234
|
+
clause.
|
|
235
|
+
"""
|
|
236
|
+
field = _MODEL_COLUMNS.get(col_name)
|
|
237
|
+
if field is None:
|
|
238
|
+
# Should never happen — caller must pre-check the allowlist.
|
|
239
|
+
raise ValueError(f"column not in model: {col_name!r}")
|
|
240
|
+
_type = field.type
|
|
241
|
+
try:
|
|
242
|
+
_dbtype = field.db_type()
|
|
243
|
+
except Exception: # pragma: no cover - defensive
|
|
244
|
+
_dbtype = None
|
|
245
|
+
sql_val = Entity.toSQL(value, _type, dbtype=_dbtype)
|
|
246
|
+
if sql_val == "NULL":
|
|
247
|
+
return "NULL"
|
|
248
|
+
return Entity.quoteString(str(sql_val), no_dblquoting=False)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def build_where_clause(
|
|
252
|
+
params: PaginationParams,
|
|
253
|
+
extra_filters: dict,
|
|
254
|
+
) -> str:
|
|
255
|
+
"""Return a SQL WHERE clause (including the leading ``WHERE``) or ``""``.
|
|
256
|
+
|
|
257
|
+
The clause merges:
|
|
258
|
+
* Equality predicates for every key in ``extra_filters``. Every key
|
|
259
|
+
MUST be in :data:`FILTERABLE_COLUMNS` — unknown keys raise
|
|
260
|
+
``ValueError`` (callers convert to HTTP 400). This matches the
|
|
261
|
+
spec §7 "kwargs filter passthrough" requirement: "any filter key
|
|
262
|
+
NOT in the QueryModel.columns allowlist MUST be rejected (400)
|
|
263
|
+
rather than silently forwarded".
|
|
264
|
+
* An ``ILIKE '%term%'`` predicate over :data:`SEARCHABLE_COLUMNS`
|
|
265
|
+
when ``params.search`` is set (combined with ``OR`` inside a
|
|
266
|
+
parenthesised group).
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
params: Pre-validated :class:`PaginationParams`.
|
|
270
|
+
extra_filters: Additional filter kwargs (typically the leftover
|
|
271
|
+
query-string params).
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The WHERE clause (possibly empty).
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
ValueError: If any key of ``extra_filters`` is not in
|
|
278
|
+
:data:`FILTERABLE_COLUMNS`.
|
|
279
|
+
"""
|
|
280
|
+
predicates: list[str] = []
|
|
281
|
+
|
|
282
|
+
unknown = [k for k in extra_filters if k not in FILTERABLE_COLUMNS]
|
|
283
|
+
if unknown:
|
|
284
|
+
raise ValueError(f"unknown filter column(s): {unknown}")
|
|
285
|
+
|
|
286
|
+
for key, value in extra_filters.items():
|
|
287
|
+
coerced = _coerce_value(key, value)
|
|
288
|
+
if coerced == "NULL":
|
|
289
|
+
predicates.append(f"{_quote_ident(key)} IS NULL")
|
|
290
|
+
else:
|
|
291
|
+
predicates.append(f"{_quote_ident(key)} = {coerced}")
|
|
292
|
+
|
|
293
|
+
if params.search:
|
|
294
|
+
term = params.search.replace("\\", "\\\\").replace("'", "''")
|
|
295
|
+
like_literal = f"'%{term}%'"
|
|
296
|
+
or_terms = [
|
|
297
|
+
f"{_quote_ident(col)}::text ILIKE {like_literal}"
|
|
298
|
+
for col in SEARCHABLE_COLUMNS
|
|
299
|
+
if col in FILTERABLE_COLUMNS
|
|
300
|
+
]
|
|
301
|
+
if or_terms:
|
|
302
|
+
predicates.append("(" + " OR ".join(or_terms) + ")")
|
|
303
|
+
|
|
304
|
+
if not predicates:
|
|
305
|
+
return ""
|
|
306
|
+
return "WHERE " + " AND ".join(predicates)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def build_order_by(params: PaginationParams) -> str:
|
|
310
|
+
"""Return ``ORDER BY "<col>" <DIR>`` with a validated column name.
|
|
311
|
+
|
|
312
|
+
Defence in depth: even though :class:`PaginationParams` already rejects
|
|
313
|
+
unknown sort columns, we re-check here before interpolating.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
params: Pre-validated :class:`PaginationParams`.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
The ``ORDER BY`` clause.
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
ValueError: If the sort field is not in :data:`SORTABLE_COLUMNS`.
|
|
323
|
+
"""
|
|
324
|
+
if params.sort_field not in SORTABLE_COLUMNS:
|
|
325
|
+
raise ValueError(
|
|
326
|
+
f"sort field not allowed: {params.sort_field!r}"
|
|
327
|
+
)
|
|
328
|
+
direction = "ASC" if params.sort_direction == "asc" else "DESC"
|
|
329
|
+
return f'ORDER BY "{params.sort_field}" {direction}'
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def build_count_sql(schema: str, table: str, where: str) -> str:
|
|
333
|
+
"""Return ``SELECT COUNT(*) FROM "<schema>"."<table>" <where>``.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
schema: Postgres schema name. Must be a bare identifier.
|
|
337
|
+
table: Table name. Must be a bare identifier.
|
|
338
|
+
where: WHERE clause as produced by :func:`build_where_clause`. May be
|
|
339
|
+
empty.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
A complete ``SELECT COUNT(*)`` statement.
|
|
343
|
+
"""
|
|
344
|
+
_validate_bare_identifier(schema, "schema")
|
|
345
|
+
_validate_bare_identifier(table, "table")
|
|
346
|
+
base = f'SELECT COUNT(*) FROM "{schema}"."{table}"'
|
|
347
|
+
if where:
|
|
348
|
+
return f"{base} {where}"
|
|
349
|
+
return base
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def build_page_sql(
|
|
353
|
+
schema: str,
|
|
354
|
+
table: str,
|
|
355
|
+
fields: list[str],
|
|
356
|
+
where: str,
|
|
357
|
+
order_by: str,
|
|
358
|
+
limit: int,
|
|
359
|
+
offset: int,
|
|
360
|
+
) -> str:
|
|
361
|
+
"""Return a paged SELECT statement.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
schema: Postgres schema name. Must be a bare identifier.
|
|
365
|
+
table: Table name. Must be a bare identifier.
|
|
366
|
+
fields: List of columns to project. Each must be in
|
|
367
|
+
:data:`FILTERABLE_COLUMNS`. ``[]`` projects all columns (``*``).
|
|
368
|
+
where: Clause from :func:`build_where_clause` (may be empty).
|
|
369
|
+
order_by: Clause from :func:`build_order_by`.
|
|
370
|
+
limit: Non-negative integer used as ``LIMIT``.
|
|
371
|
+
offset: Non-negative integer used as ``OFFSET``.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
A complete ``SELECT`` statement safe to pass to ``conn.fetch``.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
ValueError: On unknown columns, non-bare identifiers, or negative
|
|
378
|
+
``limit`` / ``offset``.
|
|
379
|
+
"""
|
|
380
|
+
_validate_bare_identifier(schema, "schema")
|
|
381
|
+
_validate_bare_identifier(table, "table")
|
|
382
|
+
if not isinstance(limit, int) or limit < 0:
|
|
383
|
+
raise ValueError(f"limit must be a non-negative int, got {limit!r}")
|
|
384
|
+
if not isinstance(offset, int) or offset < 0:
|
|
385
|
+
raise ValueError(f"offset must be a non-negative int, got {offset!r}")
|
|
386
|
+
|
|
387
|
+
if fields:
|
|
388
|
+
unknown = [c for c in fields if c not in FILTERABLE_COLUMNS]
|
|
389
|
+
if unknown:
|
|
390
|
+
raise ValueError(f"unknown projection column(s): {unknown}")
|
|
391
|
+
select_list = ", ".join(f'"{c}"' for c in fields)
|
|
392
|
+
else:
|
|
393
|
+
select_list = "*"
|
|
394
|
+
|
|
395
|
+
parts = [f'SELECT {select_list} FROM "{schema}"."{table}"']
|
|
396
|
+
if where:
|
|
397
|
+
parts.append(where)
|
|
398
|
+
if order_by:
|
|
399
|
+
parts.append(order_by)
|
|
400
|
+
parts.append(f"LIMIT {limit} OFFSET {offset}")
|
|
401
|
+
return " ".join(parts)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _validate_bare_identifier(value: str, kind: str) -> None:
|
|
405
|
+
"""Ensure ``value`` contains only ``[A-Za-z0-9_]`` characters.
|
|
406
|
+
|
|
407
|
+
Schema and table names come from ``QueryModel.Meta`` (static config), but
|
|
408
|
+
we still validate to avoid accidental future misuse.
|
|
409
|
+
"""
|
|
410
|
+
if not value or not all(
|
|
411
|
+
c.isalnum() or c == "_" for c in value
|
|
412
|
+
) or value[0].isdigit():
|
|
413
|
+
raise ValueError(f"invalid {kind} identifier: {value!r}")
|