Metadata-Version: 2.4
Name: ares-client
Version: 0.3.0
Summary: Typed async client for ARES (Administrativní registr ekonomických subjektů), the Czech business registry API at ares.gov.cz
Keywords: ares,ares.gov.cz,ico,ičo,dic,dič,rejstřík,czech business registry
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Framework :: Pydantic :: 2
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Requires-Dist: pyrate-limiter>=3.7
Requires-Dist: tenacity>=9.0
Requires-Python: >=3.12
Project-URL: Repository, https://github.com/jogobeny/ares-client
Project-URL: Issues, https://github.com/jogobeny/ares-client/issues
Project-URL: ARES API, https://ares.gov.cz/swagger-ui/
Description-Content-Type: text/markdown

# ares-client

[![PyPI](https://img.shields.io/pypi/v/ares-client)](https://pypi.org/project/ares-client/)
[![Python](https://img.shields.io/pypi/pyversions/ares-client)](https://pypi.org/project/ares-client/)
[![License](https://img.shields.io/pypi/l/ares-client)](https://github.com/jogobeny/ares-client/blob/main/LICENSE)

Typed async client for **ARES** (Administrativní registr ekonomických subjektů), the Czech business registry API at [ares.gov.cz](https://ares.gov.cz/swagger-ui/).

- Fully typed: pydantic v2 models ([`models.py`]) and a typed exception tree ([`exceptions.py`]).
- The client retries failed requests with exponential backoff ([`client.py`]). It also obeys the ARES limit of [500 requests each minute][limits].
- The client validates an IČO locally ([`ico.py`]). Invalid input does not go to the network.

The library is **async only**. In synchronous code, wrap each call in `asyncio.run(...)`.

## Installation

The library needs **Python 3.12+**.

```bash
uv add ares-client   # or: pip install ares-client
```

## Quick start

```python
import asyncio

from ares_client import AresClient


async def main():
    async with AresClient() as ares:
        subject = await ares.get_subject("27082440")  # an int is also valid, zeros are added

    print(subject.obchodni_jmeno)  # Alza.cz a.s.
    print(subject.sidlo.textova_adresa)  # Jankovcova 1522/53, Holešovice, 17000 Praha 7
    print(subject.dic)  # CZ27082440
    print(subject.seznam_registraci.dph)  # SourceState.AKTIVNI
    print(subject.datum_zaniku)  # None — the subject continues
    print(subject.datum_vzniku)  # datetime.date(2003, 8, 26)


asyncio.run(main())
```

## Client methods

| Method | Description |
|---|---|
| `get_subject(ico)` | Get one subject, **with the ROS and RŽP detail merged in**. An unknown IČO causes `SubjectNotFound`. |
| `search(filter)` | Get the subjects that match a filter (`SubjectList`). |
| `search_code_lists(filter)` | Get the ARES code lists — the translations of codes to names. |

`get_subject` sends three parallel requests — `/ekonomicke-subjekty`, `/ekonomicke-subjekty-ros`, and `/ekonomicke-subjekty-rzp` — and returns one `Subject` with the ROS records in `zaznamy_ros` and the RŽP records in `zaznamy_rzp`. A register that does not know the subject leaves its list empty.

A ROS record (Registr osob) adds what the base subject lacks: contacts, data boxes, establishments, statutory bodies, liquidators, and the other engagements. ROS keeps one record for each evidence period, so the full history is there; the current record has `stav_subjektu == SourceState.AKTIVNI`.

```python
subject = await ares.get_subject("27082440")
zaznam = next(z for z in subject.zaznamy_ros if z.stav_subjektu is SourceState.AKTIVNI)

print(zaznam.kontaktni_udaje.email)  # petr.bena@alza.cz
print(zaznam.datove_schranky[0].identifikator_ds)  # xtkcrkz
print(zaznam.statutarni_organy[0].osoba_fyzicka.osoba_rob.prijmeni.hodnota)  # ZAVORAL
print(zaznam.provozovny[0].adresa_provozovny.textova_adresa)  # Komunardů 306/1, ... Praha 7
```

A RŽP record (Registr živnostenského podnikání) adds the trade licences with their establishments, fields of activity, and responsible representatives. For a sole trader it also names the person behind the business name, which the base subject reduces to `obchodni_jmeno`.

```python
subject = await ares.get_subject("27082440")
zaznam = subject.zaznamy_rzp[0]

print(zaznam.zivnosti_stav.pocet_aktivnich)  # 8
print(zaznam.zivnosti[0].predmet_podnikani)  # Hostinská činnost
print(zaznam.zivnosti[0].druh_zivnosti)  # R — a code list, like the other code fields
print(zaznam.zivnosti[0].provozovny[0].sidlo_provozovny.textova_adresa)  # U Pergamenky 1522/2, ...
print(zaznam.zivnosti[0].odpovedni_zastupci[0].typ_angazma)  # ODPOVEDNY_ZASTUPCE_RZP
```

For a sole trader (`typ_subjektu == "F"`), `osoba_podnikatel` names the person behind the business name; a legal entity leaves it `None`.

A search result never carries the ROS or RŽP records — ARES search does not return them.

```python
# search
page = await ares.search(SubjectFilter(obchodni_jmeno="Alza", pocet=10))
print(page.pocet_celkem, [s.obchodni_jmeno for s in page.ekonomicke_subjekty])

# ARES never returns more than 1 000 results, so one request can hold them all
page = await ares.search(SubjectFilter(obchodni_jmeno="Alza", pocet=1000))
```

You can also make the filter first and send it later:

```python
from ares_client import AddressFilter, SubjectFilter

filter = SubjectFilter(
    pravni_forma=["112"],  # s.r.o. (limited liability company)
    sidlo=AddressFilter(kod_obce=554782),  # Praha (RÚIAN code)
)
page = await ares.search(filter)
```

## The `Subject` model

The full model, with the same field names and structure as the ARES schema, is in [`models.py`]. For example, the state of the subject in the source registers:

```python
from ares_client import SourceState

if subject.seznam_registraci.rzp is SourceState.AKTIVNI:
    ...  # the subject has an active trade licence
```

## Errors

The errors that ARES reports are typed ([`exceptions.py`]):

```
AresError
├─ InvalidIco             # fails locally, without an API call
└─ AresAPIError           # has kod, sub_kod, popis, status_code
   ├─ SubjectNotFound
   ├─ InvalidRequest
   │  └─ TooManyResults   # has .found — the number of matched subjects
   └─ AresServerError
```

**The client does not wrap network errors.** A timeout or a refused connection is not an ARES error. These errors keep their httpx form. Thus the caller sees the true cause and can use the usual httpx logic:

```python
import httpx
from ares_client import AresClient, AresError

try:
    subject = await ares.get_subject("27082440")
except httpx.TimeoutException:
    ...  # the connection was too slow, try again later
except AresError:
    ...  # ARES replied with an error
```

The client retries network errors (`httpx.TransportError`) first. Only the last error goes to the caller.

## ARES limits that the client knows

| Limit | Behavior |
|---|---|
| Max **1 000** results for each query | ARES refuses a wider query (it does **not** cut the result). You get `TooManyResults` with the match count |
| Max **100 IČO** values in a filter | ARES refuses more (`InvalidRequest`) |
| [**500 requests each minute**][limits] | The client obeys the limit with [`pyrate-limiter`]. When the window is full, the request **waits**. It does not fail. ARES sends no rate-limit headers, so a client cannot react to an error |
| An IČO has exactly 8 digits | `normalize_ico()` adds the leading zeros. A character that is not a digit is an error |
| An empty filter | ARES refuses it: `InvalidRequest` with `sub_kod=VSTUP_PRAZDNY` |

The client retries a request after `408`, `429`, `5xx`, and network errors (exponential backoff with jitter). It does not retry `4xx` input errors. A retry cannot help there.

## Important notes

- **`pravni_forma`, `cz_nace`, and `financni_urad` are code-list codes, not names** (`"112"` = s.r.o.). Use `search_code_lists()` to translate the codes to names.
- **Almost all fields are optional.** The Ministry of Finance has no `datum_vzniku`. A self-employed person frequently has no `dic`.
- **Read the VAT registration from `seznam_registraci.dph`.** This is the ARES state, not the [VAT payer register][adisreg] of the Financial Administration. For tax decisions (for example the § 109 liability), use that register.
- ARES contains only Czech registrations. It has no foreign subjects.

## Resources

- [Swagger UI](https://ares.gov.cz/swagger-ui/) · [OpenAPI spec][openapi]
- [Technical documentation, MF (PDF)][mf-pdf]
- [Terms of use](https://data.mf.gov.cz/api/ares.html)

[`models.py`]: https://github.com/jogobeny/ares-client/blob/main/src/ares_client/models.py
[`exceptions.py`]: https://github.com/jogobeny/ares-client/blob/main/src/ares_client/exceptions.py
[`client.py`]: https://github.com/jogobeny/ares-client/blob/main/src/ares_client/client.py
[`ico.py`]: https://github.com/jogobeny/ares-client/blob/main/src/ares_client/ico.py
[openapi]: https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/v3/api-docs
[limits]: https://ares.gov.cz/stranky/podminky-provozu
[adisreg]: https://adisspr.mfcr.cz/dpr/DphReg
[`pyrate-limiter`]: https://github.com/vutran1710/PyrateLimiter
[mf-pdf]: https://mf.gov.cz/assets/attachments/2023-08-01_ARES-Technicka-dokumentace-Katalog-verejnych-sluzeb_v07.pdf
