Metadata-Version: 2.5
Name: firmadb
Version: 0.1.0
Summary: Python SDK for the FirmaDB European company data API
Project-URL: Homepage, https://firmadb.com
Project-URL: Documentation, https://docs.firmadb.com
Author-email: FirmaDB <support@firmadb.com>
License: MIT
License-File: LICENSE
Keywords: company-data,europe,firmadb,kyb,registry
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Requires-Dist: httpx>=0.24.0
Description-Content-Type: text/markdown

# FirmaDB Python SDK

Official Python client for the [FirmaDB](https://firmadb.com) European company data API. ~25.0M entities across 18 European countries, sourced from official government registries. The precise served figure changes with every crawl — `GET /v1/countries` is the live number.

## Install

```bash
pip install firmadb
```

Requires Python 3.8+ and `httpx>=0.24`.

## Quickstart

```python
from firmadb import FirmaDB

client = FirmaDB(api_key="fdb_...")
# or set FIRMADB_API_KEY in the environment and: FirmaDB()

# Exact lookup by country + national registry id
company = client.companies.get(country="FR", registry_id="552120222")
print(company.name, company.status, company.nace_code)

# Fuzzy search
results = client.companies.search("Société Générale", country="FR", limit=5)
for hit in results:
    print(f"{hit.match.score:.2f}  {hit.name}  ({hit.registry_id})")

# Batch enrichment (1-100 references per call, idempotent)
batch = client.companies.lookup_batch(
    items=[
        {"country": "FR", "registry_id": "552120222"},
        {"country": "GB", "registry_id": "12345678"},
    ],
)
for row in batch.results:
    if row.status == "found":
        print(row.company.name)
    else:
        print("not found:", row.error["detail"])

# Coverage / capability discovery (anonymous)
for c in client.countries.list():
    print(f"{c.code}  {c.record_count:>10,d}  {c.registry_name}")

# Account usage and rate-limit state
usage = client.account.usage()
print(usage.plan, usage.consumed_units, usage.included_units, usage.remaining_units)
print(client.rate_limit)  # snapshot from the most recent response
```

## Errors

Every non-2xx response is an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html) Problem Detail. The SDK parses the body and raises a typed subclass of `FirmaDBError`. **Branch on the `code` attribute, never on `title` or `detail`.**

```python
from firmadb import FirmaDB, NotFoundError, RateLimitError, InvalidCountryError

try:
    client.companies.get(country="FR", registry_id="999999999")
except NotFoundError as e:
    print(e.correction)        # "Try searching by name with /v1/companies/search?country=FR&q=..."
    print(e.country_freshness) # {"country": "FR", "last_loaded_at": "...", "record_count": 16851670}
except InvalidCountryError as e:
    print(e.supported_countries)
except RateLimitError as e:
    print(e.retry_after, e.limit)
```

| Status | Code | Exception |
|---|---|---|
| 400 | `invalid_country` | `InvalidCountryError` |
| 400 | `country_required` | `CountryRequiredError` |
| 400 | `invalid_registry_id` | `InvalidRegistryIdError` |
| 400 | `query_too_short` | `QueryTooShortError` |
| 400 | `invalid_parameter` | `InvalidParameterError` |
| 400 | `invalid_cursor` | `InvalidCursorError` |
| 400 | `batch_validation_failed` | `BatchValidationError` |
| 401 | `unauthenticated` | `AuthenticationError` |
| 402 | `payment_required` | `PaymentRequiredError` |
| 403 | `insufficient_scope` | `InsufficientScopeError` |
| 404 | `company_not_found` | `NotFoundError` |
| 409 | `idempotency_conflict` | `ConflictError` |
| 429 | `rate_limit_exceeded` | `RateLimitError` |
| 429 | `quota_exhausted` | `QuotaExhaustedError` |
| 503 | `search_unavailable` / `source_unavailable` | `ServiceUnavailableError` |

## Retries

The client automatically retries `rate_limit_exceeded` and `service_unavailable` responses, honoring `Retry-After`. Tune via `max_retries=` (default 3). Other 4xx are raised immediately — they will not succeed on retry.

## Rate limit headers

Every response refreshes `client.rate_limit`:

```python
client.rate_limit.limit             # per-minute ceiling
client.rate_limit.remaining
client.rate_limit.reset_seconds
client.rate_limit.resource          # "companies-read", "companies-search", ...
client.rate_limit.credits_remaining # monthly verified-result quota left
```

## Async

```python
import asyncio
from firmadb import AsyncFirmaDB

async def main():
    async with AsyncFirmaDB(api_key="fdb_...") as client:
        company = await client.companies.get(country="FR", registry_id="552120222")
        print(company.name)

asyncio.run(main())
```

## Methods

| Method | Endpoint |
|---|---|
| `client.companies.get(country=, registry_id=, include=, include_nulls=)` | `GET /companies/{country}/{registry_id}` |
| `client.companies.search(query, country=, nace=, status=, ..., limit=, cursor=)` | `GET /companies/search` |
| `client.companies.lookup_batch(items, idempotency_key=, include=)` | `POST /companies/lookup-batch` |
| `client.countries.list()` | `GET /countries` |
| `client.countries.get(code)` | `GET /countries/{code}` |
| `client.account.usage()` | `GET /account/usage` |
| `client.health()` | `GET /health` |

Full reference at [docs.firmadb.com](https://docs.firmadb.com).

## License

MIT
