Metadata-Version: 2.4
Name: honeyframeapi
Version: 0.1.6
Summary: Python client for the Honeyframe data platform — a dataikuapi-style SDK
Author: HubStudio-id
License: Proprietary
Project-URL: Homepage, https://github.com/HubStudio-id/hubstudio-data-intel
Keywords: honeyframe,hubstudio,data-platform,dataiku,analytics
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.24
Provides-Extra: pandas
Requires-Dist: pandas>=1.5; extra == "pandas"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pandas>=1.5; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"

# honeyframeapi

A `dataikuapi`-style Python client for the **Honeyframe** data platform
(Honeyframe Platform / `platform.hubstudio.id`). Script the platform from a
notebook: read data into pandas, build dashboards and cards, browse datasets
and connectors.

If you've used `dataikuapi`, this will feel identical — construct one client,
then navigate to project / dataset / dashboard handles.

```python
import dataikuapi
client = dataikuapi.DSSClient(host, api_key)   # Dataiku
```
```python
import honeyframeapi
client = honeyframeapi.HoneyframeClient(host, username=..., password=..., project_id=1)  # Honeyframe
```

## Install

```bash
pip install -e sdk/python                # core (httpx only)
pip install -e "sdk/python[pandas]"      # + pandas for get_dataframe()/sql()
```

## Authenticate

Three ways, in precedence order:

```python
import honeyframeapi

# 1. email / password (works against any deployment today; auto-refreshes the 8h JWT)
client = honeyframeapi.HoneyframeClient(
    "https://platform.hubstudio.id",
    username="you@hubstudio.id", password="…",
    project_id=1,
)

# 2. a token you already hold — a JWT, or (preferred for automation) a
#    long-lived Personal Access Token minted from the UI
#    (avatar menu → "Personal access tokens") or via client.create_pat()
client = honeyframeapi.HoneyframeClient("https://platform.hubstudio.id", token="hf_…", project_id=1)

# 3. from environment variables
#    HONEYFRAME_URL (or HUB_API_BASE), HONEYFRAME_TOKEN,
#    HONEYFRAME_USERNAME/PASSWORD (or HUB_USERNAME/PASSWORD), HONEYFRAME_PROJECT_ID
client = honeyframeapi.HoneyframeClient.from_env()
```

## Vibe-code the warehouse, end to end

One governed loop, and it adapts to the deployment — see
[`examples/vibe_code_end_to_end.py`](examples/vibe_code_end_to_end.py):

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

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

(On a native install the authoring lines change — see `proj.engine()` — but the
upload → read → dashboard → webapp → share legs are identical.)

## Read data into pandas

```python
# ad-hoc SQL — the analogue of dwh-adira's run_sql()
df = client.sql("""
    SELECT booking_source, COUNT(*) AS n
    FROM marts.fact_appointment
    GROUP BY booking_source ORDER BY n DESC
""")

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

## Build a dashboard from code

This replaces the hand-rolled `requests` script in dwh-adira
(`scripts/upload_dashboards.py`) — same card dicts, ergonomic API:

```python
proj = client.get_project(1)
dash = proj.create_dashboard("Appointments Overview",
                             description="Booking channels & volume")

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

dash.add_card("By Booking Source", "donut",
              "SELECT booking_source AS name, COUNT(*) AS value "
              "FROM marts.fact_appointment GROUP BY booking_source",
              config={"nameField": "name", "valueField": "value"})

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

Porting an existing `upload_dashboards.py` block? Pass the dicts straight through:

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

## Publish a webapp + share it by link

The last mile: 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(
    "ops-overview", "Ops 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)
print(link["token"])                       # shown ONCE — store it
print(app.public_url(link["token"],        # anonymous URL on the SaaS host
                     host="https://hospital.hubstudio.id"))

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

`publish_webapp` is project-scoped — set `project_id` on the client first.

### Build a multi-page app from code (no hand-rolled config)

The builders compose pages, blocks and filters for you — including the two
flexible block kinds: `code_card` (a governed read-only SELECT → chart, run
server-side with an 8s timeout + 5k-row cap) and `html_block` (freeform
HTML/CSS/JS; `mode="sandboxed"` runs arbitrary JS — even a canvas game — in an
opaque-origin iframe with a CSP that blocks outbound network, so it can't read
your JWT or phone home). Add `data_sql=` to a sandboxed block and the server
runs that governed SELECT and postMessages the rows into the fenced iframe —
the "data bridge", so your custom JS renders real DWH 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, branch_filter,
)

