hypequery (Python)

The Python SDK and CLI.

Latest: 0.1.0b4 · November 10, 2026 · PyPI · CHANGELOG.md

RSS
0.1.0b4Pre-release
  • measure.approx_count_distinct(field) estimates a distinct count with ClickHouse uniq (uniqIf with measure filters), far cheaper than the exact count_distinct (uniqExact) on large columns. Catalogs and semantic contracts mark it, and derived measures that use it, approximate: true, as TypeScript does; it is duplicate-insensitive through belongs_to. Publishing it is refused until deployment contract 3, as in TypeScript.

  • Exported client contracts keep isNull and isNotNull on boolean filters that allow them (RFC 0023), instead of dropping them.

  • Documentation for the ported ClickHouse features: null checks, aggregate states, row resolution, segments, window and shift measures, in the Python datasets pages and the package README. A shared execution corpus (specs/datasets/time-measures-corpus-v1.json) now checks Python's window and shift results row for row against TypeScript.

  • measure.shift can wrap a window, a derived measure or another shift, as in TypeScript: a prior-year year-to-date, a shifted average order value, or shift(shift(x)). The wrapped measure is recalculated over the earlier interval, and shifted formulas divide shifted aggregates. Derived measures over derived measures that read windows or shifts are now planned too, instead of refused. Definitions reject dependency cycles through shifts and formulas, and catalogs report the grains both the shift and its input support. A shifted formula has no single aggregation, so its catalog and semantic contract entries omit aggregation and field, hashing the same as TypeScript. A query is refused as input-invalid when nested shifts would need more than 64 evaluation contexts or 1024 evaluated measures.

  • client.stream(target, query) and client.stream_arrow(target, query) stream a dataset query's result instead of returning it whole, so memory stays flat however many rows there are. stream yields batches of the rows execute returns; stream_arrow yields pyarrow.RecordBatches with ClickHouse's native column types (decimal128, uint64), with read_all() and to_reader() for DuckDB, polars and pandas (hypequery[arrow]). Both run the query execute would, under the same settings, tenant scope, deadline and cancellation, without the cache or pagination. Use them as context managers (async with on the async client): closing a stream early stops the query, with KILL QUERY where a control connection is configured. Every ClickHouse and chDB executor streams; a result that grows past max_result_rows or max_result_bytes fails as too-large rather than ending short. Custom executors stream by implementing the new StreamingQueryExecutor protocol.

  • Streams have ceilings of their own. DatasetLimits(max_stream_rows=...) lets a dataset allow streams more rows than the pages and agents max_result_size bounds; a stream with no limit runs under that ceiling as its LIMIT and ends there. A stream's memory stays flat, so its byte limit is stream_max_result_bytes, a new create_dataset_client option of at most 1 TiB. Without it, a stream keeps a max_result_bytes the client's settings set and runs under 1 TiB in place of the default. max_result_rows and max_execution_time still bound it, and a request context can still tighten all three.

  • hypequery demo: a sample semantic layer on embedded ClickHouse, served to an AI agent over MCP stdio with no server, configuration or network. With only uv installed, uvx --from "hypequery[chdb,mcp]" hypequery demo seeds a two-tenant shop in a temporary chDB database (deleted on exit) and serves its orders, events and daily_activity datasets. --self-test prints the answers in the terminal, each beside the read a naive query gets: plain reads of a ReplacingMergeTree against resolved_rows.replacing(), count_distinct against approx_count_distinct, merge("uniq") over an AggregatingMergeTree against summed daily counts, and measure.shift and measure.trailing results. --print-config prints an mcpServers entry and --tenant picks the tenant the agent is signed in as.

  • Float measures serialize about 65 times faster, with byte-identical output. Python serve's NDJSON streams spent nearly all their time formatting float measures as strings: streaming 1M rows took 44 s and now takes about 4 s, on a par with TypeScript serve. JSON pages, MCP results and canonical protocol values use the same serializer and get the same speedup.

  • hypequery init telemetry now reports how far setup got (stage_reached: started, connection_tested, framework_selected, confirmed or completed) and whether connecting to ClickHouse worked (connection_result: ok, failed or skipped), as the TypeScript CLI does. Both are fixed enums: never hosts, table names or anything typed at a prompt.

  • measure.merge(state_function, field) reads an AggregateFunction column of an AggregatingMergeTree table (or the materialized view feeding one) by merging its states: measure.merge("uniq", "users") is uniqMerge(users), so a week of daily states merges into one distinct count instead of summing daily counts (RFC 0022). The state function must be one of RFC 0022's list; merge measures take no filters or sql and are not merged through a relationship. Estimating states (uniq, uniqCombined, ...) are marked approximate, and the deployment contract carries stateFunction, matching TypeScript (shared fixture specs/datasets/merge-authoring-v1.json).

  • A native MCP server: hypequery mcp [module:api] serves an API's authorized client contract to AI agents over stdio, with the same four tools (list_datasets, get_dataset_schema, query_metric, query_dataset), input schemas, dataset_guide prompt and MCP_* errors as @hypequery/mcp. Calls run through Api.handle in-process, so the API's authentication, policy and tenant apply; --client-contract-url bridges a deployed API instead, --self-test checks the contract and --print-config prints an mcpServers entry. Node is no longer needed. Install the mcp extra.

  • hypequery.mcp exposes the pieces: ClientContractExecutor, ApiContractBackend, HttpContractBackend, create_mcp_server and serve_stdio.

  • The standalone hypequery init app now publishes a client contract (client_profile=ClientQueryProfile() and GET /client-contract), so hypequery mcp app:api works on a fresh project.

  • Null-check filters (RFC 0023): filter.is_null(field) and filter.is_not_null(field) render IS NULL / IS NOT NULL with nothing bound. Measures may use them in fixed filters; callers may use them only on filters whose FilterDefinition(operators=...) lists them, so the default operator list, and existing deployment identities, are unchanged. A null check carries no value (not even null); served requests and the cache and deployment wire omit it. Exported SEMANTIC_NULL_CHECK_OPERATORS.

  • Resolved-row sources (RFC 0021): dataset(..., row_resolution=...) reads a ReplacingMergeTree or CollapsingMergeTree source with FINAL, so superseded row versions never double count. resolved_rows.replacing(deleted_column=...) also keeps only rows not soft-deleted, and resolved_rows.collapsing(sign_column=...) / .versioned_collapsing(...) only surviving positive rows; resolved_rows.final() is FINAL alone. Relationship targets are resolved inside their join, and relationship key checks count current rows. Every statement that reads a resolved source pins optimize_move_to_prewhere_if_final = 0, so no filter can match an older version. Catalogs, semantic contracts and deployment contracts carry rowResolution, as TypeScript does.

  • Joins and relationship key checks over a resolved (FINAL) target qualify its current-row column, so a deleted or sign column named like an internal alias (_hq_match, __hq_rows, __hq_keys) no longer reads that alias.

  • Segments (RFC 0015): dataset(segments={"paid": segment(filter.eq("status", "paid"))}) declares named row filters that queries select by name, DatasetQuery(segments=("paid",)) or "segments": ["paid"] in a served request. A segment may use any of the dataset's own dimensions, including ones not exposed as filters, but not a relationship or the tenant column, and may use null checks. Catalogs and contracts list each segment's label and description only. Publishing is refused until deployment contract 3, as in TypeScript.

  • Dataset endpoints can stream their results. With EndpointPolicy(stream_max_rows=...) (at most the dataset's max_stream_rows, else max_result_size), a request that asks with Accept: application/x-ndjson gets row batches as NDJSON frames (start, rows…, then end with rowCount, or an error frame if the query fails after the response began), and Accept: application/vnd.apache.arrow.stream gets an Arrow IPC stream, frame for frame as TypeScript serve sends them. Streams keep the endpoint's auth, tenant scope and validation, are never cached, and stop the query when the caller goes. They need to_asgi_app; WSGI, Django views and Flask answer with the usual page. Under a ProductionProfile a stream holds its slot, runs within timeout_seconds and stops at max_result_bytes: NDJSON with a PAYLOAD_TOO_LARGE frame, Arrow broken off before its end-of-stream marker.

  • Serve sends a dimension integer beyond ±(2^53 − 1), such as a UInt64 id, as a string in JSON pages and NDJSON stream frames, so JavaScript clients (React hooks, fetch, MCP clients) no longer round it. Smaller integers stay numbers, and local Python client results keep native ints.

  • hypequery skill install writes hypequery's agent skills (hypequery-datasets, hypequery-query) where Claude Code (.claude/skills/), Cursor and Codex (.agents/skills/) load them, with --agent, --scope project|user, --check and --force. The same skills ship with the TypeScript CLI.

  • Window and shift measures (RFC 0015): measure.trailing("revenue", 7, "day"), measure.to_date("revenue", "month"), measure.cumulative("orders") and measure.shift("revenue", 1, "week") re-aggregate a base measure along the time axis, and derived measures can divide by them for period-over-period ratios. A query that selects one needs a by grain and exactly one bounded range on the time key. It plans one ClickHouse statement that fills every bucket and guards the series size, in the query timezone. Client endpoints do not advertise these measures, and publishing is refused until deployment contract 3, as in TypeScript.

0.1.0b3Pre-release
  • Datasets can run on embedded ClickHouse with the new chdb extra: create_chdb_executor(path) (and create_async_chdb_executor) execute the same compiled, read-only, parameter-bound SQL in-process on chDB, with no server. path is an on-disk directory or ":memory:"; database= sets where unqualified sources resolve. Results decode exactly as from a ClickHouse server.

  • hypequery init --database chdb scaffolds a project on embedded ClickHouse, as the TypeScript CLI does: the standalone or FastAPI app opens a chDB database directory (--chdb-path, default analytics.chdb) with no server or connection settings, and installs the chdb extra. An existing database is inspected like a server's; a new one gets the orders example and a seeding step. hypequery generate datasets reads the same database from --chdb-path, the path init records in pyproject.toml, or a project analytics.chdb when no ClickHouse server is configured, and says plainly when another process such as hypequery dev has the database open.

  • hypequery dev serves a chDB project with reload, its default: the supervisor that checks the app before starting the worker now releases the app's database, since chDB holds a database directory for one process at a time (run_dev(reload=True) does the same; other chDB executors in the process stay open).

  • The CLI now collects pseudonymous usage telemetry, on by default like the TypeScript CLI: one event per command with the command, flag names (never values), outcome, a duration bucket, and install context such as the Python version and installer. It never records code, SQL, names, paths or credentials, and never delays a command. Disable it with hypequery telemetry disable, --no-telemetry, DO_NOT_TRACK=1 or HYPEQUERY_TELEMETRY_DISABLED=1; set HYPEQUERY_TELEMETRY_DEBUG=1 to print the event instead of sending it. Settings and the install ID are shared with the TypeScript CLI.

  • New hypequery telemetry [status|enable|disable] command and global --no-telemetry flag.

  • create_api(client_profile=...) puts every Dataset entry of datasets= in the client contract, so the shorthand can produce typed React hooks.

  • A build export with no client endpoints now fails: hypequery.serve.export_client_contract(api) raises ValueError naming the dataset and metric routes installed without a client profile, and hypequery export client-contract exits 1.

  • hypequery export client-contract --factory accepts the installed API (an Api, its to_asgi_app or to_wsgi_app application, or a zero-argument function returning one) and exports its build view, so the contract cannot drift from the served paths.

  • Breaking: a create_api(metrics={key: ...}) key is the metric's name, as in TypeScript: its client-contract ID, React hook key and output alias. A metric created with a different name= is refused.

  • Portable semantic queries accept having and timezone (RFC 0025). validate_protocol_semantic_query accepts having on dataset queries (each condition names a selected measure, with a numeric comparison) and timezone from RFC 0018's pinned registry, under extensions 1 and 2. New ProtocolHavingCondition, ProtocolHavingComparisonOperator and ProtocolHavingOperator exports. Cache preimages refuse a non-empty having or a non-UTC timezone; "UTC" normalizes as if absent, so no existing preimage changes.

  • The PyPI page now shows a short README.pypi.md: what the package is, one define-and-query sample, a FastAPI mount, and links into the docs. The full guide stays in README.md. Package metadata gains the Framework :: FastAPI and library classifiers, discovery keywords (semantic layer, metrics, multi-tenant, FastAPI, Django, Flask, MCP, pandas, polars, Arrow), and a Documentation link to https://hypequery.com/docs/python.

  • Null-check filters (RFC 0023): filter.is_null(field) and filter.is_not_null(field) render IS NULL / IS NOT NULL with nothing bound. Measures may use them in fixed filters; callers may use them only on filters whose FilterDefinition(operators=...) lists them, so the default operator list, and existing deployment identities, are unchanged. A null check carries no value (not even null); served requests and the cache and deployment wire omit it. Exported SEMANTIC_NULL_CHECK_OPERATORS.

  • Exported client contracts keep isNull and isNotNull on boolean filters that allow them (RFC 0023), instead of dropping them.

  • hypequery.protocol.build_client_contract_tool_schemas() compiles a signed client contract into the query_dataset and query_metric MCP tool input schemas, identical to @hypequery/datasets's, plus the manifest hash. It is the schema layer for a future Python MCP server; Python APIs still reach agents through @hypequery/mcp.

  • Server-fault errors (internal, unavailable) keep the reason they were raised with as CompiledQueryError.diagnostic, shown in tracebacks; the public message is unchanged. A relationship whose target dataset is missing from the client's registry now names the dataset and how to register it, instead of only "The query could not be executed."

0.1.0b2Pre-release
  • hypequery init is guided in a terminal, as the TypeScript CLI is. It asks for whatever its flags leave unset: where to create the project; how to reach ClickHouse (the connection already in your environment, or a URL and credentials typed in, with the password hidden, used only to read the schema and never written to a file); which tables, when the database has more than ten; the tenant column, when one looks like a tenant id; and which app to scaffold, defaulting to FastAPI, Django or Flask when it finds one. A failed connection offers to try again, change the details, or create the offline example. It ends with the same choices as flags. Every flag answers its question, and --no-interactive, a CI environment variable, or input that is not a terminal skip the questions and keep the previous behavior.

  • The hypequery CLI loads .env.local and .env from the directory it runs in, as the TypeScript CLI does. A variable already set wins over both files. The library and generated apps still read only the real environment; hypequery.cli.load_env_files(directory) applies the same rules in your own scripts, such as the scaffold README's seed step.

  • When init can't reach ClickHouse, the message names CLICKHOUSE_URL as well as the host settings and suggests --skip-connection, and the driver's own "Unexpected Http Driver Exception" log line no longer reaches the terminal.

  • hypequery init --framework fastapi scaffolds a FastAPI app.py: the bearer-token agents API mounted at /api/agents, and /analytics/<dataset>, a route of your own that refuses everyone until its current_principal dependency reads your login. The project depends on fastapi directly; hypequery adds no FastAPI extra.

  • Declaring filters on a dataset no longer makes its other dimensions unfilterable (RFC 0024), matching TypeScript. Callers may filter on every declared filter and every dimension not marked filterable=False. A dimension's own filter takes the operators of the filters declared on its field, so declaring plan_filter on plan with operators=("eq",) also limits plan to eq. Relationship filters reach every filterable, non-SQL dimension of a belongs_to or has_one target. An explicit empty filters={} no longer disables filtering; mark a dimension filterable=False to keep it out. Catalogs, client contracts and deployment contracts are unchanged.

  • Base dataset columns are always qualified (__hq_base), not only when a relationship join is present. ClickHouse resolves an output alias before a column of the same name, so a dimension status reading column country captured a filter or selection on the real status column (silent wrong rows), and a measure named like its column (amount=measure.sum("amount")) broke any other measure or filter on that column.

  • Under tenants(...) with several tenants or all_tenants(), relationship joins also match the tenant keys, so a key reused by another tenant no longer joins that tenant's row. check_relationships counts (key, tenant) pairs under those scopes to match. Single-tenant SQL is unchanged.

  • Formula divide casts the dividend to Float64. Decimal division kept the dividend's scale and truncated, so round(divide("revenue", null_if_zero("orders")), 2) could come out one cent low.

  • hypequery init --tenant-column scaffolds answer their README's first query: the development token reads the tenant in HYPEQUERY_DEV_TENANT (added to .env.example and the README), and endpoints over tenant-scoped datasets use EndpointPolicy(tenant="required"), so a principal without a tenant gets a clear 403 and its client contract leaves them out.

  • hypequery init --tenant-column records the column in schema.json, and hypequery generate datasets reuses it unless given --tenant-column or the new --no-tenant-column. Regenerating without repeating the flag used to drop every tenant_key. An unusable recorded value is refused before discovery rather than read as no tenant column.

  • A strict client endpoint's 400 Invalid client query. now carries details.issues[].path, the member or item that failed (for example ["filters", 0, "value"]), as the README promised. Paths name contract members and indexes only; an unknown member is reported at the root, and no submitted value is echoed.

  • Scaffolds discovered as a user other than default no longer suggest CLICKHOUSE_USERNAME=default in .env.example and the README, which would quietly connect as a different, usually broader, user. They show a placeholder; the real name stays out of generated files, like the password.

  • to_asgi_app and to_wsgi_app log one warning on hypequery.serve when served below a root_path or SCRIPT_NAME that differs from client_mount_path, naming the setting. Contract paths still never follow a prefix the server or a proxy reports.

  • Docs: a query mapping passed to client.execute uses DatasetQuery's field names (order_by, not the HTTP wire's orderBy); every scaffold README (standalone, Django and Flask, offline or from your schema) says timeouts need SELECT ON system.processes or a control user; a synchronous caller gets deadline-exceeded only once ClickHouse stops the query.

  • hypequery init (standalone): the generated API is now open for local development, like the TypeScript scaffold. app.py authenticates every request as one developer with from_context(..., same_origin=False), so hypequery dev answers curl without a token, and HYPEQUERY_DEV_TOKEN is no longer required or listed in .env.example. The README and app.py say to add real authentication (jwt_strategy, from_context with the host's login, or a credential check) before deploying; a ProductionProfile refuses the open API and hypequery dev still warns when bound beyond loopback. Django and Flask scaffolds keep their bearer token for /api/agents.

  • hypequery init --tenant-column (standalone): the README now explains why queries answer 403 FORBIDDEN ("requires runtime tenant scoping") and shows the one-line local fix, Principal(subject="developer", tenant_id="..."); app.py carries the same hint. Tenant-scoped datasets stay fail-closed by default.

  • Django and Flask scaffolds: principal_for_token's docstring now says it refuses every token while HYPEQUERY_DEV_TOKEN is unset.

0.1.0b1Pre-release
  • hypequery init --framework standalone|django|flask chooses the application written beside datasets.py (decision 0023 step 6). standalone, the default, is app.py: an API from create_api, served as plain ASGI by hypequery dev with no web framework. It replaces the fastapi choice, which wrote the same app; to use FastAPI, mount app in it.

  • --framework django writes a small runnable Django project (settings.py, urls.py, manage.py) and analytics.py; --framework flask writes a Flask app.py. Both serve hypequery's endpoints natively (urls(api), blueprint(api)) twice: at /api/analytics behind the framework's own login (django_user, flask_session) with the same-origin check, and Django's CSRF middleware, for @hypequery/react; and at /api/agents behind a bearer token (HYPEQUERY_DEV_TOKEN in development), for @hypequery/mcp. Each also has a view or route of its own on the per-request client. Until wired to a login, Flask's session API and route answer 401, and tenant-scoped datasets refuse signed-in Django users. Pyproject extras are django or flask; the README covers secrets, hosts, jwt_strategy and moving the pieces into an existing Django project.

  • --framework works with --skip-connection: each framework gets the offline orders example, with datasets.py and seed.sql. The standalone example's dataset now lives in datasets.py too.

  • hypequery init accepts --tenant-column, like generate datasets, so a tenant-scoped project needs one command.

  • hypequery init records --tables / --exclude-tables in schema.json, and generate datasets reuses that selection unless given --tables, --exclude-tables or the new --all-tables. Regenerating no longer widens a project to every table in the database.

  • The scaffold's .env.example, README and app.py match how discovery connected: CLICKHOUSE_URL when it was set, otherwise the host, port and TLS setting discovery used. Credentials stay placeholders.

  • "Possible tenant column" review notes name the --tenant-column flag that sets the key.

  • generate datasets and export client-contract print normalised paths.

  • Add label and description to Dataset and dataset(...). Client-contract exports and live discovery now advertise dataset, dimension and measure label/description as optional endpoint annotations (RFC 0018 amendment), so MCP agents listing datasets see each dataset's own description. Whitespace runs are folded and blank text is omitted; text over 4096 UTF-8 bytes or containing control characters fails export. Annotations are excluded from schemaHash, so editing a description does not trigger CLIENT_SCHEMA_CHANGED. Pair with @hypequery/mcp/@hypequery/cli releases that understand annotations.

  • The Django and Flask integrations' process-wide client connects on its first query, not when it is created. Modules can hold get_client() (Django) or the new hq.client_for(app) (Flask, no app context needed) at import time, and the app starts even with ClickHouse down. A closed client refuses to reconnect.

  • Publish a dataset's description in deployment contracts and bundles, matching TypeScript. The contract has no dataset-level label, so Dataset.label is not published there. Descriptions follow the existing deployment text rules: blank, multi-line, control-character or over-4096-byte text fails the export.

  • Docs: django_user's page must set Django's csrftoken cookie (ensure_csrf_cookie, or a template using {% csrf_token %}), or Django answers its own HTML 403 before hypequery runs. The django_user and urls docstrings and the --framework django scaffold's README now say so.

  • hypequery.contrib.django.urls answers a Host outside ALLOWED_HOSTS as the core answers one, 400 Invalid host header as plain text, instead of Django's HTML DisallowedHost page (the technical page under DEBUG). It carries the core's x-request-id (and a valid caller id as x-correlation-id) and is still logged to django.security.DisallowedHost. A host refused by middleware before the view runs, such as CommonMiddleware, still gets Django's own response.

  • Docs: explain that per-endpoint schemaHash values (not the document contentHash) match between the build export and the live discovery view, and how measure values differ between in-process results and hypequery's HTTP wire.

  • When ClickHouse rejects the server's own connection, the error is now a server fault instead of the caller's. A wrong password or unknown user (AUTHENTICATION_FAILED, UNKNOWN_USER, WRONG_PASSWORD, REQUIRED_PASSWORD, IP_ADDRESS_NOT_ALLOWED, USER_EXPIRED), a missing grant (ACCESS_DENIED, DATABASE_ACCESS_DENIED, NOT_ENOUGH_PRIVILEGES), a missing database (UNKNOWN_DATABASE) and a readonly-profile mismatch all raise CompiledQueryError("unavailable"), which hypequery.serve answers with 503 CLICKHOUSE_UNREACHABLE and the fixed message "The query executor is unavailable.". Before, they answered 401 UNAUTHORIZED, 403 FORBIDDEN or 404 NOT_FOUND with text that named ClickHouse. That sent browser and agent clients into re-login loops for a fault only an operator can fix. RFC 0010 keeps unauthenticated and forbidden for the authentication layer.

  • CompiledQueryError has two new fields that never appear in the envelope: code (for example HQ_EXECUTOR_CREDENTIALS_REJECTED, HQ_EXECUTOR_ACCESS_DENIED, HQ_EXECUTOR_DATABASE_MISSING or HQ_EXECUTOR_READONLY_MISCONFIGURED) and diagnostic, a sentence for operators. hypequery.serve logs both to the hypequery.serve logger with the request id and query id. The readonly guidance that used to be the error message is now the diagnostic. CLI hints are keyed by code.

  • hypequery.contrib.flask.blueprint exempts an API that reads a header credential (bearer, API key, JWT) from Flask-WTF's CSRFProtect, whether CSRFProtect is set up before or after the blueprint is registered. A blueprint for an API on the app's own login (flask_session) is still checked, so the page sends the token as X-CSRFToken.

  • ClickHouseConnection.from_url() reads clickhouse://, clickhouses://, http(s):// and SQLAlchemy HTTP-interface URLs (clickhousedb://, clickhouse+http://); from_env() (and the CLI) also read CLICKHOUSE_URL, which cannot be combined with the field variables. Native-protocol URLs are refused with an explanation, and no error repeats the URL.

  • Dataset results carry column_types from the dataset definition and convert to typed DataFrames with to_pandas(), to_polars() and to_arrow() (new pandas, polars and arrow extras). Timestamps become timezone-aware datetimes in the query timezone, counts nullable integers, numbers floats or, with decimals="decimal", exact decimals.

  • New hypequery.contrib.django (django extra): HYPEQUERY settings validated by manage.py check, a per-process client closed at exit, and middleware giving each request a lazily built, tenant-bound request.analytics (in async views, an awaitable client whose tenant function runs off the event loop, so it may use the ORM).

  • New hypequery.contrib.flask (flask extra): a Hypequery extension with app.config keys, a tenant_loader, and per-request tenant-bound client / async_client.

  • New hypequery.contrib.sqlalchemy: executor_from_engine() and connection_from_engine() point hypequery at a SQLAlchemy engine's ClickHouse server with hypequery's own clients.

  • New create_async_from_sync_clickhouse_executor(): a thread-backed async executor that needs only the clickhouse extra. AsyncFromSyncClickHouseExecutor.close() now also closes its driver clients.

  • Served query metadata omits cache when the dataset client has no result cache, matching TypeScript. It previously reported {"hit": false} on every query, which read as a cache miss.

  • hypequery init and hypequery generate datasets write a table's ClickHouse COMMENT as the dataset's description= and a column's as the dimension's, so client contracts and MCP agents describe datasets from the catalog instead of a generic fallback. Whitespace runs become one space and control characters are dropped to fit the deployment contract; a comment over 4096 bytes is shortened with a review warning. schema.json records the comments, only where set.

  • Protocol deployment validation now accepts merge measures with a stateFunction from the allowlist (RFC 0022), with the same rules as TypeScript: no filters or sql, and in contract 3 an approximate marker exactly for estimating states. This adds protocol representation only; Python code-first datasets do not yet author merge measures.

  • Protocol validation accepts the isNull and isNotNull comparisons (RFC 0023), with the same rules as TypeScript: an expression comparison has left and no right, deployment filter operator lists and client contract filters may name them on any type, and a client query filter using one has no value. A value on them is rejected. This adds protocol representation only; Python code-first datasets do not yet author null-check filters.

  • Protocol deployment validation now preserves explicit rowResolution FINAL policies, including the engine-less {"kind": "final"} form, replacing soft-delete columns and collapsing sign columns, with the same strict grammar as TypeScript. This adds protocol representation only; Python code-first datasets do not yet author resolved sources.

  • ServeRequest.scheme, used by the same-origin check's own-origin comparison, is filled in by every adapter (by Django and Flask themselves, so SECURE_PROXY_SSL_HEADER and ProxyFix apply); a trusted proxy's X-Forwarded-Proto overrides it.

  • Every response from hypequery.serve now carries X-Content-Type-Options: nosniff and Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; a route that sets its own policy keeps it. The Invalid host header refusal is sent with Cache-Control: no-store.

  • The development docs page (development_docs=True) loads an exact Redoc release checked by Subresource Integrity, instead of whatever redoc@2 currently is, under a Content-Security-Policy that allows only that script, and sends Referrer-Policy: no-referrer.

  • A request body must be UTF-8 JSON as JSON.parse reads it: NaN and Infinity, strings holding a lone surrogate ("\ud800") and nesting deeper than 64 levels are refused with 400 VALIDATION_ERROR. A lone surrogate in a filter value, and a deeply nested one, used to fail as a 500; UTF-16 and UTF-32 bodies are no longer guessed at.

  • The django extra requires sqlparse>=0.6.0 and the flask extra jinja2>=3.1.6, the first releases free of published advisories; Django and Flask themselves admit older ones.

  • The sdist no longer picks up local coverage data, reports, .env files or SQLite databases from the checkout it is built in, and the release artifact audit refuses an archive that contains them.

  • hypequery.serve no longer needs FastAPI (decision 0023 step 3). create_api(auth=...) returns a framework-neutral Api, modelled on TypeScript's createAPI: auth= (renamed from authenticate=), credentials=, tenant= (renamed from resolve_tenant=), security=, production=, limits=, events=, base_path= (replacing prefix=), max_body_bytes=, development_docs=, openapi= and client_mount_path=. datasets={"name": dataset} serves POST /datasets/<name>/query, metrics={"name": create_metric_endpoint(...)} serves POST /metrics/<name>, and with either GET /discovery is served, authenticated by default. api.add_route(method, path, handler) serves your own routes; await api.handle(ServeRequest(...)) serves a request with no server.

  • New to_asgi_app(api, lifespan=...): a pure ASGI application with no Starlette, for uvicorn and any ASGI server, or to mount in FastAPI, Starlette or Litestar. It streams the request body lazily under the body policy, cancels a query whose caller disconnects, runs the optional lifespan context and closes websockets. With a production profile, writing the response is inside the request's admission slot and deadline.

  • Development OpenAPI (3.1) and docs come from the routes' own Pydantic schemas (openapi_document(api), OpenApiInfo): GET /openapi.json and a Redoc page at GET /docs, as in TypeScript. /redoc is gone.

  • start_server(api), run_dev(api) and hypequery dev run an API or its to_asgi_app application on uvicorn. The fastapi extra is replaced by server, which installs only uvicorn; hypequery.serve itself needs no extra (anyio is now a base dependency).

  • Removed, before the first PyPI release: ServeRouter, create_router, create_app, install_http_security, request_id(request) (read call.state.request_id), the RateLimit FastAPI dependency (pass limits= instead) and the routers= arguments of export_client_contract and add_client_contract_endpoint. export_client_contract takes the API or its ASGI application. ServeError is no longer a FastAPI HTTPException.

  • Routing matches the path as sent, as TypeScript does: a trailing slash is 404 NOT_FOUND instead of FastAPI's 307 redirect, and a percent-encoded path no longer matches its decoded route. Every other answer is byte-identical to the FastAPI layer's, header order aside.

  • hypequery.serve.from_context(extract) authenticates with the host app's own login, as TypeScript's fromContext does: create_api(auth=from_context(extract)) hands extract the ServeRequest, which reads the session from request.raw and returns a Principal or None (401). A same-origin React page then calls the endpoints with its session cookie and no token. With same_origin=True, the default, the core refuses POST, PUT, PATCH and DELETE with 403 FORBIDDEN unless Sec-Fetch-Site is same-origin or none, Origin is the request's own origin (its scheme and Host, as Django's CSRF check compares them), or Origin is in the API's HttpSecurity.cors.origins, under every adapter. A browser without Fetch Metadata (Safari before 16.4) calling its own site needs no CorsPolicy entry. A sync extractor runs on the request's own thread under WSGI, Django and Flask (ServeRequest.run_on_request_thread).

  • hypequery.contrib.django.django_user(to_principal) and hypequery.contrib.flask.flask_session(to_principal) build these strategies. django_user reads request.user, through the async ORM in async views; flask_session reads Flask-Login's current_user when the app uses Flask-Login, else session. Django's urls(api) leaves Django's CSRF middleware active for them, so the page sends X-CSRFToken; header credentials (bearer, API key, JWT) stay CSRF-exempt.

  • Production now accepts from_context and refuses from_context(..., same_origin=False), replacing the flat cookie ban: ProductionProfile(cookie_auth=...) is removed, and bearer_token/api_key refuse the cookie header in any profile.

  • The Django and Flask examples serve their API behind their own login for a same-origin React page, and behind a bearer token at /api/agents for the MCP bridge.

  • jwt_strategy and analytics_token_issuer in hypequery.serve, with a new jwt extra (PyJWT and cryptography). jwt_strategy(secret=... | jwks_url=..., issuer=, audience=) is an authenticator for create_api that verifies JWT bearer tokens and maps sub, org_id, roles and scope/scopes to the Principal, as TypeScript's createJwtStrategy does. analytics_token_issuer(secret) mints the short-lived tokens createAnalyticsTokenIssuer mints. Tokens from either language verify in the other; shared fixtures (specs/serve-http/fixtures/jwt-v1) pin which tokens both accept and refuse. Key sets are fetched off the event loop with a timeout and cached; an unreachable key set answers 503. Python is stricter than TypeScript in a few places: it refuses tokens without sub, HMAC secrets shorter than the hash, and http key-set URLs outside localhost.

  • Django's urls(api) now checks the request's Host against ALLOWED_HOSTS itself, so an API without an HttpSecurity is protected even in a project without CommonMiddleware.

  • ServeRequest.is_disconnected is replaced by wait_disconnected, an awaitable that returns once the caller has gone. The core no longer polls for disconnection every 50 ms per running query.

  • The default tenant resolver runs inline instead of on a worker thread for every authenticated request.

  • A WSGI, Django or Flask request arriving after interpreter exit has stopped the serving loop is refused instead of starting a loop that is never closed.

  • Removed the re-export modules hypequery.serve.security, hypequery.serve.rate_limit, hypequery.serve.request_ids and hypequery.serve.errors; import those names from hypequery.serve.

  • Django and Flask serve hypequery's HTTP endpoints natively, for @hypequery/react and @hypequery/mcp, with no FastAPI and no second app (decision 0023 step 4). hypequery.contrib.django.urls(api) returns URL patterns to include() below any prefix: the view is async under ASGI and sync under WSGI, ServeRequest.raw is the HttpRequest, and the body is read from the request's stream, bounded by the API's body policy rather than DATA_UPLOAD_MAX_MEMORY_SIZE. An API whose credential is a header (bearer, API key, JWT) is CSRF-exempt; one on the site's own login (django_user) stays under Django's CSRF middleware, so the page sends X-CSRFToken. hypequery.contrib.flask.blueprint(api) returns a blueprint to register below any url_prefix; raw is Flask's request, and host code runs in the request's Flask context. Client contracts and discovery name the paths a request was routed under, unless client_mount_path is set.

  • hypequery.serve.to_wsgi_app(api) serves an API to gunicorn, waitress or any WSGI framework. Sync hosts run the API on one event loop per process, on its own thread, while the request's thread waits and reads the body for it, so no loop is created per request and threaded servers serve requests concurrently. The loop restarts in each forked worker and stops at exit.

  • Api.handle and Api.serve take mounted_at=, the prefix a host framework's router served the request under, for adapters mounted inside one.

  • HttpSecurity(allowed_hosts=...) now matches bracketed IPv6 literals ([::1] allows [::1]:8000 and [0:0::1]), compares names case-insensitively and ignores a trailing dot. An allowed host that is not a valid host, such as [nope], is refused when the profile is built.

  • With CORS configured, every response carries Vary: Origin, including responses to requests without an Origin, and preflight answers also vary on Access-Control-Request-Method, -Headers and -Private-Network. This keeps shared caches from serving one origin's answer to another.

  • Avoid terminating the process holding a dataset regeneration lock on Windows.

  • Give the ClickHouse installation hint only when that driver is the missing app dependency, including when the executor factory reports it.

  • Redact application import errors in development reload workers as well as initial startup.

  • Add authenticated add_client_contract_endpoint discovery for installed strict client endpoints. Apply each target's role/scope/tenant policy before projection, sign the caller's authorized view, and return no-store responses that vary by credential transport. Keep metric-only/empty views valid, enforce a bounded response budget without truncation, and expose no tenant values or physical SQL.

  • Add hypequery export client-contract --factory module:factory for offline canonical artifacts, including JSON-only stdout, bounded input/output handling, read-only drift checks and explicit atomic replacement. Validate both signed contracts and prepared identities, redact factory chatter/import failures, and preserve concurrent edits and file permissions without importing serving/driver extras. The explicitly named project factory should use definition-only SDK APIs.

  • Add definition-only ClientEndpointDefinition, ClientQueryProfile, and export_client_contract APIs for deterministic frontend/MCP build artifacts. Export logical dimensions, safe related measures, target-authorized filters, metric output aliases, effective definition limits and ordered base defaults without importing FastAPI, connecting to ClickHouse, or exposing physical SQL.

  • Add portable client-contract v1 models, bounded JSON decoding, canonical identities, and strict HTTP/MCP query validation for generated React and MCP integrations. Validate the same shapes, safe failures and query profile as TypeScript through a required shared conformance family.

  • Add a runnable Python → generated React → HTTP MCP tutorial with tenant-scoped ClickHouse serving, decimal/null seed data, per-login browser cache isolation and installed-artifact preparation.

  • Add opt-in client_profile bindings for Python dataset/metric endpoints and router/application client-contract export using resolved FastAPI paths. Enforce the advertised strict query profile, clamp against actual policy, client and production ceilings, and check X-Hypequery-Client-Schema only after authorization. Reject stale pins with a safe HTTP 409. Legacy endpoints retain their input behavior and are omitted from client exports. Add trusted client_mount_path configuration and the matching default CORS allow-header.

  • Disable automatic ClickHouse driver sessions for owned query/control clients so overlapping HTTP and worker queries can share an executor without session-lock failures.

  • Add ClickHouseConnection.from_env(), reading CLICKHOUSE_HOST, CLICKHOUSE_PORT, CLICKHOUSE_DATABASE, CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD and CLICKHOUSE_SECURE. Malformed ports and booleans are refused by variable name. An unset port lets the driver choose 8123, or 8443 when secure. The CLI, project scaffolds and examples now use it, so scaffolded apps honour CLICKHOUSE_SECURE.

  • ClickHouse executors are context managers (with / async with) that close their connections on exit.

  • The async executor's cancellation-control connection now uses the same 2-second receive timeout as the sync executor's.

  • Fix deployment contracts that widened a filter declared with an empty operators allow-list to every operator. The empty list now reaches protocol validation and is rejected, matching TypeScript, so a deployment can no longer accept filters the local planner refuses.

  • Fix hypequery init and hypequery generate datasets emitting a SQL-backed totalCount measure without dependencies, which made every generated project fail to build a deployment contract and refused relationship joins. totalCount now counts the first supported non-nullable column. All-nullable tables instead receive the explicitly named nonNullCount, with a warning that it excludes nulls.

  • Fix deployment of measure filters with integer values, such as filter.gte("amount", 10), which failed with HQ_VALUE_INTEGER_TAG_REQUIRED. An exactly representable integer now publishes as the same binary64 number TypeScript writes, with an identical deployment identity. Integers beyond 2**53 are refused with a clear error rather than rounded.

  • TenantDatasetClient.execute and AsyncTenantDatasetClient.execute accept paginate=True, as the unbound clients do, so request handlers holding a tenant-bound client can fetch pages with has_more metadata.

  • Bind timestamp parameters as Unix seconds. RFC 3339 filter values such as 2026-10-25T01:30:00Z were rejected by ClickHouse, and aware datetimes could resolve to the wrong instant on a non-UTC server in a repeated daylight-saving hour. Values without an offset are passed through unchanged.

  • Add run_dev and python -m hypequery.serve.dev app:app for local development. The runner binds to loopback by default, warns with ExternalBindWarning when bound beyond it, supports --reload, and refuses apps created with a ProductionProfile.

  • Add a README Getting started path from install to a served dataset, run as written from a built wheel against ClickHouse in CI.

  • Add a validated ASGI production profile and Uvicorn runner with explicit bind and proxy trust, startup refusals for debug/docs/reload/cookie auth, bounded admission, cancellation-aware deadlines and database/HTTP result ceilings.

  • Qualify shared HTTP fixtures under the production profile, test the real runner and add the production HTTP path to the live ClickHouse matrix.

  • Add authenticated dataset and measure-backed metric HTTP endpoints with role, scope, tenant, and page-size policy; support synchronous and asynchronous clients.

  • Add accurate over-fetch pagination without changing ordinary client executions.

  • Return public operational metadata separately from authorized, audited, redacted SQL diagnostics. Semantic measures use strings on the HTTP wire, matching TypeScript; local client result values retain their native types.

  • Propagate disconnect and handler cancellation to execution; optionally emit validated, metadata-only RFC 0011 success and failure events.

  • Add bounded logical discovery and an application factory with docs and OpenAPI disabled by default. Development documentation requires explicit opt-in.

  • Run shared semantic HTTP fixtures in Python and TypeScript CI, including the FastAPI and Uvicorn floors.

  • Breaking (pre-release): every authoring helper now lives in exactly one namespace: dimension.*, measure.*, filter.*, order.* and the new formula.* (formula.divide, formula.null_if_zero, formula.round, …). hypequery.datasets no longer re-exports the loose helpers. That covers the aggregations (sum, count, min, max, …), the filters (eq, in_list, …), the orderings (asc, desc) and the formula functions (divide, round, …). The package no longer shadows Python builtins such as sum, min, max and round. Write measure.sum("amount"), filter.eq("status", "paid"), order.desc("revenue") and measure.derived(formula.divide("a", "b")).

  • Breaking (pre-release): hypequery.serve.validate_correlation_id is now sanitize_correlation_id. The new name says it returns the caller's id or None instead of raising. It no longer shares a name with the planner's validate_correlation_id, which raises.

  • create_api and start_server are now the definitions, matching TypeScript, and their error messages use those names. create_router and run_production remain as aliases.

  • Add measure.derived(formula) over local numeric measures, including validated references/dependency cycles, aggregate planning, HAVING, HTTP serving, and safe discovery.

  • Emit derived formulas and inputs in catalogs, semantic snapshots and deployment contract 2, with shared TypeScript fixtures. Chained derived measures execute locally but cannot be exported under contract 2; relationship traversal remains limited to base measures.

  • Formula limits and literal types match TypeScript: at most 256 nodes and 16 levels per formula, and integer literals keep integer arithmetic (add("orders", 1) returns an integer).

  • Serialize relationship-qualified measures as strings (preserving NULL), like local measures, without changing dimension values or native client results.

  • Allow X-Include-Meta in the default CORS request-header policy so React infinite dataset and metric queries can request pagination across origins.

  • Stop suggesting totalCount for tables with no supported non-nullable column. Generate nonNullCount with an explicit warning instead, and use it in the scaffold example. It counts non-null values, not every row. Review existing totalCount references when regenerating definitions for these tables.

  • Add explicit time_grains policies to Python datasets, enforced by the planner and dataset/metric HTTP endpoints and reflected in catalogs, semantic contracts and OpenAPI. Unsupported grains are rejected with TypeScript's message, Unsupported time grain "hour". Supported: day, month.

  • Deployment export refuses a policy that excludes any day-through-year grain, because contract 2 cannot preserve it. A policy that includes every one of those grains publishes unchanged, matching TypeScript.

  • Add validated query and client timezones (UTC by default), apply the zone to time buckets and local time-key filter bounds, and accept timezone overrides on dataset and metric HTTP endpoints.

  • Behavior change: time buckets and offset-free time-key bounds previously used the column's or server's timezone; they now use the query zone, UTC unless set. Pass timezone= to create_dataset_client to keep a non-UTC zone. Persistent result caches populated before upgrading can return old buckets until their entries expire.

  • Partition non-UTC cached results using the same deployed-identity convention as TypeScript.

  • Behavior change: minute, hour and day buckets now return period as wall-clock text in the query timezone, YYYY-MM-DD HH:MM:SS (RFC 0015), matching TypeScript. They were previously UTC ISO instants such as 2026-01-02T05:00:00+00:00. Week-and-longer buckets are unchanged (YYYY-MM-DD). A shared fixture, specs/datasets/period-format-v1.json, now pins the form in both languages.

  • Add Python release automation with opt-in canaries after successful main CI, tagged beta/RC/stable releases, build-only rehearsals, artifact and CLI checks, and token-free PyPI Trusted Publishing.

  • Prepare Python beta/stable release PRs with Towncrier, explicit version selection, synchronized package metadata and lockfile, preserved changelog history, and Python CI against the generated release commit.

  • Audit locked and minimum runtime dependency resolutions across supported Python versions, and retain release SBOMs, audit reports and distribution checksums.

  • Require urllib3 2.8.0 or newer for ClickHouse execution, fixing three published advisories. Raise core and serving dependency floors to the versions qualified by the beta suite.

  • Preserve nulls for unmatched related dimensions and filters independently of ClickHouse join_use_nulls, including queries without related measures. Missing or other-tenant targets no longer appear as empty strings or zero values.

  • ResultCache(on_error=...) observes cache failures that would otherwise be silent. Failures still never fail a query. The hook receives the stage ("key", "get" or "put") and the exception; without it, failures are logged at debug level by exception type only. ExecuteOptions types the options tenant-bound clients forward, and DEFAULT_PAGE_SIZE names the default page size.

  • Select one-hop relationship base measures as <relationship>.<measure>, matching @hypequery/datasets. Duplicate-insensitive aggregates are allowed through belongs_to and every aggregate through has_one. Unmatched target rows never feed an aggregate, and target tenant and fixed-filter scoping are preserved. Catalogs, ordering and discovery advertise the safe names.

  • Reject a query that selects one name as both a dimension and a measure.

  • Add having conditions on selected measure values, matching @hypequery/datasets (HQ-329). Operators are eq, neq, gt, gte, lt, lte, between, in and notIn. Values are finite numbers bound as parameters, and conditions count against max_filters. Dataset endpoints forward having; metric endpoints refuse it. Queries with conditions bypass the result cache until the cache preimage carries them.

  • Support composite relationship keys (RFC 0016), matching @hypequery/datasets. Pass keys=(("customer_id", "id"), ("region_code", "region")) to a relationship helper; every pair is AND-ed in the join, and related-measure projections keep every target key. Catalogs, semantic contracts and deployment contracts carry keys, and protocol validation accepts and checks them. Single-key relationships are unchanged.

  • Align Python CLI names with TypeScript: init --path, dev --hostname, -p, --no-watch, -V and help [command]; preserve positional destinations, --host and --no-reload as aliases.

  • Add framework-free hypequery and python -m hypequery CLI entry points, help/version and the init/dev command contract (PYE-01A).

  • Add hypequery init [directory] with packaged Python project templates, authenticated orders endpoint, setup instructions and collision protection (PYE-01B).

  • Inspect the configured ClickHouse schema during hypequery init, generate typed dataset definitions and a schema snapshot, and serve selected tables through authenticated endpoints. Match TypeScript's table selection and semantic naming; report unsupported columns, collisions and tenant candidates. Count nullable rows accurately. The offline demo requires --skip-connection.

  • Add hypequery dev [module:app] with reload enabled by default, safe local runner policies, --no-reload, and actionable startup errors (PYE-01C).

  • Gate Python CLI onboarding on wheel/sdist installs outside the checkout across Python 3.11–3.14, including a real authenticated ClickHouse query (PYE-01D).

  • Verify schema-backed generation from installed wheel/sdist, serve discovered tables, and compare query results with raw ClickHouse SQL. Keep explicit offline example checks for base installations without driver extras.

  • Align Python public authoring names with TypeScript: add dataset, dimension.string/number/boolean/timestamp, base measure helpers, filter and order namespaces, create_memory_cache_store, create_api, endpoint factories, serve_dev, and start_server. Existing constructors, callable dimension helpers and run_dev remain available; use the namespaces above for aggregation, filter, ordering and formula helpers.

  • Add hypequery generate datasets (alias generate:datasets) using live ClickHouse metadata, with TypeScript-equivalent --output, --path, table selection, --diff, --check, --force and --tenant-column options. It defaults to datasets.py, matching the project layout created by hypequery init. Read-only drift checks exit 1 without writing; replacement is atomic and preserves the existing file's permissions. --force replaces the whole file and warns when it removes a configured tenant_key; --tenant-column sets tenant_key on tables that have that column and reports tables that do not.

  • Reject destinations changed during discovery or writing, coordinate CLI writers through replacement, and accept concurrently created parent directories. Installed wheel/sdist journeys now restart serving and compare the regenerated column's query response with raw SQL.

  • Add explicit synchronous and asynchronous relationship uniqueness checks. Checks respect trusted tenant scopes, deadlines and cancellation, validate complete composite target keys, and report duplicate to-one keys without automatically running database queries.

  • Require aiohttp 3.14.4 or newer with the clickhouse-async extra, fixing three published advisories (CVE-2026-111063, CVE-2026-111064, GHSA-2g87-pp6c-29x5).

  • Retry server-side KILL QUERY from the async executors until ClickHouse confirms it or the driver call returns. A cancellation or deadline that landed before the query registered on the server previously let the query run to max_execution_time while holding its worker slot; the synchronous executor already retried. AsyncFromSyncClickHouseExecutor keeps retrying for as long as its driver call runs, so a submission slower than max_execution_time cannot escape cancellation; the native async executor stops when its driver request ends if the server cancels read-only queries on a closed request (clickhouse-connect enables this whenever the user may change it), and otherwise at the server's own time limit. aclose() on either async executor stops its outstanding retries.

  • The result cache treats a stored value that cannot answer the query as a reported get failure and a miss. That covers anything other than CachedRows, rows that don't match the columns, non-scalar values, and columns that differ from the query's. Previously a malformed value failed the request, and a well-formed entry with other columns was served as a hit. ResultCache.get accepts the expected columns.

  • Canonicalize text containing unpaired UTF-16 surrogates as JSON.stringify does, writing a \\udXXX escape, instead of failing with a UnicodeEncodeError. Deployment and bundle contracts with such a label or description now produce the same canonical bytes as TypeScript.

  • hypequery init and hypequery generate datasets now work for least-privilege ClickHouse users. Schema discovery sent readonly = 1 on every catalog query, so a readonly = 2 user failed with "Cannot modify 'readonly' setting" and a readonly = 1 user was refused on the query limits. Discovery now follows the executor: it leaves out readonly for a user whose profile is already read-only, keeps the time and row limits for readonly = 2, and relies on a SQL LIMIT for readonly = 1, which may not change any setting.

  • When discovery fails, init and generate now name what to check, as dev does: credentials, host and port, database, or grants. Previously every failure gave one generic message.

  • hypequery dev keeps its documented exit codes: a busy port now exits 1 with a hint naming the port, instead of exiting 3. A missing environment variable, such as HYPEQUERY_DEV_TOKEN, is named instead of reported as a generic failure. ClickHouse driver warnings, such as "Unexpected Http Driver Exception" and urllib3 retry lines that can include bound parameter values, no longer print ahead of the CLI's own hint.

  • hypequery init prints cd <directory> && hypequery dev when the project was created somewhere else. The generated .env.example lists CLICKHOUSE_SECURE. The export client-contract --check hint now says to review with --stdout and replace with --force.

  • Canonicalize client-contract numbers beyond 2**53 with ECMAScript's shortest round-trip digits, matching TypeScript's JSON.stringify. Contract identities and schema hashes containing such a value no longer differ between the two implementations.

  • serialize_semantic_contract and hash_contract no longer raise UnicodeEncodeError when a label or description contains an unpaired UTF-16 surrogate. Such a character is escaped as \udXXX, as JSON.stringify does, so the hash matches TypeScript's for the same model. hypequery.protocol now exports serialize_protocol_string.

  • Lower the dependency floors to measured minimums: pydantic 2.9.2, FastAPI 0.137.2, Starlette 1.3.1, Uvicorn 0.30.6 and clickhouse-connect 1.4.2 (urllib3 stays at 2.8.0). Each is the oldest release that passes the full suite, so hypequery installs next to projects that pin older releases of those libraries.

  • Add validate_deployable(dataset), which refuses derived measures the deployment contract cannot carry (a reference to another derived measure, or a literal anywhere other than round decimals or a coalesce fallback) with an error naming the dataset and measure, for example orders.doubled: a numeric literal cannot be an arithmetic operand in a deployable formula. build_protocol_deployment_contract() and write_dataset_bundle() run the check first instead of failing with only a contract path.

  • Order time buckets by their instant rather than their text label. On a daylight-saving fall-back day, two hourly buckets share a label such as 01:00:00, and they could come back in either order. TypeScript already orders them chronologically.

  • The embedded_analytics example is now tenant-scoped. Its dataset declares tenant_key="org_id", the endpoint requires a tenant, and the example token belongs to organization "a". Previously every caller saw every organization's orders.

  • Served dataset and metric endpoints no longer expose related datasets' fields by default. Previously a caller refused by the shipments endpoint could still read shipment.carrier through the orders endpoint, and the client contract advertised those fields. Related dimensions, related measures, and named filters defined on a related field now need include_related={"shipment"} on create_dataset_endpoint, add_dataset_endpoint or the metric equivalents. Without it, they are refused with 400 VALIDATION_ERROR and left out of the endpoint's client contract. So are measures whose fixed filters read a related field, directly or through a derived measure; an omitted measures list selects only the base measures the endpoint can serve, and a metric endpoint for such a measure is refused when created. ClientEndpointDefinition gains related for the same purpose, with the same default and checks, so an offline hypequery export client-contract matches the live contract of an endpoint installed with the same arguments. Pass related={"shipment"} where the endpoint passes include_related={"shipment"}; an offline metric whose measure filters through an excluded relationship is refused, as Serve refuses it.

  • Report a filter, ordering or having field name outside the portable identifier grammar as an input-invalid query error from DatasetClient.validate and execute. It previously escaped as a raw ProtocolIdentifierError (a TypeError), which non-Serve hosts answered as a server fault.

  • Add control_username and control_password to ClickHouseConnection, read from CLICKHOUSE_CONTROL_USERNAME and CLICKHOUSE_CONTROL_PASSWORD by from_env(), so the connection that sends KILL QUERY can sign in as its own user. Without them, the query user cancels its own queries and needs GRANT SELECT ON system.processes; a separate control user also needs KILL QUERY. The README now documents both grants.

  • When ClickHouse refuses a KILL QUERY, executors now log one error naming the missing grant and stop retrying, instead of sending a warning and dozens of refused KILLs per cancelled query.

  • validate_protocol_client_query(..., transport="mcp") refuses a limit above the endpoint's maxLimit instead of clamping it, matching TypeScript's MCP executor. HTTP input still clamps.

  • Return null for non-finite measure values instead of failing the query. ClickHouse reports nan for avg, stddev and variance over no matching rows and inf for a derived division by zero; such results previously surfaced as an internal error, so a metric endpoint whose filters matched nothing answered 500.

  • Refuse integer filter values outside ±2**53 on number fields with input-invalid. Number filters bind as Float64, which can't hold larger integers exactly, so ClickHouse compared a rounded value and an exact UInt64 or Int64 id silently matched nothing. Declare a large id as dimension.string() to filter it exactly.

  • Ship the Apache-2.0 LICENSE file in the wheel and sdist, and leave a stale internal review document out of the sdist.

  • Raise transitive minimum versions that the direct floors still admitted with published advisories: h11>=0.16.0 (via uvicorn) and anyio>=4.14.2 (via Starlette) in the fastapi extra, certifi>=2026.10.19 (via clickhouse-connect) in both ClickHouse extras, and idna>=3.15 (via anyio and aiohttp) in the fastapi and clickhouse-async extras.

  • create_async_clickhouse_executor now raises ModuleNotFoundError naming hypequery[clickhouse-async] when aiohttp is missing. With only hypequery[clickhouse] installed, it previously failed with an opaque internal error.

  • Rewrite the README's beta notice for PyPI, where the old relative link was broken. Document what each install extra is for, including clickhouse-async.

  • hypequery dev now says which ClickHouse setting to check when the app cannot connect: refused credentials, an unreachable host, a missing database or denied access each get their own hint instead of "App could not start". Wrong-password failures, which ClickHouse reports as REQUIRED_PASSWORD, WRONG_PASSWORD or UNKNOWN_USER, are now classified unauthenticated rather than internal, and an unknown database is not-found.

  • A ClickHouse user whose profile sets readonly = 1 refuses HypeQuery's per-query limits; that failure is now reported as forbidden with guidance to use a readonly = 2 user instead of an opaque internal error.

  • Deadline is exported from hypequery.datasets so an ExecutionContext deadline can be built from the public package.

  • Passing a dataset name instead of a Dataset to belongs_to, has_one or has_many raises a TypeError that says so, instead of an AttributeError.

  • AsyncFromSyncClickHouseExecutor no longer leaves "Future exception was never retrieved" in the logs when a caller cancels a query.

  • README: the relationship example names the physical customer_id column (join keys are columns, not dimensions), the result cache documents a store failure as a miss, and the bridge executor documents the session-free driver clients it needs.

  • Report a query that exceeds its result limit as too-large (HTTP 413) on ClickHouse 26, which renamed the error to TOO_MANY_ROWS_OR_BYTES (code 396). It was reported as internal (HTTP 500).

  • Exclude the Hypothesis example database from source distributions and refuse archives that contain one in the release artifact audit.

  • Serve routes now map Starlette's HTTPException like FastAPI's, keeping its status and string detail. They previously returned 500 for it, and FastAPI raises it itself for a body it cannot parse. So a body with invalid UTF-8, or one nested past the recursion limit, now gets 400 VALIDATION_ERROR instead of 500, without logging a traceback.

  • Serve routes refuse a request body with any Content-Encoding other than identity with 415. A gzip body previously got 500. The body cap counts bytes as they arrive, so a compressed body is never expanded.

  • Fix Serve requests that could hang until the client gave up, or until the production timeout (30 s by default), after the query had already finished. The request's disconnect watcher is now stopped with a flag rather than Task.cancel(), which Starlette's Request.is_disconnected() could absorb. The stall hit sequential requests too, and became frequent under concurrency or host load: about 60% of requests at 50 concurrent.

  • Refuse timestamp filter values that are not a real ISO 8601 date or date-time, such as 2026-13-45, 2026-02-30, yesterday or 2026-01-31 12:00, with input-invalid naming the filter. ClickHouse coerced some of these to a different instant instead of failing: 2026-13-45 became 1900-01-01 and 2026-02-30 rolled over to March 2, so the query silently returned wrong numbers. validate() now reports them too. Accepted forms are YYYY-MM-DD, or YYYY-MM-DD[T ]hh:mm:ss[.fraction], optionally followed by Z or ±hh:mm. like patterns on timestamp fields are unchanged.

  • Require pydantic 2.12.2 or newer on Python 3.14. pydantic-core publishes no Python 3.14 wheel for the previous 2.9.2 floor, and pydantic 2.12.0 rejects one of the serve request models on 3.14. Python 3.11–3.13 keep the 2.9.2 floor.

  • The README that hypequery init generates installs the pinned SDK from PyPI with pip install -e ., keeping the local-wheel route for unreleased builds.

Get early access to hypequery cloud.

Cloud hosts the same model you run in your backend, with a managed API and MCP server, versions, and rollback. We're letting teams in a few at a time. Join the waitlist and we'll reach out when your project can go live.

How do you want to build your analytics layer? (Optional)
What features do you most need? (Optional)

Pick as many as you like.

Only your email is required. All other questions are optional.