Python SDK

Flask

A Flask extension with app.config, a tenant loader, per-request tenant-bound clients, and hypequery's endpoints as a blueprint.

hypequery.contrib.flask is a Flask extension. It reads its connection from app.config or the environment, and hq.client gives each request a dataset client bound to that request's tenant.

Install

pip install "hypequery[flask,jwt]==0.1.0b2"

The flask extra includes the ClickHouse driver and Flask's async support.

Set up the extension

from flask import Flask
from flask_login import current_user
from hypequery.contrib.flask import Hypequery

hq = Hypequery()


@hq.tenant_loader
def current_tenant():
    return current_user.org_id if current_user.is_authenticated else None


def create_app():
    app = Flask(__name__)
    app.config["HYPEQUERY_REGISTRY"] = "analytics.datasets:registry"
    hq.init_app(app)

    @app.get("/revenue")
    def revenue():
        result = hq.client.execute("orders", {"dimensions": ["country"], "measures": ["revenue"]})
        return {"data": list(result.data)}

    return app

Hypequery(app) works too if you don't use an app factory.

Config keyDefaultMeaning
HYPEQUERY_URLNoneA ClickHouse URL. None reads CLICKHOUSE_URL or CLICKHOUSE_HOST, _PORT, ...
HYPEQUERY_REGISTRYNoneYour DatasetRegistry, or an import string such as "analytics.datasets:registry"
HYPEQUERY_READONLY_POLICY"query"See read-only policy
HYPEQUERY_TIMEZONE"UTC"The default query timezone
HYPEQUERY_MAX_CONCURRENT4Concurrent queries per event loop for the async client

init_app validates these without connecting, so a bad URL, registry path or timezone fails at startup.

Clients

AttributeReturns
hq.clientThe sync client bound to this request's tenant, built once per request
hq.async_clientThe async client bound to this request's tenant, for async def routes
hq.base_clientThe app's process-wide client, not bound to any tenant

The tenant loader runs inside the request, so it can read flask.request, g or Flask-Login's current_user. It returns a tenant id, a TenantScope, or None. With None, the client is unscoped and any dataset with a tenant_key refuses to run.

@app.get("/revenue/daily")
async def revenue_by_day():
    result = await hq.async_client.execute("orders", {"measures": ["revenue"], "by": "day"})
    return {"data": list(result.data)}

Connections and shutdown

The client lives for the process: it is created on first use, connects on its first query, is closed at interpreter exit, and is rebuilt in a forked worker. It is deliberately not closed in teardown_appcontext, which runs after every request. Call hq.close() inside an app context to close it sooner, for example in tests.

A decorator on an async def route must itself be async-aware. A plain sync wrapper, such as a hand-written login_required, returns the route's coroutine unawaited and Flask answers 500. Check inspect.iscoroutinefunction(view) and wrap with an async def.

The async client runs the sync HTTP driver on worker threads, so it needs no aiohttp session. Flask runs each async route on its own event loop; the extension keeps one client per live loop and closes it once that loop ends.

Outside a request (CLI commands, background jobs, setup code in create_app), hq.client_for(app) returns the app's client with no app context. It does not connect until the first query, so the app starts even while ClickHouse is down.

Serve hypequery's endpoints

The generated React hooks and the MCP bridge call hypequery's own HTTP endpoints. Flask serves them natively as a blueprint, with no second application and no FastAPI: build the API with create_api and register blueprint(api) under any prefix.

from flask import Flask
from hypequery.contrib.flask import Hypequery, blueprint
from hypequery.serve import create_api, jwt_strategy

from analytics.datasets import orders

hq = Hypequery()


def create_app():
    app = Flask(__name__)
    app.config["HYPEQUERY_REGISTRY"] = "analytics.datasets:registry"
    hq.init_app(app)

    api = create_api(
        auth=jwt_strategy(secret=app.config["ANALYTICS_JWT_SECRET"], audience="analytics"),
        client=hq.client_for(app),
        datasets={"orders": orders},  # POST /api/analytics/datasets/orders/query
    )
    app.register_blueprint(blueprint(api), url_prefix="/api/analytics")
    return app

