Metadata-Version: 2.4
Name: honeyframeapi
Version: 0.1.8
Summary: Official Python client and CLI for the Honeyframe data platform
Author: HubStudio-id
License: Proprietary
Project-URL: Homepage, https://docs.honeyframe.io/
Project-URL: Documentation, https://docs.honeyframe.io/
Keywords: honeyframe,data-platform,analytics,dbt,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx<1,>=0.24
Provides-Extra: pandas
Requires-Dist: pandas>=1.5; extra == "pandas"
Provides-Extra: parquet
Requires-Dist: pandas>=1.5; extra == "parquet"
Requires-Dist: pyarrow>=12; extra == "parquet"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pandas>=1.5; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"

# honeyframeapi

The official Python client for the **Honeyframe** data platform. Script the
platform from a notebook, a job or a CI pipeline: read data into pandas, author
and build warehouse models, publish dashboards and webapps, run scenarios, and
manage connectors, knowledge banks and API services.

**Documentation: <https://docs.honeyframe.io/>** — this page covers the SDK;
the portal covers the platform behind it (Datasets, Recipes, Flow, Dashboards,
Webapps, Scheduler, API).

```python
from honeyframeapi import HoneyframeClient

client = HoneyframeClient("https://platform.your-domain.com", token="hf_…", project_id=1)
df = client.sql("SELECT region, SUM(amount) AS total FROM sales GROUP BY region")
```

One client, then project / dataset / dashboard handles hanging off it.

## Install

```bash
pip install honeyframeapi              # core (httpx only)
pip install "honeyframeapi[pandas]"    # + pandas for get_dataframe() / sql()
pip install "honeyframeapi[parquet]"   # + pandas and pyarrow for to_parquet()
```

