firmendata 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firmendata-0.1.0/.github/workflows/ci.yml +91 -0
- firmendata-0.1.0/.github/workflows/publish.yml +99 -0
- firmendata-0.1.0/.gitignore +13 -0
- firmendata-0.1.0/LICENSE +21 -0
- firmendata-0.1.0/PKG-INFO +195 -0
- firmendata-0.1.0/README.md +162 -0
- firmendata-0.1.0/contracts/openapi.v1.json +9288 -0
- firmendata-0.1.0/pyproject.toml +90 -0
- firmendata-0.1.0/scripts/generate_types.py +163 -0
- firmendata-0.1.0/src/firmendata/__init__.py +51 -0
- firmendata-0.1.0/src/firmendata/_base.py +97 -0
- firmendata-0.1.0/src/firmendata/_retry.py +72 -0
- firmendata-0.1.0/src/firmendata/_version.py +1 -0
- firmendata-0.1.0/src/firmendata/async_client.py +229 -0
- firmendata-0.1.0/src/firmendata/client.py +265 -0
- firmendata-0.1.0/src/firmendata/errors.py +193 -0
- firmendata-0.1.0/src/firmendata/params.py +98 -0
- firmendata-0.1.0/src/firmendata/py.typed +0 -0
- firmendata-0.1.0/src/firmendata/types.py +1003 -0
- firmendata-0.1.0/tests/test_client.py +279 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
- run: pip install -e '.[dev]'
|
|
25
|
+
- run: pytest -q
|
|
26
|
+
- run: ruff check .
|
|
27
|
+
- run: mypy
|
|
28
|
+
# One version is enough for type checking; the others only run tests.
|
|
29
|
+
if: matrix.python-version == '3.11'
|
|
30
|
+
|
|
31
|
+
# The SDK vendors contracts/openapi.v1.json and generates its types from it.
|
|
32
|
+
# If the committed types don't match what the vendored spec produces, the
|
|
33
|
+
# SDK is lying about the API — fail rather than ship that.
|
|
34
|
+
generated-types-are-current:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- uses: actions/setup-python@v5
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.11"
|
|
41
|
+
- run: pip install -e '.[dev]'
|
|
42
|
+
- run: python scripts/generate_types.py
|
|
43
|
+
- name: Types match the vendored contract
|
|
44
|
+
run: |
|
|
45
|
+
if ! git diff --exit-code -- src/firmendata/types.py src/firmendata/params.py; then
|
|
46
|
+
echo "::error::Generated types are stale. Run 'python scripts/generate_types.py' and commit."
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Advisory: tells us when the live API has moved past our vendored copy.
|
|
51
|
+
# Never blocks — the SDK targets the spec it ships with, and a drifting
|
|
52
|
+
# upstream is a signal to cut a release, not a reason to fail this build.
|
|
53
|
+
upstream-drift:
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
if: github.event_name == 'push'
|
|
56
|
+
continue-on-error: true
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
- name: Compare vendored spec against production
|
|
60
|
+
run: |
|
|
61
|
+
curl -sSL https://api.firmendata.com/v1/openapi.json -o /tmp/live.json
|
|
62
|
+
python - <<'PY'
|
|
63
|
+
import json
|
|
64
|
+
live = json.load(open('/tmp/live.json'))
|
|
65
|
+
vendored = json.load(open('contracts/openapi.v1.json'))
|
|
66
|
+
lp, vp = set(live['paths']), set(vendored['paths'])
|
|
67
|
+
if lp != vp:
|
|
68
|
+
print(f"::warning::Vendored spec is out of date. "
|
|
69
|
+
f"added={sorted(lp - vp)} removed={sorted(vp - lp)}")
|
|
70
|
+
else:
|
|
71
|
+
print("Vendored spec matches production paths.")
|
|
72
|
+
PY
|
|
73
|
+
|
|
74
|
+
secrets:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
with:
|
|
79
|
+
# Full history so a secret committed and later removed is still found.
|
|
80
|
+
fetch-depth: 0
|
|
81
|
+
# The gitleaks CLI, not gitleaks/gitleaks-action. The scanner is MIT and
|
|
82
|
+
# free; it is the Action wrapper that requires a paid licence for
|
|
83
|
+
# organisation-owned repos, and it fails the job outright without one.
|
|
84
|
+
# Running the binary directly is the same scan with no licence.
|
|
85
|
+
- name: Scan git history for secrets
|
|
86
|
+
env:
|
|
87
|
+
GITLEAKS_VERSION: 8.30.1
|
|
88
|
+
run: |
|
|
89
|
+
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
|
90
|
+
| tar xz -C /tmp gitleaks
|
|
91
|
+
/tmp/gitleaks git . --no-banner --redact --exit-code 1
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Publishes to PyPI on a version tag.
|
|
2
|
+
#
|
|
3
|
+
# ─── AUTHENTICATION ────────────────────────────────────────────────────────
|
|
4
|
+
#
|
|
5
|
+
# Trusted Publishing (OIDC). There is no PyPI token anywhere — GitHub mints a
|
|
6
|
+
# short-lived credential per run and PyPI verifies it came from this exact
|
|
7
|
+
# repository and workflow. Nothing to rotate, nothing to leak.
|
|
8
|
+
#
|
|
9
|
+
# PyPI supports "pending publishers", so this works for the very first
|
|
10
|
+
# release of a project that does not exist yet. (npm does not, which is why
|
|
11
|
+
# the JavaScript packages take a different route on their first publish.)
|
|
12
|
+
#
|
|
13
|
+
# ─── ONE-TIME SETUP ────────────────────────────────────────────────────────
|
|
14
|
+
#
|
|
15
|
+
# On https://pypi.org/manage/account/publishing/ add a pending publisher:
|
|
16
|
+
#
|
|
17
|
+
# PyPI project name: firmendata
|
|
18
|
+
# Owner: FirmenData
|
|
19
|
+
# Repository name: firmendata-python
|
|
20
|
+
# Workflow name: publish.yml
|
|
21
|
+
# Environment name: pypi
|
|
22
|
+
#
|
|
23
|
+
# Then create the `pypi` environment under repo Settings → Environments. The
|
|
24
|
+
# environment is not decoration: it is where you add a required reviewer, so
|
|
25
|
+
# a tag push cannot publish to PyPI without a human approving the run.
|
|
26
|
+
#
|
|
27
|
+
# ─── RELEASING ─────────────────────────────────────────────────────────────
|
|
28
|
+
#
|
|
29
|
+
# 1. bump __version__ in src/firmendata/_version.py
|
|
30
|
+
# 2. commit, then: git tag 0.1.0 && git push origin 0.1.0
|
|
31
|
+
#
|
|
32
|
+
# A version can never be re-uploaded to PyPI, even after deletion. The build
|
|
33
|
+
# is verified here before the publish step runs, and the publish step is a
|
|
34
|
+
# separate job so the artifacts are inspectable if it fails.
|
|
35
|
+
|
|
36
|
+
name: Publish
|
|
37
|
+
|
|
38
|
+
on:
|
|
39
|
+
push:
|
|
40
|
+
tags: ['*.*.*']
|
|
41
|
+
workflow_dispatch:
|
|
42
|
+
|
|
43
|
+
permissions:
|
|
44
|
+
contents: read
|
|
45
|
+
|
|
46
|
+
jobs:
|
|
47
|
+
build:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
- uses: actions/setup-python@v5
|
|
52
|
+
with:
|
|
53
|
+
python-version: '3.11'
|
|
54
|
+
|
|
55
|
+
- run: pip install -e '.[dev]' build twine
|
|
56
|
+
|
|
57
|
+
# A tag is not a licence to skip the tests.
|
|
58
|
+
- run: pytest -q
|
|
59
|
+
- run: mypy
|
|
60
|
+
- run: ruff check .
|
|
61
|
+
|
|
62
|
+
# The generated types must match the vendored contract, or we would
|
|
63
|
+
# ship an SDK that lies about the API.
|
|
64
|
+
- run: python scripts/generate_types.py
|
|
65
|
+
- run: git diff --exit-code -- src/firmendata/
|
|
66
|
+
|
|
67
|
+
- name: Tag must match the package version
|
|
68
|
+
run: |
|
|
69
|
+
TAG="${GITHUB_REF_NAME}"
|
|
70
|
+
PKG=$(python -c "import re,pathlib; \
|
|
71
|
+
print(re.search(r'\"([^\"]+)\"', pathlib.Path('src/firmendata/_version.py').read_text()).group(1))")
|
|
72
|
+
if [ "$TAG" != "$PKG" ]; then
|
|
73
|
+
echo "::error::Tag $TAG does not match __version__ $PKG"
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
if: github.event_name == 'push'
|
|
77
|
+
|
|
78
|
+
- run: python -m build
|
|
79
|
+
- run: twine check dist/*
|
|
80
|
+
|
|
81
|
+
- uses: actions/upload-artifact@v4
|
|
82
|
+
with:
|
|
83
|
+
name: dist
|
|
84
|
+
path: dist/
|
|
85
|
+
|
|
86
|
+
publish:
|
|
87
|
+
needs: build
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
environment: pypi
|
|
90
|
+
permissions:
|
|
91
|
+
# Required to mint the OIDC token PyPI verifies. Nothing else is needed —
|
|
92
|
+
# in particular there is no PYPI_TOKEN secret.
|
|
93
|
+
id-token: write
|
|
94
|
+
steps:
|
|
95
|
+
- uses: actions/download-artifact@v4
|
|
96
|
+
with:
|
|
97
|
+
name: dist
|
|
98
|
+
path: dist/
|
|
99
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
firmendata-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FirmenData
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: firmendata
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the firmendata German company-data API
|
|
5
|
+
Project-URL: Homepage, https://firmendata.com
|
|
6
|
+
Project-URL: Documentation, https://api.firmendata.com/v1/docs
|
|
7
|
+
Project-URL: Source, https://github.com/FirmenData/firmendata-python
|
|
8
|
+
Project-URL: Issues, https://github.com/FirmenData/firmendata-python/issues
|
|
9
|
+
Author-email: FirmenData <info@firmendata.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api-client,company-data,company-register,firmendata,germany,handelsregister,kyc,ubo,unternehmensregister
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: datamodel-code-generator>=0.25; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# firmendata-python
|
|
35
|
+
|
|
36
|
+
Official Python client for the [firmendata](https://firmendata.com) API — data on
|
|
37
|
+
**2.4 million German companies** from the Unternehmensregister and Handelsregister:
|
|
38
|
+
register profiles, parsed annual financial statements, shareholder cap tables,
|
|
39
|
+
UBO chains, insolvency notices and public-tender links.
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/firmendata/)
|
|
42
|
+
[](https://pypi.org/project/firmendata/)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install firmendata
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Try it without signing up
|
|
50
|
+
|
|
51
|
+
Company-name autocomplete is free and needs **no API key**:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from firmendata import FirmenData
|
|
55
|
+
|
|
56
|
+
for hit in FirmenData().autocomplete("siemens")["data"]:
|
|
57
|
+
print(hit["eu_id"], hit["display_name"])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Keyless calls are rate limited, modestly and by address — enough to try the
|
|
61
|
+
API, back a search box, or run low-volume queries. Add a key for substantially
|
|
62
|
+
higher limits plus every other endpoint. On a `429`, honour `Retry-After`;
|
|
63
|
+
the client already does this for you.
|
|
64
|
+
|
|
65
|
+
## With an API key
|
|
66
|
+
|
|
67
|
+
Create one at [firmendata.com](https://firmendata.com/de/account/api-keys) — the
|
|
68
|
+
free plan includes 100 credits.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from firmendata import FirmenData
|
|
72
|
+
|
|
73
|
+
fd = FirmenData(api_key="firmendata_live_...")
|
|
74
|
+
|
|
75
|
+
# Advanced search — filters combine with AND, lists with OR
|
|
76
|
+
results = fd.search(
|
|
77
|
+
city=["Berlin", "Hamburg"],
|
|
78
|
+
revenue_min=1_000_000,
|
|
79
|
+
legal_status=["insolvent"],
|
|
80
|
+
limit=25,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
for hit in results["data"]:
|
|
84
|
+
print(hit["display_name"], hit["address"]["city"])
|
|
85
|
+
|
|
86
|
+
# Paginate
|
|
87
|
+
if results["pagination"]["has_more"]:
|
|
88
|
+
next_page = fd.search(cursor=results["pagination"]["next_cursor"])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
eu_id = "DEB1103R_HRB123456"
|
|
93
|
+
|
|
94
|
+
fd.get_company(eu_id) # full profile
|
|
95
|
+
fd.get_financials(eu_id) # multi-year statements, parsed into figures
|
|
96
|
+
fd.get_shareholders(eu_id) # cap table from the Gesellschafterliste
|
|
97
|
+
fd.get_ubo(eu_id) # beneficial owners through ownership chains
|
|
98
|
+
fd.get_history(eu_id) # chronological register history
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Async
|
|
102
|
+
|
|
103
|
+
Same methods, same semantics:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import asyncio
|
|
107
|
+
from firmendata import AsyncFirmenData
|
|
108
|
+
|
|
109
|
+
async def main():
|
|
110
|
+
async with AsyncFirmenData(api_key="firmendata_live_...") as fd:
|
|
111
|
+
company = await fd.get_company("DEB1103R_HRB123456")
|
|
112
|
+
print(company["display_name"])
|
|
113
|
+
|
|
114
|
+
asyncio.run(main())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Errors
|
|
118
|
+
|
|
119
|
+
Every failure is a typed exception carrying the API's RFC 7807 problem detail,
|
|
120
|
+
including a `request_id` you can quote to support.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from firmendata import FirmenData, InsufficientCreditsError, RateLimitError
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
fd.get_ubo(eu_id)
|
|
127
|
+
except InsufficientCreditsError:
|
|
128
|
+
... # top up or upgrade
|
|
129
|
+
except RateLimitError as e:
|
|
130
|
+
... # e.retry_after is the server's own hint
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| Exception | Status | Meaning |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `AuthenticationError` | 401 | Missing/invalid key, or a keyless call used a paid feature |
|
|
136
|
+
| `TokenExpiredError` | 401 | Key expired |
|
|
137
|
+
| `InsufficientCreditsError` | 402 | Balance too low for this call |
|
|
138
|
+
| `NotFoundError` | 404 | No such company, subscription or event |
|
|
139
|
+
| `ConflictError` | 409 | Conflicts with existing state |
|
|
140
|
+
| `ValidationError` | 422 | Bad parameters — see `.errors` for the fields |
|
|
141
|
+
| `RateLimitError` | 429 | Retry budget exhausted — see `.retry_after` |
|
|
142
|
+
| `ServerError` | 5xx | Retried automatically for idempotent calls |
|
|
143
|
+
| `APIConnectionError` / `APITimeoutError` | — | No response at all |
|
|
144
|
+
|
|
145
|
+
### Retries
|
|
146
|
+
|
|
147
|
+
Automatic and deliberately conservative:
|
|
148
|
+
|
|
149
|
+
- **429 is always retried**, on any method — the server rejects rate-limited
|
|
150
|
+
calls before the handler runs, so nothing happened and nothing was billed.
|
|
151
|
+
The server's `Retry-After` is used verbatim.
|
|
152
|
+
- **5xx and connection failures are retried only for idempotent methods.** A
|
|
153
|
+
`create_subscription` that times out may already have been applied; replaying
|
|
154
|
+
it would create a second one.
|
|
155
|
+
- Backoff is exponential with full jitter, so clients that trip the same limit
|
|
156
|
+
together don't all return at the same instant.
|
|
157
|
+
|
|
158
|
+
Tune with `FirmenData(max_retries=...)`; `0` disables it.
|
|
159
|
+
|
|
160
|
+
## Types
|
|
161
|
+
|
|
162
|
+
Responses are plain dictionaries described by generated `TypedDict`s, so editors
|
|
163
|
+
complete every field and `mypy` checks them — with **no pydantic dependency** to
|
|
164
|
+
collide with your own. The only runtime requirement is `httpx`.
|
|
165
|
+
|
|
166
|
+
Both `src/firmendata/types.py` and `src/firmendata/params.py` are generated from
|
|
167
|
+
[`contracts/openapi.v1.json`](contracts/openapi.v1.json), a vendored copy of the
|
|
168
|
+
published spec:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python scripts/generate_types.py
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
CI regenerates them and fails if the result differs from what is committed, so
|
|
175
|
+
the SDK cannot silently drift from the API it targets.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install -e '.[dev]'
|
|
181
|
+
pytest # no network, no credentials
|
|
182
|
+
mypy && ruff check
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Links
|
|
186
|
+
|
|
187
|
+
- API reference — <https://api.firmendata.com/v1/docs>
|
|
188
|
+
- TypeScript SDK — <https://github.com/FirmenData/firmendata-node>
|
|
189
|
+
- n8n node — <https://github.com/FirmenData/n8n-nodes-firmendata>
|
|
190
|
+
- MCP server (for AI agents) — `https://mcp.firmendata.com/mcp`
|
|
191
|
+
- Website — <https://firmendata.com>
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# firmendata-python
|
|
2
|
+
|
|
3
|
+
Official Python client for the [firmendata](https://firmendata.com) API — data on
|
|
4
|
+
**2.4 million German companies** from the Unternehmensregister and Handelsregister:
|
|
5
|
+
register profiles, parsed annual financial statements, shareholder cap tables,
|
|
6
|
+
UBO chains, insolvency notices and public-tender links.
|
|
7
|
+
|
|
8
|
+
[](https://pypi.org/project/firmendata/)
|
|
9
|
+
[](https://pypi.org/project/firmendata/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install firmendata
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Try it without signing up
|
|
17
|
+
|
|
18
|
+
Company-name autocomplete is free and needs **no API key**:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from firmendata import FirmenData
|
|
22
|
+
|
|
23
|
+
for hit in FirmenData().autocomplete("siemens")["data"]:
|
|
24
|
+
print(hit["eu_id"], hit["display_name"])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Keyless calls are rate limited, modestly and by address — enough to try the
|
|
28
|
+
API, back a search box, or run low-volume queries. Add a key for substantially
|
|
29
|
+
higher limits plus every other endpoint. On a `429`, honour `Retry-After`;
|
|
30
|
+
the client already does this for you.
|
|
31
|
+
|
|
32
|
+
## With an API key
|
|
33
|
+
|
|
34
|
+
Create one at [firmendata.com](https://firmendata.com/de/account/api-keys) — the
|
|
35
|
+
free plan includes 100 credits.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from firmendata import FirmenData
|
|
39
|
+
|
|
40
|
+
fd = FirmenData(api_key="firmendata_live_...")
|
|
41
|
+
|
|
42
|
+
# Advanced search — filters combine with AND, lists with OR
|
|
43
|
+
results = fd.search(
|
|
44
|
+
city=["Berlin", "Hamburg"],
|
|
45
|
+
revenue_min=1_000_000,
|
|
46
|
+
legal_status=["insolvent"],
|
|
47
|
+
limit=25,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
for hit in results["data"]:
|
|
51
|
+
print(hit["display_name"], hit["address"]["city"])
|
|
52
|
+
|
|
53
|
+
# Paginate
|
|
54
|
+
if results["pagination"]["has_more"]:
|
|
55
|
+
next_page = fd.search(cursor=results["pagination"]["next_cursor"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
eu_id = "DEB1103R_HRB123456"
|
|
60
|
+
|
|
61
|
+
fd.get_company(eu_id) # full profile
|
|
62
|
+
fd.get_financials(eu_id) # multi-year statements, parsed into figures
|
|
63
|
+
fd.get_shareholders(eu_id) # cap table from the Gesellschafterliste
|
|
64
|
+
fd.get_ubo(eu_id) # beneficial owners through ownership chains
|
|
65
|
+
fd.get_history(eu_id) # chronological register history
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Async
|
|
69
|
+
|
|
70
|
+
Same methods, same semantics:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
from firmendata import AsyncFirmenData
|
|
75
|
+
|
|
76
|
+
async def main():
|
|
77
|
+
async with AsyncFirmenData(api_key="firmendata_live_...") as fd:
|
|
78
|
+
company = await fd.get_company("DEB1103R_HRB123456")
|
|
79
|
+
print(company["display_name"])
|
|
80
|
+
|
|
81
|
+
asyncio.run(main())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Errors
|
|
85
|
+
|
|
86
|
+
Every failure is a typed exception carrying the API's RFC 7807 problem detail,
|
|
87
|
+
including a `request_id` you can quote to support.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from firmendata import FirmenData, InsufficientCreditsError, RateLimitError
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
fd.get_ubo(eu_id)
|
|
94
|
+
except InsufficientCreditsError:
|
|
95
|
+
... # top up or upgrade
|
|
96
|
+
except RateLimitError as e:
|
|
97
|
+
... # e.retry_after is the server's own hint
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| Exception | Status | Meaning |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `AuthenticationError` | 401 | Missing/invalid key, or a keyless call used a paid feature |
|
|
103
|
+
| `TokenExpiredError` | 401 | Key expired |
|
|
104
|
+
| `InsufficientCreditsError` | 402 | Balance too low for this call |
|
|
105
|
+
| `NotFoundError` | 404 | No such company, subscription or event |
|
|
106
|
+
| `ConflictError` | 409 | Conflicts with existing state |
|
|
107
|
+
| `ValidationError` | 422 | Bad parameters — see `.errors` for the fields |
|
|
108
|
+
| `RateLimitError` | 429 | Retry budget exhausted — see `.retry_after` |
|
|
109
|
+
| `ServerError` | 5xx | Retried automatically for idempotent calls |
|
|
110
|
+
| `APIConnectionError` / `APITimeoutError` | — | No response at all |
|
|
111
|
+
|
|
112
|
+
### Retries
|
|
113
|
+
|
|
114
|
+
Automatic and deliberately conservative:
|
|
115
|
+
|
|
116
|
+
- **429 is always retried**, on any method — the server rejects rate-limited
|
|
117
|
+
calls before the handler runs, so nothing happened and nothing was billed.
|
|
118
|
+
The server's `Retry-After` is used verbatim.
|
|
119
|
+
- **5xx and connection failures are retried only for idempotent methods.** A
|
|
120
|
+
`create_subscription` that times out may already have been applied; replaying
|
|
121
|
+
it would create a second one.
|
|
122
|
+
- Backoff is exponential with full jitter, so clients that trip the same limit
|
|
123
|
+
together don't all return at the same instant.
|
|
124
|
+
|
|
125
|
+
Tune with `FirmenData(max_retries=...)`; `0` disables it.
|
|
126
|
+
|
|
127
|
+
## Types
|
|
128
|
+
|
|
129
|
+
Responses are plain dictionaries described by generated `TypedDict`s, so editors
|
|
130
|
+
complete every field and `mypy` checks them — with **no pydantic dependency** to
|
|
131
|
+
collide with your own. The only runtime requirement is `httpx`.
|
|
132
|
+
|
|
133
|
+
Both `src/firmendata/types.py` and `src/firmendata/params.py` are generated from
|
|
134
|
+
[`contracts/openapi.v1.json`](contracts/openapi.v1.json), a vendored copy of the
|
|
135
|
+
published spec:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python scripts/generate_types.py
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
CI regenerates them and fails if the result differs from what is committed, so
|
|
142
|
+
the SDK cannot silently drift from the API it targets.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install -e '.[dev]'
|
|
148
|
+
pytest # no network, no credentials
|
|
149
|
+
mypy && ruff check
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Links
|
|
153
|
+
|
|
154
|
+
- API reference — <https://api.firmendata.com/v1/docs>
|
|
155
|
+
- TypeScript SDK — <https://github.com/FirmenData/firmendata-node>
|
|
156
|
+
- n8n node — <https://github.com/FirmenData/n8n-nodes-firmendata>
|
|
157
|
+
- MCP server (for AI agents) — `https://mcp.firmendata.com/mcp`
|
|
158
|
+
- Website — <https://firmendata.com>
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT — see [LICENSE](LICENSE).
|