Metadata-Version: 2.4
Name: shab-parser
Version: 0.1.0
Summary: Parse publications from the Swiss Official Gazette of Commerce (SHAB/SOGC/FUSC)
Keywords: shab,sogc,fusc,swiss,commercial-register,handelsregister
Author: Prospex
Author-email: Prospex <hello@prospex.ch>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Text Processing :: Markup :: XML
Classifier: Typing :: Typed
Requires-Dist: pytest>=8 ; extra == 'dev'
Requires-Dist: httpx>=0.27 ; extra == 'dev'
Requires-Dist: httpx>=0.27 ; extra == 'http'
Requires-Python: >=3.14
Project-URL: Homepage, https://prospex.ch
Project-URL: Repository, https://github.com/ssidorenko/python-shab-parser
Project-URL: Documentation, https://shab-parser.readthedocs.io
Provides-Extra: dev
Provides-Extra: http
Description-Content-Type: text/markdown

# shab-parser

[![Documentation](https://readthedocs.org/projects/shab-parser/badge/?version=latest)](https://shab-parser.readthedocs.io/en/latest/)

Typed Python client for the Swiss Official Gazette of Commerce (SHAB/SOGC/FUSC).

Fetches publications from the [Amtsblattportal](https://amtsblattportal.ch) public API,
parses the XML into dataclasses, and classifies each publication into structured events
(incorporation, seat move, capital increase, deletion, and others).

Built and maintained by [Prospex](https://prospex.ch), a Swiss B2B sales intelligence platform.

## Install

```bash
pip install shab-parser
```

To use the HTTP client (for fetching from the live API):

```bash
pip install shab-parser[http]
```

## Quick start

### Parse XML you already have

```python
from shab_parser import parse_xml

with open("publication.xml", "rb") as f:
    pub = parse_xml(f.read())

print(pub.company_name)  # "Alpenblick Handel AG"
print(pub.uid)           # "CHE-123.456.789"
print(pub.canton)        # "ZH"

for event in pub.events:
    print(event.event_type, event.effective_date, event.payload)
```

### Fetch and parse from the API

```python
from datetime import date
from shab_parser.client import ShabClient
from shab_parser import parse

with ShabClient() as client:
    refs = client.discover(date(2026, 6, 15), date(2026, 6, 15))

    for ref in refs[:5]:
        raw = client.fetch(ref)
        pub = parse(raw)
        print(f"{pub.company_name}: {[e.event_type.value for e in pub.events]}")
```

The client rate-limits itself to one request per second and retries transient failures
with exponential backoff.

## Event types

The parser classifies each publication into one or more of these events,
based on the machine-readable XML fields (not free text):

| Event | Sub-rubric | Trigger |
|---|---|---|
| `INCORPORATION` | HR01 | `<registration>true</registration>` |
| `SEAT_MOVED` | HR02 | Different seat in commonsNew vs. commonsActual |
| `ADDRESS_CHANGED` | HR02 | `<addressChanged>true</addressChanged>` |
| `NAME_CHANGED` | HR02 | Different company name in commonsNew vs. commonsActual |
| `CAPITAL_INCREASED` | HR02 | Structured nominal comparison, phrase fallback |
| `LIQUIDATION` | any | Dissolution flags or "in Liquidation" added to name |
| `DELETED` | HR03 | `<delete>` block with deletion date |

## Data model

`parse()` and `parse_xml()` return a `Publication` dataclass:

```python
@dataclass(frozen=True)
class Publication:
    external_id: str
    publication_date: date
    language: str          # "de", "fr", or "it"
    source_url: str
    company_name: str
    raw_text: str
    sub_rubric: str        # "HR01", "HR02", or "HR03"
    effective_date: date | None
    canton: str | None
    uid: str | None        # CHE-xxx.xxx.xxx
    legal_form_code: str | None
    publication_state: str # "PUBLISHED" or "CANCELLED"
    events: list[Event]
    company_new: Company | None
    company_actual: Company | None
    capital_new: float | None
    capital_actual: float | None
```

## API reference

### `shab_parser.parse(raw: RawResponse) -> Publication`

Parse a `RawResponse` (as returned by `ShabClient.fetch()`) into a `Publication`.

### `shab_parser.parse_xml(xml_bytes, *, source_url="", ref_state=None) -> Publication`

Parse raw XML bytes directly. Use this when you already have the XML
and don't need the HTTP client.

### `shab_parser.client.ShabClient`

HTTP client for the Amtsblattportal API. Requires the `http` extra.

- `discover(start, end)` lists all HR publications in a date range.
  Queries both `PUBLISHED` and `CANCELLED` states, deduplicating by external ID.
- `fetch(ref)` downloads one publication's full XML.

### `shab_parser.client.parse_bulk_export(xml_bytes) -> (list[PublicationRef], int)`

Parse a bulk-export list page into publication references and a total count.
Useful if you handle pagination yourself.

## Background

SHAB (Schweizerisches Handelsamtsblatt) is the official gazette where Swiss commercial
register entries are published. Every new company, every seat change, every capital
increase, every deletion passes through it. The same publication appears in German, French,
and Italian, each under a different namespace (`HR01:`, `HR02:`, `HR03:`), but with
identical XML structure.

This library handles the namespace differences transparently using ElementPath's `{*}`
wildcard, so you get the same parsed output regardless of language.

## License

MIT
