Metadata-Version: 2.4
Name: libciss
Version: 0.1.0a2
Summary: Python client for CISS applications
Author: Redson
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31.0

# libciss

Python client for [CISS](https://www.ciss.com.br/) applications. Currently supports **CISS Control**, with helpers for authentication and **pré-carga / separação** (warehouse pre-load picking).

> **Status:** `0.1.0a2` (alpha) — the API surface may change.

## Requirements

- Python 3.10+ (uses `X | None` type syntax and `dataclass(slots=True)`)
- `requests>=2.31.0`

## Installation

```bash
pip install libciss
```

Everything is imported from the single top-level `libciss` package:

```python
from libciss import CISSClient
```

> **Upgrading from `0.1.0a1`:** that release was unusable — it shipped its
> modules under a top-level `services` package (which collided with any
> application that had its own `services/`), left `client` and `exceptions`
> out of the distribution entirely, and imported from a `src` package that
> was never packaged. `0.1.0a2` moves everything under `libciss`, so
> `from src import ...` becomes `from libciss import ...`.

## Quick start

```python
from datetime import datetime

from libciss import CISSClient
from libciss.services.cisscontrol.models import PreCargaSeparacaoSubmission

client = CISSClient("https://your-ciss-host.example.com")

# Authenticate — stores the bearer token on the shared session
user = client.cisscontrol.auth.login("username", "password")
print(user.id_empresa, user.razao_social)

# List pre-loads
preloads = client.cisscontrol.precarga_separacao.get_all(
    id_empresa=user.id_empresa,
    num_dias_sync=30,
)

# Fetch the line items of one pre-load
items = client.cisscontrol.precarga_separacao.items(
    id_empresa=user.id_empresa,
    id_precarga=preloads[0].id_pre_carga,
    id_usuario=user.id_user,
)

# Report a separated item back to CISS
item = items[0]
submission = PreCargaSeparacaoSubmission(
    id_usuario=user.id_user,
    id_empresa=item.id_empresa,
    id_documento=item.id_documento,
    id_pre_carga=item.id_pre_carga,
    id_produto=item.id_produto,
    id_subproduto=item.id_subproduto,
    id_local_retirada=item.id_local_retirada,
    ds_local_retirada=item.ds_local_retirada,
    num_sequencia=item.num_sequencia,
    qtd_produto=item.qtd_produto,
    fg_separado="S",
    qtd_contada=item.qtd_produto,
    dt_hr_inicio=datetime.now(),
)

response = client.cisscontrol.precarga_separacao.submit([submission])

client.cisscontrol.auth.logout()
```

## Architecture

One `CISSClient` owns a single `requests.Session`. Services never hold their own transport — they call back into `client.request(...)`, so the token set at login is automatically applied to every later call.

```text
CISSClient(base_url)
├── session          # requests.Session (shared headers / cookies)
├── request(...)     # low-level HTTP helper, raises on 4xx/5xx
└── cisscontrol      # CISSControl
      ├── auth                 # AuthenticationService
      │     ├── login / logout
      │     ├── token / authenticated
      │     └── user → LoginResponse | None
      └── precarga_separacao   # PreCargaSeparacaoService
            ├── get_all(...)  → list[PreCargaSeparacao]
            ├── items(...)    → list[PreCargaSeparacaoItem]
            └── submit(...)   → requests.Response
```

## API reference

### `CISSClient`

Entry point for all services.

```python
CISSClient(base_url: str)
```

| Attribute / method | Description |
|--------------------|-------------|
| `base_url` | API root URL; a trailing `/` is stripped |
| `session` | Shared `requests.Session` — holds the `Authorization` header after login |
| `cisscontrol` | `CISSControl` service namespace |
| `request(method, endpoint, **kwargs)` | Sends `method` to `base_url + endpoint`, calls `raise_for_status()`, returns the `Response` |

`endpoint` is concatenated onto `base_url` verbatim, so it must start with `/`. Any extra keyword argument is forwarded to `requests.Session.request` (`json=`, `params=`, `timeout=`, …).

Session-level configuration goes on `client.session`:

```python
client.session.verify = "/path/to/ca-bundle.pem"
client.session.proxies = {"https": "http://proxy.internal:3128"}
```

Note that `requests.Session` has **no** `timeout` attribute — setting one has no effect. To bound a request, pass `timeout` through `request()`, or mount your own adapter:

```python
client.request("GET", "/ciss-control/precarga_separacao", params={...}, timeout=30)
```

The service methods below do not currently expose a `timeout` parameter.

---

### Authentication — `client.cisscontrol.auth`

#### `login(username, password) -> LoginResponse`

`POST /ciss-control/login`

Sends `{"username": ..., "password": ...}` as JSON. On success it writes the returned `tokenBearer` into `session.headers["Authorization"]` and caches the parsed payload on `auth.user`.

The token is stored exactly as the API returns it — the library does not prepend a `Bearer` prefix. `razao_social` is right-stripped of trailing whitespace; all other fields are passed through unchanged.

#### `logout() -> None`

Removes the `Authorization` header and clears `auth.user`. **Client-side only** — no request is sent, so the token stays valid server-side until it expires.

#### Properties

| Property | Type | Description |
|----------|------|-------------|
| `token` | `str \| None` | Current `Authorization` header value, or `None` |
| `authenticated` | `bool` | `True` when a token is set |
| `user` | `LoginResponse \| None` | Payload of the last successful login |

`authenticated` only reports whether a token is *present* — it does not validate or check expiry. An expired token still reads as `True`, and the call then fails with `requests.HTTPError` (401).

#### `requires_login` decorator

`libciss.services.cisscontrol.auth.requires_login` guards service methods. It reads `self.client.cisscontrol.auth.authenticated` and raises `NotAuthenticatedError` when no token is set, so it can only decorate methods of a service that holds a `client` attribute.

```python
from libciss.services.cisscontrol.auth import requires_login

class MyService:
    def __init__(self, client):
        self.client = client

    @requires_login
    def do_something(self):
        return self.client.request("GET", "/ciss-control/something")
```

---

### Pré-carga separação — `client.cisscontrol.precarga_separacao`

Every method is decorated with `@requires_login` and raises `NotAuthenticatedError` when called before `login()`.

#### `get_all(*, id_empresa, ...) -> list[PreCargaSeparacao]`

`GET /ciss-control/precarga_separacao`

Keyword-only. All filter parameters are always sent, empty strings included.

| Parameter | Type | Default | API query key |
|-----------|------|---------|---------------|
| `id_empresa` | `int` | *required* | `idEmpresa` |
| `num_dias_sync` | `int` | `99` | `numDiasSync` |
| `ds_separacao` | `str` | `""` | `dsSeparacao` |
| `ds_precarga` | `str` | `""` | `dsPreCarga` |
| `ds_ponto_retirada` | `str` | `""` | `dsPontoRetirada` |
| `dt_inicio` | `str` | `""` | `dtInicio` |
| `ds_pre_separacao` | `str` | `""` | `dsPreSeparacao` |
| `dt_fim` | `str` | `""` | `dtFim` |

Returns `[]` on HTTP `204`, otherwise one `PreCargaSeparacao` per element of the JSON array.

Date filters (`dt_inicio` / `dt_fim`) are typed `str` and forwarded verbatim — the library does not format `date` objects for you.

> A large `num_dias_sync` can make this endpoint hang; the source carries a `FIXME` about it. Prefer a bounded window (e.g. `num_dias_sync=30`).

#### `items(*, id_empresa, id_precarga, id_usuario, ...) -> list[PreCargaSeparacaoItem]`

`GET /ciss-control/precarga_separacao/itens`

| Parameter | Type | Default | API query key |
|-----------|------|---------|---------------|
| `id_empresa` | `int` | *required* | `idEmpresa` |
| `id_precarga` | `int` | *required* | `idPreCarga` |
| `id_usuario` | `int` | *required* | `idUsuario` |
| `ds_separacao` | `str` | `""` | `dsSeparacao` |
| `ds_precarga` | `str` | `""` | `dsPreCarga` |
| `ds_ponto_retirada` | `str` | `""` | `dsPontoRetirada` |
| `dt_inicio` | `str` | `""` | `dtInicio` |
| `ds_pre_separacao` | `str` | `""` | `dsPreSeparacao` |
| `dt_fim` | `str` | `""` | `dtFim` |

Unlike `get_all`, this method has no `204` guard — an empty response body raises `requests.exceptions.JSONDecodeError` rather than returning `[]`.

#### `submit(items) -> requests.Response`

`POST /ciss-control/precarga_separacao`

Takes a positional `list[PreCargaSeparacaoSubmission]` and posts `[item.to_dict() for item in items]` as a JSON array. Returns the raw `requests.Response` — the body is not parsed, so inspect `response.status_code` / `response.json()` yourself.

---

## Data models

<<<<<<< HEAD
Models live in `libciss.services.cisscontrol.models` and use snake_case in Python, mapped to the API’s camelCase.
=======
Models live in `libciss.services.cisscontrol.models`. They are `@dataclass(slots=True)`, use snake_case in Python, and map to the API's camelCase. Response models expose `from_dict`; the request model exposes `to_dict`.

Being slotted dataclasses, they take positional or keyword arguments, compare by value, and reject attributes that are not declared fields.
>>>>>>> 135ef23cd6ec3d4a8f50ff4eda7518ff653b2de5

### `LoginResponse`

Built by `auth.login()`.

| Field | Type | API field |
|-------|------|-----------|
| `token_bearer` | `str` | `tokenBearer` |
| `id_user` | `int` | `idUser` |
| `id_grupo` | `int` | `idGrupo` |
| `username` | `str` | `username` |
| `enabled` | `bool` | `enabled` |
| `id_empresa` | `int` | `idEmpresa` |
| `cnpj_empresa` | `str` | `cnpjEmpresa` |
| `razao_social` | `str` | `razaoSocial` (right-stripped) |
| `permissions` | `list[str]` | `permissionsList` |

### `PreCargaSeparacao`

| Field | Type | API field |
|-------|------|-----------|
| `id_empresa` | `int` | `idEmpresa` |
| `id_pre_carga` | `int` | `idPreCarga` |
| `descricao` | `str` | `descricao` |
| `dt_movimento` | `date` | `dtMovimento`, parsed with `date.fromisoformat` |

`from_dict` requires every key above; a missing one raises `KeyError`, and a `dtMovimento` that is not ISO-8601 raises `ValueError`.

### `PreCargaSeparacaoItem`

| Field | Type | API field |
|-------|------|-----------|
| `id_pre_carga` | `int` | `idPreCarga` |
| `id_empresa` | `int` | `idEmpresa` |
| `id_documento` | `int` | `idDocumento` |
| `id_produto` | `int` | `idProduto` |
| `id_subproduto` | `int` | `idSubproduto` |
| `ds_local_retirada` | `str` | `dsLocalRetirada` |
| `num_sequencia` | `int` | `numSequencia` |
| `id_lote` | `str \| None` | `idLote` — optional, `None` when absent |
| `qtd_produto` | `float` | `qtdProduto` |
| `fg_separado` | `str` | `fgSeparado` |
| `qtd_contada_divergente` | `bool` | `qtdContadaDivergente` |
| `id_local_retirada` | `int` | `idLocalRetirada` |

`id_lote` is the only tolerated-missing key; all others raise `KeyError` when absent.

### `PreCargaSeparacaoSubmission`

Request DTO for `submit()`. Every field is required — there are no defaults, so all thirteen must be supplied.

| Field | Type | JSON key |
|-------|------|----------|
| `id_usuario` | `int` | `idUsuario` |
| `id_empresa` | `int` | `idEmpresa` |
| `id_documento` | `int` | `idDocumento` |
| `id_pre_carga` | `int` | `idPreCarga` |
| `id_produto` | `int` | `idProduto` |
| `id_subproduto` | `int` | `idSubproduto` |
| `id_local_retirada` | `int` | `idLocalRetirada` |
| `ds_local_retirada` | `str` | `dsLocalRetirada` |
| `num_sequencia` | `int` | `numSequencia` |
| `qtd_produto` | `float` | `qtdProduto` |
| `fg_separado` | `str` | `fgSeparado` |
| `qtd_contada` | `float` | `qtdContada` |
| `dt_hr_inicio` | `datetime.datetime` | `dtHrInicio`, serialized with `.isoformat()` |

`to_dict()` calls `.isoformat()` on `dt_hr_inicio`, so it must be a `datetime` — passing a string raises `AttributeError`. Nothing is validated beyond that: `fg_separado` is sent as-is (`"S"` / `"N"`), and a `qtd_contada` that differs from `qtd_produto` is accepted and resolved server-side.

---

## Exceptions

```text
Exception
└── CISSClientError
    └── AuthenticationError
        └── NotAuthenticatedError
```

Defined in `libciss.exceptions`.

| Exception | Raised when |
| --------- | ----------- |
| `CISSClientError` | Base class for library errors — catch this to catch them all |
| `AuthenticationError` | Auth-related failures |
| `NotAuthenticatedError` | A `@requires_login` method is called with no token set |

Transport and HTTP failures are **not** wrapped: `client.request()` calls `raise_for_status()`, so 4xx/5xx surface as `requests.HTTPError`, and connection/timeout problems surface as the corresponding `requests.RequestException` subclasses.

```python
import requests

from libciss.exceptions import NotAuthenticatedError

try:
    preloads = client.cisscontrol.precarga_separacao.get_all(id_empresa=1)
except NotAuthenticatedError:
    client.cisscontrol.auth.login("user", "pass")
except requests.HTTPError as e:
    print(e.response.status_code, e.response.text)
```

A 401 from an expired token arrives as `requests.HTTPError`, not `NotAuthenticatedError` — the local check only sees that a token exists. To recover, call `login()` again.

---

## Endpoint map

| HTTP | Path | Client method |
|------|------|---------------|
| `POST` | `/ciss-control/login` | `auth.login` |
| `GET` | `/ciss-control/precarga_separacao` | `precarga_separacao.get_all` |
| `GET` | `/ciss-control/precarga_separacao/itens` | `precarga_separacao.items` |
| `POST` | `/ciss-control/precarga_separacao` | `precarga_separacao.submit` |

---

## Known issues

These are current limitations of the code, not of the documentation.

| Issue | Impact |
| ----- | ------ |
| `get_all` may hang on wide sync windows (`FIXME` in [precarga_separacao.py:50](libciss/services/cisscontrol/precarga_separacao.py#L50)) | Keep `num_dias_sync` small |
| `items` does not handle HTTP `204` | Empty result raises `JSONDecodeError` instead of returning `[]` |
| `logout()` is client-side only | The token remains valid on the server |
| No timeouts are set on any request | A stalled server can block indefinitely |
| No test suite | Changes are unverified |

---

## Glossary

Portuguese terms used by the CISS API, kept untranslated in the model field names.

| Term | Meaning |
|------|---------|
| **Empresa** | Company / store (`id_empresa`, CNPJ, razão social) |
| **Pré-carga** | Pre-load batch for warehouse picking |
| **Separação** | Product picking / separation |
| **Local de retirada** | Pick-up location |
| **Documento** | Related document ID |
| **Produto / Subproduto** | Product and subproduct IDs |
| **Lote** | Lot / batch code (optional) |
| **Qtd produto** | Expected quantity on the pre-load |
| **Qtd contada** | Quantity actually counted during separation |
| **fgSeparado** | Separation flag, an API string (`"S"` / `"N"`) |
| **numDiasSync** | Days of history to sync (default `99`) |

---

## License

MIT © Redson