app = client.create_webapp(
    "ops-overview", "Ops Overview",
    pages=[page(
        "home", "Home",
        blocks=[
            card_ref(dash.dashboard_id, dash.cards[0].card_id, w=12, h=4),
            code_card("SELECT branch_name, visits FROM marts.v_branch_visits "
                      "WHERE :period_start <= visit_date AND visit_date <= :period_end",
                      "bar", x=12, w=12, h=4, title="Visits by branch"),
            html_block("<canvas id=game></canvas><script>/* … */</script>",
                       mode="sandboxed", w=12, h=8, title="Mini-game"),
            # DATA BRIDGE: a fenced sandboxed block that renders REAL data —
            # the server runs data_sql and postMessages the rows into the iframe
            # (the JS listens for e.data.source === 'honeyframe').
            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 branch_name, visits FROM marts.v_branch_visits"),
            html_block("<i>internal note</i>", public_hidden=True, w=12, h=2),
        ],
        # page-level filters bound into the SQL above by param name
        filters=filters(
            period=date_range_filter(),                       # :period_start/:period_end
            branch=branch_filter("branch_id_codes", mode="multi",
                                 options_sql="SELECT afya_code AS value, branch_name AS label "
                                             "FROM marts.v_hospital_360 WHERE total_appointments > 0"),
        ),
    )],
    nav=[nav_item("home", "Home", icon="gauge")],
    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 & recipes (dataikuapi parity)

Closes the gap behind dwh-adira's `run_instinct_daily_scenario.py` and
`scaffold_*` scripts:

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

# scenarios — trigger and poll like dataikuapi's scenario.run()
sc = proj.get_scenario("daily_pipeline")
final = sc.run_and_wait()                 # polls to terminal state
print(final["status"], final.get("steps_failed"))

# jobs / pipeline
runs = client.jobs.list_runs(status="failed")
run  = client.jobs.trigger("dbt_run", select="int_appointments_unioned+")
print(client.jobs.logs("dbt_run", lines=100))

# recipes (dbt-engine build)
proj.get_recipe("dim_patient").build(downstream=True, wait=True)

# datasets — create like create_sql_table_dataset()
proj.create_dataset("ekko", connector_id=5,
                    connection_config={"schema_name": "RAW_RSL", "table_name": "EKKO"})

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

# publish a data API + mint a key
asset = client.create_asset("data_api", "appointments", "Appointments API", config={...})
asset.publish()
print(asset.create_api_key(name="readonly").key)   # dk_… shown once

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

## CLI

Installing the package also installs a `honeyframe` command. Flags go *after*
the subcommand (kubectl/gh style).

```bash
export HONEYFRAME_URL=https://platform.hubstudio.id
export HONEYFRAME_TOKEN=hf_…            # a PAT (preferred) or JWT

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

## Resource map

| Handle | Get it from | Key methods |
|---|---|---|
| `HoneyframeClient` | constructor | `sql`, `list_projects`, `get_project`, `get_dashboard`, `jobs`, `list_connectors`, `create_connector`, `get_connector`, `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`, `sql`, `create_dashboard`, `list_scenarios`, `get_scenario`, `create_scenario`, `get_recipe`, `run_dbt`, `get_preferences`, `set_preferences`, `list_connectors` |
| `Dataset` | `project.get_dataset(name)` | `get_dataframe`, `head`, `profile`, `get_metadata`, `ask`, `materialize`, `delete` |
| `Dashboard` | `project.create_dashboard(...)` / `client.get_dashboard(id)` | `add_card`, `add_cards`, `execute_all`, `cards`, `update`, `url` |
| `Card` | `dashboard.cards` / `dashboard.add_card(...)` | `execute`, `update`, `delete` |
| `Connector` | `client.create_connector(...)` / `client.get_connector(id)` | `test`, `update`, `delete` |
| `Scenario` | `project.get_scenario(id_or_name)` | `run`, `run_and_wait`, `get_last_runs`, `update`, `delete` |
| `Recipe` | `project.get_recipe(model_name)` | `get_metadata`, `runs`, `build`, `save_prepare_steps` |
| `JobsAPI` | `client.jobs` | `list_runs`, `get_run`, `wait`, `abort_run`, `trigger`, `logs` |
| `PublishedAsset` | `client.create_asset(...)` / `client.get_asset(id)` | `publish`, `unpublish`, `update`, `create_api_key`, `list_api_keys`, `revoke_api_key` |
| `Webapp` | `client.publish_webapp(...)` / `client.get_webapp(key)` | `config`, `public_links`, `create_public_link`, `revoke_public_link`, `url`, `public_url` |
| `Agent` | `client.get_agent(id)` | `ask` |

## Errors

Every failure raises a subclass of `honeyframeapi.HoneyframeError`:
`HoneyframeAuthError` (401/403), `HoneyframeNotFoundError` (404),
`HoneyframeValidationError` (400/409/422), `HoneyframeAPIError` (other non-2xx),
each carrying `.status_code` and `.detail`.

## Scope

Shipped: auth (login + token), projects, datasets→pandas + create/import/
materialize, ad-hoc SQL, dashboards + cards, connectors (full CRUD + test),
scenarios (run/wait/history), jobs + pipeline triggers + logs, recipes
(inspect + dbt build + author prepare steps), published assets + data-API-key
minting, agent chat. This covers the dataikuapi surface used across the
dwh-adira scripts.

Personal Access Tokens (`hf_…`) are first-class: mint from the settings UI or
`client.create_pat(name, expires_days=…)`, then authenticate with
`token="hf_…"`. They're independently revocable, scoped to the minting user,
and don't carry the 8h JWT expiry — the right credential for cron/automation.

Still planned: richer recipe authoring (SQL/Python recipe payloads beyond
prepare steps).

> **`client.sql()` note:** there's no public raw-SQL endpoint yet, so `sql()`
> runs through a hidden per-project scratch dashboard card (zero backend
> change). It honors the platform's per-card row cap — for full-table extracts
> use `Dataset.get_dataframe()`. When a `POST /api/sql` endpoint lands, only the
> internals change; `sql()`'s signature is stable.
