Skip to main content

squeezle (Python client)

A small, dependency-light Python client for the Squeezle SQL query API. Authenticate with a personal sqz_ token, run saved queries or ad-hoc SQL, and get the rows back.

Install

pip install squeezle            # from this directory: pip install -e .
pip install "squeezle[pandas]"  # add .to_pandas() support

Only runtime dependency: requests.

Quick start

from squeezle import Client

sqz = Client("sqz_...")                    # or: Client.from_env()

# Run a saved query and block until it finishes
run = sqz.run_saved("query-uuid", variables={"since": "2026-01-01"})

for row in run.dicts():                    # up to ~1000 rows (stored preview)
    print(row)                             # {"id": 7, "email": "a@b.com"}

df = run.to_pandas()                       # needs the [pandas] extra

Client.from_env() reads SQUEEZLE_TOKEN and, optionally, SQUEEZLE_BASE_URL (default https://api.squeezle.app).

Ad-hoc SQL

run = sqz.run_sql(
    data_source_id="ds-uuid",
    sql="SELECT status, count(*) FROM orders WHERE created_at > {{since}} GROUP BY 1",
    variables={"since": "2026-01-01"},
    row_limit=5000,                        # int, or "max"
)
print(run.dicts())

Your token needs the queries:write scope to run ad-hoc SQL, plus runs:read to read results.

Variables

{{name}} placeholders in SQL bind as typed Postgres params ($1, $2, ...), so values can never change the query shape (injection-safe). For a saved query the types are stored, so you pass only variables. For ad-hoc SQL, declare each variable's type in variable_definitions.

type JSON value you pass example
text string "hello"
integer int or numeric string 42
float number or numeric string 3.14
boolean bool (or "yes"/"no"/"1"/"0") True
date "YYYY-MM-DD" string "2026-07-29"
timestamp ISO-8601 string "2026-07-29T14:00:00Z"
uuid uuid string "0189a2f0-...-444455556666"
json object/array (bound as jsonb) {"a": 1}
text[] / integer[] / any *[] JSON array ["paid","shipped"]
date_range / timestamp_range {"start": ..., "end": ...} {"start": "2026-01-01", "end": "2026-03-31"}

Three things that trip people up:

  1. Lists use = ANY(...), not IN. A *[] variable binds as one array param, so write WHERE id = ANY({{ids}}). WHERE id IN {{ids}} is invalid SQL.
  2. A range is one variable, two placeholders. Define period as date_range, then write {{period_start}} and {{period_end}} in the SQL; pass {"period": {"start": ..., "end": ...}}.
  3. required defaults to True. For an optional variable set "required": False and a "default", or you get a 422 "is required".

Enum / dropdown is type: "text" with control: "select" and options. Datetime is type: "timestamp". There is no number/list/enum type name.

run = sqz.run_sql(
    "ds-uuid",
    "SELECT * FROM orders WHERE status = ANY({{statuses}}) AND created_at >= {{since}}",
    variables={"statuses": ["paid", "shipped"], "since": "2026-01-01"},
    variable_definitions=[
        {"name": "statuses", "type": "text[]"},
        {"name": "since", "type": "date"},
    ],
)

See examples/variables.py for a runnable example of every type.

Getting all the data (beyond 1000 rows)

The API keeps only a ~1000-row preview in the database. For the full result, export to an artifact and download it:

rows = run.fetch_all("json")               # list[dict], the whole result
csv_text = run.fetch_all("csv")            # str
run.download("xlsx", "orders.xlsx")        # straight to disk (csv/json/jsonl/xlsx)
url = run.export_url("csv")                # short-lived signed URL, do it yourself

Two-step control (start now, wait later)

run = sqz.start_saved("query-uuid", variables={"n": 3})   # returns immediately (queued)
run.wait(timeout=120, poll_interval=1.0)                  # poll until terminal
run.raise_for_status()                                    # raise unless it succeeded

Manage saved queries

q = sqz.create_query(
    name="Daily orders",
    sql="SELECT * FROM orders WHERE created_at > {{since}}",
    data_source_id="ds-uuid",
    folder_id="folder-uuid",          # optional; visibility, tags, ... too
)

# Edit. The API optimistic-locks on `version`; omit it and the client reads the
# current version first (pass one you already hold to avoid the extra GET).
sqz.update_query(q["id"], sql="SELECT * FROM orders LIMIT 100")
sqz.update_query(q["id"], version=q["version"], name="Renamed")

sqz.delete_query(q["id"])

A stale version raises ConflictError (details["current_version"]). Query writes need the queries:write scope.

Browse the workspace

sqz.me()                     # user, capabilities, active org, plan
sqz.list_data_sources()      # connections you can query
sqz.list_queries()           # saved queries
sqz.get_query("query-uuid")
sqz.list_runs(query_id="query-uuid", limit=10)
sqz.get_run("run-uuid")
sqz.compile("SELECT {{id}}") # discover a query's variables without running it

Errors

Every API error raises a subclass of SqueezleError carrying .code, .message, .details, and .status:

Exception HTTP When
AuthenticationError 401 bad/expired token, org membership lapsed
ForbiddenError 403 missing scope or role; details["reason"] explains
PlanLimitError 402 plan entitlement or feature gate hit
NotFoundError 404 no such resource, or the run's result expired
ConflictError 409 saved-query version conflict
ValidationError 422 bad request; details maps field -> messages
RateLimitError 429 too many requests (auto-retried a few times first)
RunFailedError - the run ended in error/timeout/cancelled
RunTimeout - wait() gave up before the run finished
from squeezle import ValidationError

try:
    sqz.run_sql("ds-uuid", "SELECT bad")
except ValidationError as exc:
    print(exc.code, exc.details)

Test

python -m unittest discover -s tests    # stdlib only, no network
# or: pytest

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

squeezle-0.1.0.tar.gz (17.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

squeezle-0.1.0-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

File details

Details for the file squeezle-0.1.0.tar.gz.

File metadata

  • Download URL: squeezle-0.1.0.tar.gz
  • Upload date:
  • Size: 17.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for squeezle-0.1.0.tar.gz
Algorithm Hash digest
SHA256 afaf1caeeb487c3357193fbb4f3a3d9e50f363a779be6df2e51affe37b62d9a4
MD5 95c03e441562ab55a036d3faec2d0666
BLAKE2b-256 c73ec74b5946a242bad10e80b9dd6f7ec04b6e0d3223f1cb3b30cfa1594173fd

See more details on using hashes here.

Provenance

The following attestation bundles were made for squeezle-0.1.0.tar.gz:

Publisher: publish.yml on koode/squeezle-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file squeezle-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: squeezle-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 12.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for squeezle-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 21b4109c7e697f248dd859eab19c10a990bea3d7100c6c8478a3d135e894d165
MD5 f24d7a37279ba610177d7759a5a94ca0
BLAKE2b-256 f55118ef35c32ffaeb6163d47d961117fa65e6d5231c204947b7b45df8d56012

See more details on using hashes here.

Provenance

The following attestation bundles were made for squeezle-0.1.0-py3-none-any.whl:

Publisher: publish.yml on koode/squeezle-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page