Metadata-Version: 2.4
Name: xingen-sdk
Version: 0.1.0
Summary: Python client SDK for the Xingen e-invoice validation API
Project-URL: Homepage, https://github.com/nko-technologies/xingen-python-sdk
Project-URL: Repository, https://github.com/nko-technologies/xingen-python-sdk
Author-email: Manuel Gerstner <manuel@nko-technologies.com>
License-Expression: MIT
License-File: LICENSE
Keywords: cii,e-invoice,en16931,invoice,peppol,ubl,xingen,xrechnung
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pydantic>=2.6
Requires-Dist: requests>=2.31
Provides-Extra: dev
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# xingen

Python client SDK for the [Xingen](https://xingen.de) e-invoice validation API — submit UBL, CII,
ZUGFeRD, and SAP IDoc/OData invoices for validation against EN16931, XRechnung, and Peppol.

Requires Python 3.10+. Built on `requests` and `pydantic` v2.

> Status: v1, covering invoice submission/validation and API key management. Contacts and
> dashboard/user endpoints are not exposed (they're Firebase-auth-only on the backend).

## Install

```bash
pip install xingen-sdk
```

(The distribution is named `xingen-sdk` on PyPI since `xingen` was already taken by an unrelated
project, but the import name is still `xingen`: `from xingen import XingenClient`.)

## Authentication

Every request needs an API key (`xgn_live_...` for production, `xgn_test_...` for sandbox — sandbox
requests never count toward quota). Create one from the Xingen dashboard or via `client.api_keys`.

```python
from xingen import XingenClient

client = XingenClient(api_key=os.environ["XINGEN_API_KEY"])
```

`XingenClient` holds one connection-pooled `requests.Session` — construct it once and reuse it,
don't rebuild it per request. `base_url=...` overrides the default `https://app.xingen.de/api`,
useful for self-hosted or local (`./gradlew bootRun`, port 10001) testing.

## Validate a file

Every validate/submit endpoint is asynchronous — the backend queues the invoice and returns
immediately. Use a `*_and_wait` helper to submit and poll for the result in one call:

```python
from xingen.models.enums import InvoiceStatus, ValidationProfile

result = client.invoices.validate_file_and_wait("invoice.xml", ValidationProfile.XRECHNUNG)

if result.status is InvoiceStatus.VALIDATED and result.validation_result.valid:
    print("Valid!")
else:
    for error in result.validation_result.errors:
        print(f"{error.severity}: {error.message} ({error.field})")
```

`PollOptions` controls the backoff (`initial_interval`, `max_interval`, `backoff_multiplier`), the
overall `timeout`, and an optional `cancellation_check`. A **failed validation is not an exception**
— it's a completed API call that found the invoice invalid, so `*_and_wait` returns normally with
`validation_result.valid == False`. Only a transport failure, cancellation, or timeout raises.

```python
from xingen.invoices import PollOptions

options = PollOptions(initial_interval=0.3, max_interval=3.0, timeout=30.0)
result = client.invoices.validate_file_and_wait("invoice.xml", ValidationProfile.XRECHNUNG, options)
```

If you'd rather manage polling yourself, use the low-level pair:

```python
submitted = client.invoices.validate_file("invoice.xml", ValidationProfile.EN16931)
# ... later ...
record = client.invoices.get(submitted.id)
```

`validate_idoc` / `validate_idoc_and_wait` work the same way for SAP IDoc XML files. Both, and
`validate_file`/`validate_file_and_wait`, also accept `(filename, content_bytes)` tuples if you
already hold the file bytes in memory instead of a path.

## Submit a structured invoice (JSON)

```python
from datetime import date
from decimal import Decimal

from xingen.invoices import InvoiceSubmission, LineInput, PartyInput

submission = InvoiceSubmission(
    invoice_number="INV-2024-0042",
    issue_date=date(2024, 3, 15),
    currency="EUR",
    buyer_reference="991-12345-06",
    validation_profile=ValidationProfile.XRECHNUNG,
    supplier=PartyInput(name="Acme GmbH", vat_id="DE123456789"),
    buyer=PartyInput(name="Buyer Co", leitweg_id="991-12345-06"),
    lines=[
        LineInput(
            description="Software License Q1",
            quantity=Decimal("5"),
            unit="C62",
            price=Decimal("199.00"),
            tax_rate=Decimal("19"),
        )
    ],
)

result = client.invoices.submit_and_wait(submission)
```

SAP S/4HANA OData supplier-invoice payloads are supported as a thin passthrough — pass raw JSON or
a `dict` rather than a fully typed model:

```python
client.invoices.submit_odata(raw_odata_json, ValidationProfile.EN16931)
```

## List and retrieve invoices

```python
page = client.invoices.list(0, 20, "createdAt,desc")

# or, to walk every invoice without managing page indices yourself:
for record in client.invoices.list_all(50):
    print(record.id, "->", record.status)

one = client.invoices.get("inv_01HXYZ")
```

## Download results

```python
pdf = client.invoices.download_pdf(id)          # ZUGFeRD PDF with embedded XML
idoc_xml = client.invoices.download_idoc_xml(id)  # SAP IDoc XML
```

## API keys

```python
from xingen.apikeys import CreateApiKeyRequest

created = client.api_keys.create(CreateApiKeyRequest(name="Production CI", sandbox=False))
print("Store this now, it's shown only once:", created.raw_key)

keys = client.api_keys.list()
client.api_keys.revoke(created.id)
```

## Error handling

All SDK exceptions extend `XingenException`. HTTP errors map to typed subclasses of `ApiException`:

| Exception | Status | Notes |
|---|---|---|
| `AuthenticationException` | 401 | Missing or invalid API key |
| `PermissionException` | 403 | Resource exists but isn't owned by the caller |
| `NotFoundException` | 404 | |
| `ValidationRequestException` | 400 | `.field_errors` has details for request-body validation failures |
| `QuotaExceededException` | 429 | Monthly request quota exhausted |
| `ApiException` | other 4xx/5xx | Fallback; `.status_code` / `.raw_body` always available |

```python
from xingen.error import QuotaExceededException, ValidationRequestException, XingenException

try:
    client.invoices.submit(submission)
except ValidationRequestException as e:
    for field, message in e.field_errors.items():
        print(f"{field}: {message}")
except QuotaExceededException:
    print("Quota exceeded — upgrade or wait for the next billing period")
except XingenException as e:
    print(f"Request failed: {e}")
```

## Design notes

- **No automatic retries.** Retrying a `submit()` after a client-side timeout is unsafe without
  idempotency keys, which the API doesn't support yet — a retried submit could create a duplicate
  invoice. Handle retries at the call site if you need them.
- **Monetary and quantity fields are `Decimal`, never `float`.** JSON is parsed through Python's
  stdlib `json` module with `parse_float=Decimal` rather than pydantic's native (float64-lossy)
  JSON parser, so exact literal precision is preserved end to end.

## Contributing

```bash
pip install -e ".[dev]"
ruff check .
mypy src/xingen
pytest
```

Tests run against a real (loopback) `http.server.HTTPServer`, not a mocking framework — no network
calls leave the machine, and no external test-server dependency is required.

## License

MIT — see [LICENSE](LICENSE).