Every request below the prefix reaches the API, which answers exactly as it does under any other adapter: authentication before the body, the body policy, the security profile, request ids and the error envelope. ServeRequest.raw is Flask's Request.

  • One event loop. The view is sync. The API runs on one event loop per process, started on its own thread at the first request, while the request's thread waits. A threaded server (gunicorn --threads, waitress) serves requests concurrently on it. Your authenticator and a sync dataset client see the request thread's context, so Flask's request and g work in them.
  • Bodies are read from request.stream, lazily and bounded by create_api(max_body_bytes=...). A body over MAX_CONTENT_LENGTH is refused as too large.
  • Any prefix works, including one with URL variables. Client contracts and discovery name the paths the request was routed under.
  • CSRF. A header credential (Authorization: Bearer, an API key or a JWT) is one a browser never attaches on its own, so a blueprint for it is exempt from Flask-WTF's CSRFProtect, whether you set that up before or after registering the blueprint. CSRFProtect keeps checking the rest of your app, and an API on your own login (below).

Then point the React hooks and the MCP bridge at the prefix; see React and MCP clients.

Your own login, with no token

A React page served by your Flask app can call the endpoints with the session the user already has. With Flask-Login, flask_session passes your mapping the current_user:

from hypequery.contrib.flask import blueprint, flask_session
from hypequery.serve import Principal, create_api

api = create_api(
    auth=flask_session(lambda user: Principal(subject=user.get_id(), tenant_id=user.org_id)),
    client=hq.client_for(app),
    datasets={"orders": orders},
)
app.register_blueprint(blueprint(api), url_prefix="/api/analytics")

The mapping receives a signed-in user only; an anonymous one gets 401. Without Flask-Login, the mapping receives Flask's session instead and returns None when nobody is signed in:

def from_session(session) -> Principal | None:
    user_id = session.get("user_id")
    return Principal(subject=user_id, tenant_id=session["org_id"]) if user_id else None


api = create_api(auth=flask_session(from_session), client=hq.client_for(app), datasets={"orders": orders})

Either way it runs on the request's own thread, so Flask-Login's user loader and thread-local database sessions behave as in your routes.

A session cookie rides along with any request the browser makes, so these endpoints are protected from cross-site requests:

  • The same-origin check refuses an unsafe method (POST, PUT, PATCH, DELETE) with 403 unless the browser marks the request same-origin, or its Origin is the request's own or in your CORS policy. Keep it on (same_origin=True, the default). Without Flask-WTF, it is the protection.
  • Flask-WTF's CSRFProtect, if your app uses it, checks them too. Render the token into the page that hosts React ({{ csrf_meta_tag() }}) and send it as X-CSRFToken:
const csrfToken = () => document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
const client = createHypequeryClient({
  baseUrl: '/api/analytics',
  headers: () => ({ 'X-CSRFToken': csrfToken() }),
});

Without the token, Flask-WTF answers before hypequery does, with its own HTML 400.

Running Flask and another app on 127.0.0.1 at once? Browsers don't separate cookies by port, so two local apps share any cookie with the same name. Give each its own SESSION_COOKIE_NAME, or open one on localhost and the other on 127.0.0.1.

For agents and other non-browser callers, serve a second API with a header credential (jwt_strategy or api_key) under another prefix. The tenant always comes from the principal either way.

Runnable example

examples/flask_app.py has a sync route, an async route, and hypequery's endpoints served through blueprint(api) both behind the Flask login (for a same-origin React page) and behind a bearer token (for @hypequery/mcp). It runs against a live ClickHouse in CI.

To start from your own schema instead, hypequery init --framework flask writes a Flask app.py with datasets for your tables and both APIs wired the same way. See Python CLI.

On this page

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.