Python 3.9 or newer. Installing the package also installs the `honeyframe`
command-line tool (see [CLI](#cli)).

## Authenticate

The recommended credential is a **Personal Access Token (PAT)** — a long-lived
`hf_…` token you mint from the platform UI (avatar menu → *Personal access
tokens*) or with `client.create_pat()`. Unlike the short-lived session token it
does not expire on the hour, so it is the right fit for scripts and automation.

```python
import os
import honeyframeapi

# 1. a Personal Access Token (recommended) — read it from the environment or
#    a secret store, never from a literal in the script
client = honeyframeapi.HoneyframeClient(
    "https://platform.your-domain.com",
    token=os.environ["HONEYFRAME_TOKEN"],
    project_id=1,
)

# 2. email / password for interactive use — the client signs in and signs in
#    again when the session expires; prefer a PAT for unattended jobs
import getpass
client = honeyframeapi.HoneyframeClient(
    "https://platform.your-domain.com",
    username="you@your-domain.com", password=getpass.getpass(),
    project_id=1,
)

# 3. from environment variables
#    HONEYFRAME_URL, HONEYFRAME_TOKEN (or HONEYFRAME_USERNAME + HONEYFRAME_PASSWORD),
#    HONEYFRAME_PROJECT_ID
client = honeyframeapi.HoneyframeClient.from_env()
```

The older `HUB_API_BASE`, `HUB_USERNAME`, `HUB_PASSWORD` and `HUB_PROJECT_ID`
names are still read as fallbacks but are deprecated: using one emits a
`DeprecationWarning`. Rename them to the `HONEYFRAME_*` names.

`client.create_pat()` mints a token that expires after 90 days by default
(`expires_days=0` for one that never expires, up to 730 days otherwise).

### Connection security

- `base_url` must be `https://`. Plain `http://` is accepted only for a
  loopback host (`localhost`, `127.0.0.1`, `::1`); for a trusted private
  network pass `allow_insecure_http=True` or set
  `HONEYFRAME_ALLOW_INSECURE_HTTP=1`. Otherwise the client raises
  `HoneyframeConfigError` before sending anything.
- `verify=` takes `True` (default), a CA bundle path (file or directory) for a
  private CA, or an `ssl.SSLContext`. `verify=False` works but warns.
- Redirects are never followed, so the token cannot be forwarded to another
  host. Every value the SDK puts into a URL path (dataset names, keys, slugs)
  is percent-encoded, so a name containing `/`, `?`, `#` or `..` cannot
  re-route a request.
- Exceptions never carry credentials: `exc.response.request` has no
  Authorization header or body, and a failed login keeps no response at all.
- Files the SDK writes for you (exports, bundle / folder / artifact
  downloads, PDFs, notebooks) are created readable by your user only (0600).

## Author the warehouse, end to end

One governed loop that adapts to the deployment:

```python
proj = client.get_project(1)
proj.engine()                      # 'dbt' or 'native' — does this install have dbt?
proj.upload_dataframe("raw_sales", df, materialize=True)   # push local data in
if proj.uses_dbt():
    proj.dbt.create_model("stg_sales", sql="SELECT … FROM {{ ref('raw_sales') }}")
    proj.dbt.run(select="stg_sales")   # author + build a dbt model
df = proj.sql("SELECT * FROM stg_sales")                   # read results back
proj.get_dataset("raw_sales").upstream()                   # trace lineage

# …then share it: dashboard → webapp → token-gated public link
dash = proj.create_dashboard("Sales")
dash.add_card("By region", "bar", "SELECT region, total FROM stg_sales")
app  = client.publish_webapp("sales", "Sales", config={"pages": [...]})
print(app.create_public_link(expires_days=7)["share_url"])   # default: 30 days
```

On a native (non-dbt) install the authoring lines change — check
`proj.engine()` — but the upload → read → dashboard → webapp → share steps are
the same.

## Read data into pandas

```python
# ad-hoc SQL in the current project
df = client.sql("""
    SELECT channel, COUNT(*) AS orders
    FROM marts.fact_orders
    GROUP BY channel ORDER BY orders DESC
""")

# or read a whole dataset
proj = client.get_project(1)
df = proj.get_dataset("fact_orders").get_dataframe()
head = proj.get_dataset("fact_orders").head(20)
stats = proj.get_dataset("fact_orders").profile()
```

## Build a dashboard from code

```python
proj = client.get_project(1)
dash = proj.create_dashboard("Orders overview",
                             description="Order channels and volume")

dash.add_card("Total orders", "kpi",
              "SELECT COUNT(*) AS n FROM marts.fact_orders",
              card_config={"color_theme": "ocean"}, size_x=6, size_y=2)

dash.add_card("Orders by channel", "donut",
              "SELECT channel AS name, COUNT(*) AS value "
              "FROM marts.fact_orders GROUP BY channel",
              card_config={"nameField": "name", "valueField": "value"})

results = dash.execute_all()
print("Open:", dash.url)
```

Already have card definitions as dicts? Pass them straight through:

```python
dash.add_cards(cards)   # list of {"title", "card_type", "sql_query", "card_config", …}
```

## Publish a webapp and share it by link

Wrap dashboard cards in a webapp and hand a stakeholder a URL they can open
without an account (token-gated, PII-masked).

```python
app = client.publish_webapp(
    "orders-overview", "Orders overview",
    config={"pages": [{"key": "home", "title": "Home", "blocks": [
        {"kind": "card_ref", "dashboard_id": dash.dashboard_id,
         "card_id": dash.cards[0].card_id},
    ]}]},
)

link = app.create_public_link(label="exec share", expires_days=30)  # 1..365; 0 = never
print(link["token"])                       # shown ONCE — store it
print(app.public_url(link["token"]))       # anonymous URL

# later: audit + revoke
for l in app.public_links():
    print(l["label"], l["status"], l["view_count"])
app.revoke_public_link(link["id"])
```

Public links expire after 30 days unless you pass another `expires_days`
(1–365); `expires_days=0` makes a link that never expires.

`publish_webapp` is project-scoped — set `project_id` on the client first.
If your webapps are served from a different host than the API, pass it:
`app.public_url(token, host="https://apps.your-domain.com")`.

### Build a multi-page app from code

The builders compose pages, blocks and filters for you — including two
flexible block kinds: `code_card` (a governed read-only SELECT rendered as a
chart, run server-side with an 8 s timeout and a 5,000-row cap) and
`html_block` (freeform HTML/CSS/JS). With `mode="sandboxed"` the block runs in
an opaque-origin iframe whose CSP blocks outbound network, so its script cannot
read the viewer's session or call out. Add `data_sql=` to a sandboxed block and
the server runs that governed SELECT and posts the rows into the iframe — your
custom JS renders real warehouse data without any API access of its own.

```python
from honeyframeapi import (
    page, nav_item, code_card, html_block, card_ref,
    filters, date_range_filter, entity_filter, pill_multi_filter,
    metric_search_filter, chrome,
)

app = client.create_webapp(
    "orders-overview", "Orders overview",
    pages=[page(
        "home", "Home",
        blocks=[
            card_ref(dash.dashboard_id, dash.cards[0].card_id, w=12, h=4),
            code_card("SELECT store_name, orders FROM marts.v_store_orders "
                      "WHERE :period_start <= order_date AND order_date <= :period_end",
                      "bar", x=12, w=12, h=4, title="Orders by store"),
            # a sandboxed block that renders REAL data: the server runs
            # data_sql and posts the rows into the iframe
            html_block("<div id=viz></div><script>addEventListener('message',e=>{"
                       "if(e.data?.source==='honeyframe')draw(e.data.rows)})</script>",
                       mode="sandboxed", w=12, h=8, title="Custom viz",
                       data_sql="SELECT store_name, orders FROM marts.v_store_orders"),
            html_block("<i>internal note</i>", public_hidden=True, w=12, h=2),
        ],
        # page-level filters, bound into the SQL above by parameter name
        filters=filters(
            metric=metric_search_filter(),                  # finds a card, binds nothing
            period=date_range_filter(locale="en", timezone="UTC"),  # :period_start / :period_end
            store=entity_filter("store_codes", mode="multi", chips=True,
                                options_sql="SELECT store_code AS value, store_name AS label "
                                            "FROM marts.dim_store"),
            # a fixed list → inline pills; binds :tier and :tier_codes
            tier=pill_multi_filter("tier", ["A", "B", "C"], label="Store tier",
                                   empty_value="ALL"),
        ),
    )],
    nav=[nav_item("home", "Home", icon="gauge")],
    # header/rail affordances — each one is off until you turn it on
    chrome=chrome(show_sidebar_search=True, show_clock=True, show_theme_toggle=True),
    publish=True,
)

# incremental edits read-modify-write the stored config for you
app.add_page(page("detail", "Detail", [code_card("SELECT 1", "kpi")]),
             nav=nav_item("detail", "Detail"), publish=True)
app.set_theme({"primaryColor": "#0E7C66"})
app.remove_page("detail")
```

## Scenarios, jobs and recipes

```python
proj = client.get_project(1)

# scenarios — trigger and wait
sc = proj.get_scenario("daily_pipeline")
final = sc.run_and_wait()                 # polls to a terminal state
print(final["status"], final.get("steps_failed"))

# jobs / pipeline
runs = client.jobs.list_runs(status="failed", limit=50, offset=0)   # limit ≤ 200
client.jobs.trigger("dbt_run", select="stg_sales+")              # returns at once
run = client.jobs.trigger_and_wait("dbt_run", select="stg_sales+")  # blocks
print(run["status"], client.jobs.logs("dbt_run", lines=100))

# dbt builds: wait=True blocks until the run finishes and returns it
proj.run_dbt("stg_sales+", wait=True)
proj.get_recipe("fact_orders").build(downstream=True, wait=True)

# datasets — point at an existing table
proj.create_dataset("orders", connector_id=5,
                    connection_config={"schema_name": "sales", "table_name": "orders"})

# datasets — surface existing raw tables in the picker (no data copied)
proj.register_source_table("staging", "orders_raw", connector_id=5)
proj.register_schema("staging", connector_id=5)   # or the whole schema, idempotent

# connectors (org-level)
conn = client.create_connector("ERP replica", "postgresql", config={"host": "…"})
print(conn.test())

# publish a data API + mint a key (90-day expiry, sensitivity_max="medium"
# by default: high/critical columns come back [BLOCKED]); store the key in a
# secret manager — it is shown once
asset = client.create_asset("data_api", "orders", "Orders API", config={...})
asset.publish()
key = asset.create_api_key(name="readonly")

# ask an agent
print(client.get_agent(42).ask("Cancellations last month?")["answer"])
```

## CLI

Installing the package also installs a `honeyframe` command. It reads the same
environment variables as `HoneyframeClient.from_env()`. Flags go *after* the
subcommand (kubectl/gh style).

```bash
export HONEYFRAME_URL=https://platform.your-domain.com
export HONEYFRAME_TOKEN=hf_…            # a Personal Access Token

honeyframe whoami
honeyframe projects -f csv
honeyframe datasets -p 1
honeyframe sql "SELECT COUNT(*) FROM marts.fact_orders" -p 1 -f csv
honeyframe engine -p 1                       # dbt or native install?
honeyframe upload sales sales.csv --materialize -p 1
honeyframe lineage fact_orders --down -p 1
honeyframe export fact_orders out.parquet --max-rows 100000 -p 1
honeyframe pat create --name laptop --expires-days 90
honeyframe scenario run daily_pipeline -p 1 --wait
```

To pass a token explicitly, use `--token-file PATH` or `--token -` (read from
stdin); a literal `--token VALUE` still works but warns, because it shows up
in the process list and shell history. `honeyframe pat create` prints the new
token once on stdout (as JSON, for scripts) with a reminder on stderr to store
it. `honeyframe plugin run` executes plugin code in-process as you (it is not
sandboxed) and hides `HONEYFRAME_*` / `HUB_*` variables from it unless you pass
`--pass-env`.

## Resource map

| Handle | Get it from | Key methods |
|---|---|---|
| `HoneyframeClient` | constructor | `sql`, `request`, `list_projects`, `get_project`, `get_dashboard`, `jobs`, `list_connectors`, `create_connector`, `get_connector`, `list_my_credentials`, `list_knowledge_bases`, `create_knowledge_base`, `get_knowledge_base`, `kb_connector_options`, `llm_mesh_overview`, `get_llm_policy`, `set_llm_policy`, `llm_mesh_usage`, `list_card_templates`, `create_card_template`, `list_collections`, `create_collection`, `update_collection`, `delete_collection`, `list_assets`, `create_asset`, `get_asset`, `list_webapps`, `get_webapp`, `publish_webapp`, `list_agents`, `get_agent`, `chat`, `get_auth_info` |
| `Project` | `client.get_project(id_or_slug)` | `list_datasets`, `get_dataset`, `create_dataset`, `import_dataset`, `register_source_table`, `register_schema`, `sql`, `create_dashboard`, `list_scenarios`, `get_scenario`, `create_scenario`, `get_recipe`, `run_dbt`, `change_recipe_engines`, `create_bundle`, `list_bundles`, `bundle`, `list_signoffs`, `list_bundle_activations`, `dbt` → `DbtIDE`, `get_preferences`, `set_preferences`, `list_connectors` |
| `Dataset` | `project.get_dataset(name)` | `get_dataframe`, `head`, `profile`, `statistics`, `get_metadata`, `ask`, `materialize`, `versions`, `preview_version`, `rollback_version`, `update_version`, `delete` |
| `DatasetStatistics` | `dataset.statistics` | one method per EDA card — `univariate`, `correlation`, `scatter_fit`, `quantile_table`, `cdf_plot`, `fit_distribution`, `shapiro_wilk`, `chi2_independence_test`, `ttest_2samp`, `mann_whitney_u`, `paired_ttest`, `anova_1way`, `levene_test`, `ks_test_2samp`, `acf_plot`, `mann_kendall_test`, `pca` — plus `compute(card_type, **params)`, `list_worksheets`, `create_worksheet`, `get_worksheet` |
| `StatisticsWorksheet` | `dataset.statistics.create_worksheet(name)` | `cards`, `add_card`, `update_card`, `remove_card`, `set_cards`, `rename`, `compute_card`, `compute_all`, `reload`, `delete` |
| `Dashboard` | `project.create_dashboard(...)` / `client.get_dashboard(id)` | `add_card`, `add_cards`, `execute_all` (`filters=[...]`), `cards`, `update`, `move_to_collection`, `public_links`, `create_public_link`, `revoke_public_link`, `export_pdf`, `shares`, `add_share`, `update_share`, `remove_share`, `revisions`, `create_revision`, `get_revision`, `diff_revisions`, `restore_revision`, `undo`, `undo_card`, `url` |
| `Card` | `dashboard.cards` / `dashboard.add_card(...)` | `execute`, `update`, `delete`, `unlink_template` |
| `CardTemplate` | `client.create_card_template(...)` / `client.list_card_templates()` | `update`, `delete`, `insert_into`, `link_into`, `preview`, `duplicate`, `versions`, `get_version`, `restore_version` |
| `Connector` | `client.create_connector(...)` / `client.get_connector(id)` | `test`, `update`, `delete`, `set_my_credentials`, `delete_my_credentials`, `rotate`, `secret_history`, `rollback_secret` |
| `Scenario` | `project.get_scenario(id_or_name)` | `run`, `run_and_wait`, `get_last_runs`, `update`, `delete`, `list_triggers`, `add_trigger`, `update_trigger`, `delete_trigger`, `list_reporters`, `add_reporter`, `update_reporter`, `delete_reporter`, `test_reporter` |
| `Recipe` | `project.get_recipe(model_name)` | `get_metadata`, `runs`, `build`, `engine_status`, `save_prepare_steps`, `save` |
| `Bundle` | `project.create_bundle(...)` / `project.bundle(version)` | `download`, `activate`, `deploy`, `push`, `governance_status`, `request_signoff`, `submit_feedback`, `submit_for_approval`, `approve`, `reject`, `abandon` |
| `KnowledgeBase` | `client.create_knowledge_base(...)` / `client.get_knowledge_base(id)` | `update`, `delete`, `docs`, `upload_doc`, `remove_doc`, `embed`, `sync`, `clear`, `search`, `check_vector_store`, `create_tool`, `link_agent`, `unlink_agent`, `usage` |
| `JobsAPI` | `client.jobs` | `list_runs`, `get_run`, `wait`, `abort_run`, `trigger`, `trigger_and_wait`, `list_pipeline_runs`, `get_pipeline_run`, `logs` |
| `PublishedAsset` | `client.create_asset(...)` / `client.get_asset(id)` | `publish`, `unpublish`, `update`, `create_api_key`, `list_api_keys`, `revoke_api_key`, `versions`, `rollback`, `diff_versions` |
| `APIService` | `client.create_api_service(...)` / `client.get_api_service(id)` | `add_endpoint`, `set_endpoints`, `publish`, `create_key`, `call`, `test`, `versions`, `rollback`, `diff_versions` |
| `Webapp` | `client.publish_webapp(...)` / `client.get_webapp(key)` | `config`, `public_links`, `create_public_link`, `revoke_public_link`, `versions`, `rollback`, `diff_versions`, `url`, `public_url` |
| `DbtIDE` | `project.dbt` | `write_file`, `read_file`, `create_model`, `list_models`, `compile`, `run`, `test`, `build`, `git_commit` |
| `Agent` | `client.get_agent(id)` | `ask`, `ask_stream` |

Global-filter builders (`from honeyframeapi import ...`): `dashboard_filter`,
`filter_between`, `filter_compare`, `filter_in` — build the `filters=[...]`
list `Dashboard.execute_all` applies to every card whose SQL references the
filter column.

## Errors and retries

Every failure raises a subclass of `honeyframeapi.HoneyframeError`:
`HoneyframeAuthError` (401/403), `HoneyframeNotFoundError` (404),
`HoneyframeValidationError` (400/409/422), `HoneyframeRateLimitError` (429,
with `.retry_after`), `HoneyframeBudgetExceededError` (the org's metered budget
is spent; not retryable) and `HoneyframeAPIError` (other non-2xx), each carrying
`.status_code` and `.detail` (the server's message, capped at 4 KB). A missing
or unsafe setting (no URL, a plain-http URL, no project) raises
`HoneyframeConfigError` before any request is sent.

Long calls take their own `timeout=` in seconds (`Dashboard.export_pdf`,
`Dashboard.execute_all`, `Dataset.materialize`, ML recipe `run`), overriding
the client's `timeout` (default 60 s).

Idempotent requests (`GET`, `HEAD`, `OPTIONS`) are retried on transport errors
and on 502/503/504 with exponential backoff; writes and 429s are never
retried automatically. Tune with `max_retries` (default 2) and
`retry_backoff` (default 0.5 s), or pass `max_retries=0` to turn it off.

## Scope

Covered today: authentication (PAT, session login), projects, datasets to
pandas plus create / import / materialize, snapshot versions and as-of reads,
ad-hoc SQL, dataset statistics, dashboards and cards, webapps and public links,
connectors (full CRUD and test), knowledge banks (the RAG build loop), LLM
governance and spend, scenarios (run, wait, history, triggers, reporters),
project bundles with deployment sign-offs, jobs and pipeline triggers, recipes
(inspect, dbt build, prepare steps, `Recipe.save`, per-recipe engine selection), published
assets and data-API keys, API services, and agent chat.

Also in the package, documented on the portal: machine learning (train,
score, cluster, evaluate, saved models), API services, streaming chat and push
notifications, managed folders, dataset metrics and checks, experiments,
notebooks, wiki pages, and plugin recipes (`honeyframe plugin …`).

> **`client.sql()` note:** `sql(query, connector_id=None, row_limit=10000,
> params=None)` runs through the platform's governed SQL endpoint and returns
> at most `row_limit` rows (the platform caps it at 50,000). `params` are named
> binds: `sql("… WHERE order_date >= :start", params={"start": "2026-01-01"})`.
> When the result was cut at `row_limit` the SDK warns and sets
> `df.attrs["truncated"] = True`. For full-table extracts use
> `Dataset.get_dataframe()`, `Dataset.to_parquet()` or `honeyframe export`.

## Calling an endpoint without a typed method

`client.request(method, path, **kwargs)` calls any platform endpoint with the
same authentication, org/project headers, retries and typed exceptions as the
typed methods:

```python
client.request("GET", "/api/traces/sessions", params={"limit": 50})
client.request("POST", "/api/some/endpoint", json={...}, timeout=300)
```

`path` must start with `/` (an absolute URL is refused). Values you
interpolate into `path` yourself are not encoded for you.

## Versioning and stability

`honeyframeapi` is 0.x and follows semantic versioning in spirit: a minor
release (0.1 → 0.2) may change behaviour, a patch release does not, and every
change is listed in the changelog.

- The public API is what `honeyframeapi.__all__` exports, plus the methods and
  properties reachable from those objects. Names starting with `_` and the
  handle constructors (`Project(...)`, `Dataset(...)`, …; get handles from the
  client instead) are private and may change without notice.
- A deprecated name or argument keeps working and emits a
  `DeprecationWarning` for at least one minor release before it is removed.
- Behaviour changes that tighten security (for example shorter default
  expiries) are called out under "Changed" in the changelog.

## Support

Documentation and guides: <https://docs.honeyframe.io/>.
