Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down Expand Up @@ -50,6 +50,8 @@ jobs:
# Advisory: tells us when the live API has moved past our vendored copy.
# Never blocks — the SDK targets the spec it ships with, and a drifting
# upstream is a signal to cut a release, not a reason to fail this build.
# Compares the whole document (paths, schemas, descriptions), ignoring only
# `servers`.
upstream-drift:
runs-on: ubuntu-latest
if: github.event_name == 'push'
Expand All @@ -58,17 +60,43 @@ jobs:
- uses: actions/checkout@v4
- name: Compare vendored spec against production
run: |
curl -sSL https://api.firmendata.com/v1/openapi.json -o /tmp/live.json
curl -fsSL https://api.firmendata.com/v1/openapi.json -o /tmp/live.json
python - <<'PY'
import json
live = json.load(open('/tmp/live.json'))
vendored = json.load(open('contracts/openapi.v1.json'))
lp, vp = set(live['paths']), set(vendored['paths'])
if lp != vp:
print(f"::warning::Vendored spec is out of date. "
f"added={sorted(lp - vp)} removed={sorted(vp - lp)}")

def load(path):
spec = json.load(open(path))
spec.pop("servers", None)
return spec

def diff(a, b):
a, b = a or {}, b or {}
return {
"added": sorted(set(a) - set(b)),
"removed": sorted(set(b) - set(a)),
"changed": sorted(k for k in set(a) & set(b) if a[k] != b[k]),
}

live = load("/tmp/live.json")
vendored = load("contracts/openapi.v1.json")
if live == vendored:
print("Vendored spec matches production (ignoring servers).")
else:
print("Vendored spec matches production paths.")
summary = {
"version": {
"live": live.get("info", {}).get("version"),
"vendored": vendored.get("info", {}).get("version"),
},
"sections": sorted(
k for k in set(live) | set(vendored) if live.get(k) != vendored.get(k)
),
"paths": diff(live.get("paths"), vendored.get("paths")),
"schemas": diff(
live.get("components", {}).get("schemas"),
vendored.get("components", {}).get("schemas"),
),
}
print(f"::warning::Vendored spec is out of date. {json.dumps(summary)}")
PY

secrets:
Expand Down
69 changes: 59 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,72 @@

## 1.3.0

Regenerated from the updated API contract. **Additive, non-breaking:**
Regenerated from the updated API contract. **Additive, non-breaking**, except
where noted for `get_financials()`.

**Coverage and fields**

- Austria: `country="AT"`, the Austrian register type (`AT-FB`), Firmenbuch
courts in `register_court`, the Austrian states in `bundesland`, and
Austrian legal forms.
- Optional `country` (`DE`, `CH` or `AT`) on autocomplete in both clients;
autocomplete hits include `registered_seat`.
- Company contact details include `phones` (phone and fax numbers) and
`primary_email`.
- Financial summaries include `latest_equity`; each year in
`history["metrics"]` carries its top-level `balance_sheet` and
`income_statement` positions.
- Error messages fall back to the problem's new `message` field.

**New methods**

- Optional `country` (`DE`, `CH` or `AT`) on autocomplete in both clients.
- Beta `feed_new_companies(**filters)` and `feed_insolvencies(**filters)`
methods on sync and async clients, with generated filter and response types.
Both accept `register_court` (German Registergericht or Austrian Firmenbuch
court names, repeated values combined with OR); on the insolvency feed it
filters by the company's register court, not the insolvency court.
- `iter_feed_new_companies` and `iter_feed_insolvencies` drain available pages
while `has_more` is true, deduplicate event IDs within each run, and expose
`.next_cursor` after fully consumed pages for resuming, including empty pages.
Async variants use `async for`.
- Both feeds accept `register_court` (German Registergericht or Austrian
Firmenbuch court names, repeated values combined with OR); on the insolvency
feed it filters by the company's register court, not the insolvency court.
- Generated types reflect Austrian countries, register types, courts, states
and legal forms, plus balance-sheet and income-statement positions and
latest equity in financial responses.
- README documents feed polling, cursor persistence, deduplication and pricing:
1 credit per page with events; empty pages are free.
- `get_lists()` and `get_list(list_id, limit=, cursor=, sort=, sort_direction=)`
on both clients read your company lists. Both are free.
- `iter_search(..., max_results=None)` on both clients yields every search hit,
following `next_cursor` with only the cursor and `limit` after the first page,
and stops at `has_more=False`, `max_results` or 10,000 results. Each page of
up to 50 results costs 1 credit.
- `create_subscription(**body)` and `test_delivery(**body)` are typed from the
contract (`SubscriptionCreateRequest`, `SubscriptionTestRequest`) instead of
`**body: Any`.

**Financials**

- `get_financials()` takes `details`. The API now returns the full line items
by default; pass `details=False` for the lean response. `include_line_items`
is deprecated: it emits a `DeprecationWarning` and is sent as `details` (it
no longer sends `include=line_items`). Calls that passed nothing now receive
the full line items, as the API does.

**Client behaviour**

- Every request sends `X-FirmenData-Client: python-sdk/<version>`; the
`User-Agent` stays `firmendata-python/<version>`. The new
`app_info={"name": ..., "version": ..., "url": ...}` option appends your
integration to it.
- New `on_response` option: called for every HTTP response with a
`ResponseInfo` (`status`, `headers`, `request_id`, `credits_charged`). The
async client also awaits a coroutine callback.
- Realtime calls (`fetch_realtime=True`), document listings and downloads are
no longer retried after a timeout, a gateway timeout or a connection that
broke after the request was sent — the server bills them when it finishes,
so a retry could be billed twice. Their timeout is at least 150 seconds
(`REALTIME_TIMEOUT`) unless the call passes its own `timeout`, which the
live-capable methods now accept.
- A `429` whose `Retry-After` exceeds 20 seconds raises `RateLimitError` at
once, with `retry_after` in seconds, instead of waiting 20 seconds per retry
and failing anyway.
- The generator toolchain (`datamodel-code-generator`, `black`) is pinned in
the `dev` extra; CI also runs on Python 3.14.

## 1.2.1

Expand Down
Loading
Loading