Skip to content

feat(data-warehouse): implement katana import source - #67834

Merged
talyn-app[bot] merged 5 commits into
masterfrom
posthog-code/katana-import-source
Jul 3, 2026
Merged

feat(data-warehouse): implement katana import source#67834
talyn-app[bot] merged 5 commits into
masterfrom
posthog-code/katana-import-source

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

Problem

PostHog customers who run manufacturing and inventory on Katana Cloud Inventory (formerly Katana MRP) have no way to pull their ERP data into the data warehouse. Katana was a scaffolded-only source (registered, empty stub, hidden). This turns it into a working connector so users can join products, orders, and inventory with their product analytics.

Changes

Implements the Katana source end-to-end as a ResumableSource over Katana's v1 REST/JSON API (https://api.katanamrp.com/v1, Bearer API key).

  • settings.py — declarative catalog of 18 endpoints. Incremental (server-side updated_at/created_at timestamp filters, created_at partition key): products, materials, variants, services, customers, suppliers, sales/purchase/manufacturing orders, sales returns, stock adjustments/transfers, stocktakes, and inventory movements (created_at only, immutable log). Full refresh only (no server-side timestamp filter): inventory (composite [variant_id, location_id] key), price_lists, locations, tax_rates.
  • katana.py — transport over make_tracked_session(): page-number pagination (page/limit=250), a ~1 req/s throttle plus Retry-After-honoring backoff for Katana's 60 req/min limit, ISO 8601 <field>_min filters, and page-level resumable state saved after each yielded batch.
  • source.pyKatanaSource with validate_credentials (cheap /user_info token probe), get_schemas, get_non_retryable_errors (401/403), resumable manager wiring, and canonical table/column descriptions. Shipped releaseStatus=alpha and kept behind unreleasedSource=True per the task.
  • canonical_descriptions.py — curated table/column docs from Katana's official API reference.
  • Moved katana into the Implemented table in SOURCES.md and regenerated generated_configs.py.

Incremental design note

Katana list endpoints always return newest-first by created_at and expose no sort override, so the source declares sort_mode="desc" (the pipeline finalizes the max-cursor watermark only at the end for desc sources, which stays correct despite the fixed order). The timestamp filter is a plain query param applied to every page, so incremental syncs terminate naturally at the last page with no unbounded history re-walk. Resume is page-based and idempotent (re-reading a page on resume is deduped by the delta merge).

How did you test this code?

I (Claude) verified the API contract with live unauthenticated curl against api.katanamrp.com (confirmed the {"data": [...]} envelope, page/limit params, created_at_min/updated_at_min/created_at_max/updated_at_max filters, newest-first ordering, 60 req/60s rate limit with X-RateLimit-* + Retry-After headers, and 401/400 error shapes) and the published OpenAPI spec at /v1/openapi.json.

Automated tests I ran locally (61 new, all passing):

  • test_katana.py — transport: timestamp formatting, future-cursor clamping, incremental param building (per-endpoint, user-chosen field, full-refresh no-filter), pagination termination on a short page, resume-from-saved-page, per-page filter application, 429/5xx/401 status handling, Retry-After backoff, and per-endpoint SourceResponse shape.
  • test_katana_source.py — source class: config fields, category, get_schemas incremental vs full-refresh per endpoint, credential validation, resumable manager binding, source_for_pipeline plumbing, and canonical-description key integrity.

