Metadata-Version: 2.4
Name: beliq
Version: 0.2.0
Summary: Official beliq SDK: generate, validate, parse, and convert EU-compliant e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, drift-checked rules.
Project-URL: Homepage, https://beliq.eu
Project-URL: Repository, https://github.com/beliq-eu/beliq-sdk-python
Project-URL: Issues, https://github.com/beliq-eu/beliq-sdk-python/issues
Author-email: beliq <hello@beliq.eu>
License-Expression: MIT
License-File: LICENSE
Keywords: beliq,compliance,e-invoice,einvoice,en16931,factur-x,peppol,xrechnung,zugferd
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial :: Accounting
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# beliq

Official Python SDK for the [beliq](https://beliq.eu) e-invoicing compliance API. Generate, validate, parse, and convert EN 16931 invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, nightly-drift-checked rules.

beliq produces and checks the compliant document. Transmission (Peppol, PDP, KSeF, SDI), archiving, and tax-authority reporting stay with your access point.

## Install

```bash
pip install beliq
```

Requires Python >= 3.10.

## Quick start

```python
from beliq import Beliq

beliq = Beliq(api_key="blq_...")

# Account, plan, and quota context (no quota cost).
account = beliq.me()

# Generate an XRechnung document from an EN 16931 invoice.
generated = beliq.generate(
    standard="xrechnung",
    verify=True,
    invoice={
        "number": "INV-2026-001",
        "issueDate": "2026-01-15",
        "currencyCode": "EUR",
        "seller": {"name": "Seller GmbH", "address": {"city": "Berlin", "postalCode": "10115", "countryCode": "DE"}},
        "buyer": {"name": "Buyer GmbH", "address": {"city": "Munich", "postalCode": "80331", "countryCode": "DE"}},
        "lines": [
            {"description": "Consulting", "quantity": 10, "unitCode": "HUR", "unitPrice": 100, "lineTotal": 1000, "vatRate": 19, "vatCategoryCode": "S"}
        ],
        "totalNetAmount": 1000,
        "totalTaxAmount": 190,
        "totalGrossAmount": 1190,
    },
)
print(generated.xml, generated.meta.schematron_version)

# Validate any document against authority-pinned rules.
result = beliq.validate(generated.xml, format="auto")
if not result.valid:
    for issue in result.errors:
        print(issue.rule_id, issue.message)
```

## Authentication

Create an API key in the beliq dashboard under API Keys:

```python
Beliq(api_key="blq_...")                    # sends X-API-Key (default)
Beliq(api_key="blq_...", auth="bearer")      # sends Authorization: Bearer
Beliq(api_key="blq_...", base_url="https://staging.beliq.eu")
```

## Async

`AsyncBeliq` mirrors the sync client with `await`:

```python
import asyncio
from beliq import AsyncBeliq

async def main():
    async with AsyncBeliq(api_key="blq_...") as beliq:
        result = await beliq.validate(open("invoice.xml", "rb").read(), format="auto")
        print(result.valid)

asyncio.run(main())
```

## API

| Method | Endpoint | Input | Returns |
|---|---|---|---|
| `me()` | GET /v1/me | none | `AccountInfo` (no quota cost) |
| `generate(...)` | POST /v1/generate | EN 16931 invoice dict | `GenerateResult` |
| `validate(document, ...)` | POST /v1/validate | XML or PDF | `ValidationResult` |
| `parse(document, ...)` | POST /v1/parse | XML or PDF | `ParseResult` |
| `convert(document, ...)` | POST /v1/convert | XML or PDF | `ConvertResult` |

`document` accepts a `str`, `bytes`, or `bytearray`. The content type is sniffed from the bytes (PDF vs XML) unless you pass `content_type=`. `generate` and `convert` return the raw document `content` (bytes) plus the response-header metadata: `meta.schematron_version`, `meta.pdf_kind`, `meta.source_format`/`meta.target_format`, `meta.lost_elements`, `meta.conversion_tools`, the ruleset fingerprint `meta.ruleset_sha256` / `meta.ruleset_artifacts`, and `meta.livemode`. For an XML output, `generate` also decodes `xml`.

JSON responses are Pydantic models. Any field not explicitly typed (such as the per-country authority versions on a validation result) is preserved and accessible. Errors raise `BeliqApiError` with a typed `.code`, HTTP `.status`, and any `.details`:

```python
from beliq import BeliqApiError

try:
    beliq.validate("not xml")
except BeliqApiError as err:
    print(err.code, err.status, err.message)
```

## The seal: verify it yourself

Pass `seal=True` to `generate` to get the document back as a JSON envelope: the decoded `content` (bytes) plus its `sha256` and the full `validation_result`. Hashing the returned bytes reproduces the returned hash, so you can prove which ruleset the document passed.

```python
import hashlib

sealed = beliq.generate(standard="xrechnung", verify=True, invoice=invoice, seal=True)
assert hashlib.sha256(sealed.content).hexdigest() == sealed.sha256
print(sealed.validation_result.valid, sealed.meta.ruleset_sha256)
```

Without `seal`, `generate` returns the raw document body (unchanged, the default). The ruleset fingerprint (`meta.ruleset_sha256`, `meta.ruleset_artifacts`) is present in both modes.

## Sandbox and live keys

A `blq_test_` key is a sandbox key; a `blq_live_` key is live. The client derives the mode from the key prefix before any request:

```python
beliq = Beliq(api_key="blq_test_...")
beliq.livemode  # False for a sandbox key, True for a live key
```

Each `generate` / `convert` response also carries the authoritative mode from the server as `meta.livemode`.

## Generate presets

`LIVE_GENERATE_PRESETS` is the curated set of public generate targets (matching beliq.eu's own generator). NLCIUS is a Peppol BIS profile rather than a standalone standard, so it is reachable here:

```python
from beliq import LIVE_GENERATE_PRESETS

nlcius = next(p for p in LIVE_GENERATE_PRESETS if p.id == "nlcius")
beliq.generate(standard=nlcius.standard, profile=nlcius.profile, output=nlcius.output, invoice=invoice)
```

## Development

```bash
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
ruff check src tests
mypy
pytest                                   # unit tests (no network)
BELIQ_API_KEY=blq_xxx pytest tests/test_integration.py   # hits the live API; draws quota
```

`tests/test_spec_contract.py` reads the vendored `openapi.json` and fails if the error-code set, the core validate/seal fields, or the public option lists drift from the spec. Refresh the vendored spec with `python scripts/sync_spec.py`. A weekly workflow (`scripts/check_live_drift.py`) flags when the vendored spec falls behind the deployed API.

## Publishing

Released to PyPI as [`beliq`](https://pypi.org/project/beliq/). Releases run from `.github/workflows/release.yml` via PyPI Trusted Publishing (OIDC, with attestations): push a `v*.*.*` tag to publish. No token is stored in the repo.

## License

MIT
