Metadata-Version: 2.1
Name: microspec-py
Version: 0.1.1
Summary: microspec: micro protocol parser and data validator from a simple JSON spec
Home-page: https://gitlab.com/meehai/microspec
License: MIT
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE.TXT
Requires-Dist: numpy>=2.2.0
Requires-Dist: loggez>=0.8
Provides-Extra: dev
Requires-Dist: pytest>=8.4; extra == "dev"

# microspec

A micro specification parser and data validator for TCP protocols. The protocol can be defined either inside the code or as a standalone JSON which can be loaded via `Protocol.from_dict(...)`. Then, all the data payloads for all the defined endpoints are simply validated via `protocol.validate_endpoint(endpoint, payload) -> ValidationError | None`.

Usage:

- Via pip: `pip install microspec-py` (the PyPI name is `microspec-py`; the import name is `microspec`)
- From source code:
```bash
git clone https://gitlab.com/meehai/microspec                 # clone the source code
cd microspec                                                  # go in the cloned directory
python -m venv .venv && source .venv/bin/activate             # make a virtual env, optional but useful
python -m pip install -e .                                    # install microspec in this virtual env
python -m pytest test/                                        # run the unit & integration tests to verify installation
python microspec/microspec.py test/integration/protocol.json  # smoke run: parse + validate the bundled spec
```

Docs: [meehai.gitlab.io/microspec](https://meehai.gitlab.io/microspec/) — built by
[`docs/build_docs.sh`](docs/build_docs.sh) (pdoc; no sphinx/config). Build locally with
`bash docs/build_docs.sh` and open the printed `file://` link.

## Usage

Protocol:
```json
{
  "move": {
    "input":  {"control_input": {"dtype": "float32", "shape": [6], "range": [-100, 100]}},
    "output": {"status": {"dtype": "str_enum", "enum": ["move_applied"]}, "new_state": {"dtype": "dict"}}
  }
}
```

```python
import json
from microspec import Protocol

# Can also be defined here manually via the `Endpoint`, `Field` classes and `Dtype` enum from the library.
protocol = Protocol.from_dict(json.load(open("test/integration/protocol.json")), n_max_robots=10)

err = protocol.validate_endpoint("move", {"control_input": [5, 5, 5, 3, 3, 3]})
if err is not None: # err is of type ValidationError (has .error, .endpoint, .field for context)
  raise ValueError(f"payload is not valid: {err.endpoint}: {err.error}")
```

## Spec format

Each command is an `input` / `output` map of `name -> field`. Errors are not per-command: every
endpoint shares one error shape, declared once as `Protocol`'s `error_field` (default `Field("error",
Dtype.STR)`).

```json
{
  "move": {
    "input":  {"control_input": {"dtype": "float32", "shape": [6], "range": [-100, 100]}},
    "output": {"status": {"dtype": "str_enum", "enum": ["move_applied"]}, "new_state": {"dtype": "dict"}}
  },
  "robot_get_state": {
    "input":  {"robot_ix": {"dtype": "int32", "range": [0, "${n_max_robots}"]}},
    "output": {"robot": {"dtype": "dict"}}
  }
}
```

## Field schema

| key       | applies to            | meaning                                                        |
|-----------|-----------------------|----------------------------------------------------------------|
| `dtype`   | required              | `str` `int32` `float32` `bool` `dict` `bytes` `str_enum` `int_enum` |
| `shape`   | array dtypes          | e.g. `[6]`; `null` = free first axis (`[null, 6]`)             |
| `range`   | `int32` / `float32`   | `[min, max]`, inclusive (NaN always rejected; ±Inf only if outside the range) |
| `enum`    | `str_enum` / `int_enum` | non-empty list of allowed values (required for enum dtypes)  |
| `min_len` | arrays with free axis | minimum length of the `null` axis                              |
| `fields`  | `dict`                | optional nested `name -> field` map; omit for an opaque dict   |

Notes: a `shape` key means "array" (numpy, exact dtype — `np.float32`, not `int64`); scalars have no
`shape`. A `dict` with `fields` is validated recursively (keys must match exactly, nested arrays are
list→ndarray converted); without `fields` it is opaque (any dict passes). `${var}` is a single bare
variable filled at parse time (e.g. `n_max_robots`). The spec file is plain **JSON**.

## Public API

`Protocol`, `Endpoint`, `Field`, `Dtype`, `ValidationError`. Everything else is internal.