I also re-ran test_generated_configs.py, test_source_config_generator.py, and test_source_categories.py (all pass), regenerated generated_configs.py (only diff is Katana's api_key field), and ran ruff check/ruff format.

I could not run an authenticated end-to-end sync (no Katana API key available), so server-side filter honoring on the less common endpoints is inferred from the OpenAPI spec plus docs rather than a live authenticated smoke test. This is called out in a code comment. Correctness holds either way: the desc + per-page filter + primary-key merge design produces correct data even if a filter is silently ignored (worst case it fetches more than needed).

Docs update

The user-facing doc lives in the posthog.com repo, which isn't checked out in this environment, so audit_source_docs was not run. The doc is written and ready to land at contents/docs/cdp/sources/katana.md (matching docsUrl); its full content is in the agent context below.

🤖 Agent context

Autonomy: Fully autonomous

Authored by Claude Code. Skills invoked: implementing-warehouse-sources (followed the source.py/settings.py/katana.py split, base-class choice, incremental and tracked-transport rules), documenting-warehouse-sources (doc template), and writing-tests conventions.

Key decisions:

  • Base class: ResumableSource (not +WebhookSource). The research recommended adding webhooks, but full webhook plumbing (HogFunction template, create/delete, resource map) can't be verified without a live account, and the core value is the pull-based incremental sync. Shipped resumable-only and left webhooks as a clear future enhancement rather than landing an untested surface.
  • sort_mode="desc". Katana exposes no sort override and returns newest-first, so ascending was not an option. Verified against the pipeline's desc watermark handling.
  • Category FINANCE___ACCOUNTING (ERP) over the scaffold's E_COMMERCE, since the enum lists ERP there and Katana is an MRP/ERP.
  • Hand-edited generated_configs.py (no DB available for the generator initially), then installed deps and re-ran the generator to confirm the output matched exactly.

Doc content to add to posthog.com at contents/docs/cdp/sources/katana.md:

---
title: Linking Katana as a source
sidebar: Docs
showTitle: true
availability: { free: full, selfServe: full, enterprise: full }
sourceId: Katana
beta: true
---

import SourceSetupIntro from "../_snippets/source-setup-intro.mdx"
import SyncModes from "../_snippets/sync-modes.mdx"
import TroubleshootingLink from "../_snippets/dw-troubleshooting-link.mdx"
import AlphaRelease from "../_snippets/alpha-release.mdx"

<AlphaRelease />

[Katana Cloud Inventory](https://katanamrp.com) (formerly Katana MRP) is a cloud manufacturing ERP/MRP for inventory, production, and order management. This connector syncs your Katana products, materials, orders, inventory, and more into the PostHog data warehouse so you can join manufacturing and fulfillment data with your product analytics.

## Prerequisites

- An active Katana account with API access (historically a Professional plan or the API access add-on).
- A Katana API key, generated in-app under **Settings > API**.

## Adding a data source

<SourceSetupIntro />

You need a single credential: your Katana **API key**. To generate one:

1. In Katana, go to **Settings > API**.
2. Create a new API key and copy it.
3. Paste it into the **API key** field when creating the source in PostHog.

The key is sent as a Bearer token and has read access to your factory's data.

## Sync modes

<SyncModes />

Most tables (products, materials, variants, customers, suppliers, and the sales/purchase/manufacturing orders) support **incremental** syncs using Katana's server-side `updated_at`/`created_at` timestamp filters, so only new and changed rows are pulled after the first sync. A few tables — `inventory`, `price_lists`, `locations`, and `tax_rates` — have no server-side timestamp filter and are **full refresh** only.

## Configuration

<SourceParameters />

## Supported tables

<SourceTables />

## Troubleshooting

- **`401 Unauthorized`** — your API key is invalid or has been revoked. Generate a new key under **Settings > API** in Katana and reconnect the source.
- **Rate limiting** — Katana allows 60 requests per 60 seconds. The connector throttles itself to stay under this limit and honors `Retry-After` on `429` responses, so large backfills may take a while.

<TroubleshootingLink />

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hey @Gilbert09! 👋

It looks like your git author email on this PR isn't your @posthog.com address (owerstom@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@Gilbert09
Gilbert09 marked this pull request as ready for review July 2, 2026 13:39
Copilot AI review requested due to automatic review settings July 2, 2026 13:39
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 2, 2026 13:39
Comment thread products/warehouse_sources/backend/temporal/data_imports/sources/katana/katana.py Outdated
Comment thread products/warehouse_sources/backend/temporal/data_imports/sources/katana/katana.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(data-warehouse): implement katana i..." | Re-trigger Greptile

Comment thread products/warehouse_sources/backend/temporal/data_imports/sources/katana/katana.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Gilbert09 added 5 commits July 3, 2026 05:15
Implements the Katana Cloud Inventory (Katana MRP) warehouse source end-to-end as a
ResumableSource over Katana's v1 REST API.

- settings.py: 18-endpoint catalog (products, materials, variants, services, customers,
  suppliers, sales/purchase/manufacturing orders, sales returns, stock adjustments/transfers,
  stocktakes, inventory movements, plus full-refresh inventory/price_lists/locations/tax_rates)
  with per-endpoint primary keys, created_at partition keys, and updated_at/created_at
  incremental fields.
- katana.py: tracked-session transport with page-number pagination, ~1 req/s throttling and
  Retry-After honoring backoff for the 60 req/min limit, ISO 8601 timestamp filters, and
  page-level resumable state.
- source.py: KatanaSource (alpha, unreleasedSource) with validate_credentials, get_schemas,
  get_non_retryable_errors, resumable manager wiring, and canonical descriptions.
- Tests, SOURCES.md move to Implemented, regenerated generated_configs.py.

Generated-By: PostHog Code
Task-Id: 48a199c3-48b3-4eb9-90cd-da1466f375a4
Split the retry-wrapped `_fetch_page` into a pure `_request_page` so tests can call
it directly instead of reaching for tenacity's `__wrapped__` attribute (invisible to
mypy). Pass `response=` when constructing the test HTTPError to match the typed
signature.

Generated-By: PostHog Code
Task-Id: 48a199c3-48b3-4eb9-90cd-da1466f375a4
A 2xx response with valid JSON but no `data` key (changed envelope, maintenance
page) previously read as an empty page and silently ended pagination mid-sync. Treat
a missing `data` key as a retryable error so the sync retries and then fails loudly
instead of losing data. Genuine empty results (`{"data": []}`) still terminate
pagination normally.

Generated-By: PostHog Code
Task-Id: 48a199c3-48b3-4eb9-90cd-da1466f375a4
Register the Katana API key with the tracked HTTP transport (redact_values)
in both the credential probe and the sync session so it's masked in logged
URLs and captured samples. Replace the bare raise_for_status() 4xx path with
a manually built HTTPError whose URL is scrubbed to scheme/host/path (no query,
no userinfo), keeping the stable "... for url: https://api.katanamrp.com..."
prefix that get_non_retryable_errors() matches on. Read the envelope's data key
directly now that _request_page validates it, so a regression in that guard
fails loudly instead of silently ending the sync.

Generated-By: PostHog Code
Task-Id: b1bc573d-e0ed-4097-b013-da9d35b2b2d0
…ed error

The 401/403 non-retryable errors are now raised by _request_page with a
scrubbed URL rather than response.raise_for_status(); update the comment to
match.

Generated-By: PostHog Code
Task-Id: b1bc573d-e0ed-4097-b013-da9d35b2b2d0
@Gilbert09
Gilbert09 force-pushed the posthog-code/katana-import-source branch from fc7eb17 to 63c9ea2 Compare July 3, 2026 04:15
@talyn-app
talyn-app Bot merged commit ac986be into master Jul 3, 2026
211 checks passed
@talyn-app
talyn-app Bot deleted the posthog-code/katana-import-source branch July 3, 2026 04:43
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-03 05:09 UTC Run
prod-us ✅ Deployed 2026-07-03 05:19 UTC Run
prod-eu ✅ Deployed 2026-07-03 05:21 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants