odcs 0.9.1

Reference implementation of the Open Data Contract Standard (ODCS)
Documentation
# Python API

The `pyodcs` package wraps the Rust implementation via PyO3. All parsing and validation semantics match the `odcs` crate.

## Installation

```bash
pip install pyodcs
```

## Module overview

```python
import pyodcs

pyodcs.__version__                # package version
pyodcs.UPSTREAM_SPEC_VERSION      # "3.1.0"
pyodcs.UPSTREAM_REPOSITORY_URL    # upstream ODCS GitHub URL
pyodcs.CODES                      # dict of diagnostic code constants
pyodcs.VALIDATION_PHASES          # dict of validation phase name constants (since 0.6.0)
```

### `CODES`

Maps short names to stable `odcs:*` identifiers:

```python
pyodcs.CODES["INVALID_KIND"]  # "odcs:invalid-kind"
```

See [diagnostics.md](diagnostics.md) for when each code fires.

### `VALIDATION_PHASES`

Maps short names to `validationPhase` JSON values (metadata, not error codes):

```python
pyodcs.VALIDATION_PHASES["DOCUMENT"]     # "document"
pyodcs.VALIDATION_PHASES["JSON_SCHEMA"]  # "jsonSchema"
```

Validation reports include `validationPhase` on each validation-stage diagnostic. Parse-stage diagnostics omit the field.

For choosing between `parse_and_validate`, `parse`, and `validate_result`, see [API decision guide](api-guide.md).

## Recommended quick start

```python
import pyodcs

report = pyodcs.parse_and_validate(open("contract.yaml", "rb").read(), format="yaml")
assert pyodcs.is_valid(report)
```

## Data shapes

### Parse result (`parse()`, `parse_file()`)

```python
{
    "contract": {...} | None,   # parsed contract dict when parse succeeded
    "report": {
        "diagnostics": [...]
    }
}
```

### Validation report (`validate()`, `parse_and_validate()`, `validate_result()`)

```python
{
    "diagnostics": [
        {
            "id": "odcs:missing-required-field",
            "severity": "error",
            "stage": "validation",
            "category": "structure",
            "message": "...",
            "object_ref": "id",
            "remediation": None
        }
    ]
}
```

`validate_result()` may add internal cache keys (`_odcs_validated`) — do not rely on them in application code.

A report is valid when `is_valid(report)` is `True` (no `error`-severity diagnostics). `is_valid()` also accepts a parse result dict and reads diagnostics from `report`.

## Parsing

### `parse(content, format="yaml")`

Parse a document from text or bytes. Returns a dict with `contract` and `report` keys.

```python
result = pyodcs.parse(open("contract.yaml", "rb").read(), format="yaml")
contract = result["contract"]   # dict or None
report = result["report"]       # {"diagnostics": [...]}
```

`format` may be `"yaml"`, `"yml"`, or `"json"`.

### `parse_file(path)`

Parse from a file path. Raises `FileNotFoundError` when the file is missing. Raises `ValueError` for unsupported file extensions.

```python
result = pyodcs.parse_file("examples/minimal.odcs.yaml")
```

## Validation

### `validate(contract)`

Validate a parsed contract dict. Returns `{"diagnostics": [...]}`. Validation includes pinned ODCS v3.1.0 JSON Schema checks.

```python
validation = pyodcs.validate(contract)
```

### `validate_result(result)`

Merge parse-time and validation diagnostics from a `parse()` / `parse_file()` result.

```python
report = pyodcs.validate_result(result)
```

### `parse_and_validate(content, format="yaml")`

Parse and validate in one step. Returns `{"diagnostics": [...]}`.

```python
report = pyodcs.parse_and_validate(content, format="yaml")
```

### `parse_and_validate_paths(primary, deps=None, *, includes=None, registry=None)`

Parse and validate a primary contract with optional dependency paths and registry root (since 0.9.0).

```python
report = pyodcs.parse_and_validate_paths(
    "consumer.yaml",
    registry="./contracts/",
)
```

### Registry helpers (0.9.0+)

```python
indexed = pyodcs.registry_index_and_save("./contracts/")
entry = pyodcs.registry_lookup("./contracts/", "provider-contract")
entries = pyodcs.registry_list("./contracts/")
```

`registry_index` builds an in-memory index without writing to disk. `registry_load` reads an existing index file.

Each registry helper returns JSON-compatible dicts. Index operations return:

```python
{
    "entries": [
        {
            "id": "provider-contract",
            "version": "1.0.0",
            "path": "contracts/provider.yaml",
            "apiVersion": "v3.1.0",
            "tags": [],
            "contentHash": "...",
            "indexedAt": "...",
        }
    ],
    "report": {"diagnostics": [...]}
}
```

`registry_lookup` returns an entry dict or `None`. `registry_list` returns a list of entry dicts.

## Compatibility diff

Compare two **parsed** contract dicts:

```python
old = pyodcs.parse_file("old.yaml")["contract"]
new = pyodcs.parse_file("new.yaml")["contract"]
report = pyodcs.diff(old, new)
```

### Report shape

```python
{
    "hasBreaking": True,
    "changes": [
        {
            "kind": "breaking",
            "code": "odcs:compatibility-breaking",
            "message": "removed property 'email'",
            "path": "schema[customers].properties[email]",
        }
    ],
}
```

| Field | Description |
|-------|-------------|
| `hasBreaking` | `True` when any change has `kind: "breaking"` |
| `changes` | List of changes with `kind`, `code`, `message`, `path` |

See [Compatibility analysis](compatibility.md).

### `pinned_schema(*, json_metadata=False)`

Return the pinned ODCS v3.1.0 JSON Schema dict.

```python
schema = pyodcs.pinned_schema()
metadata = pyodcs.pinned_schema(json_metadata=True)
```

### `is_valid(report)`

Returns `True` when no error-level diagnostics are present.

```python
assert pyodcs.is_valid(report)
```

## Inspection

### `inspect(contract)`

Human-readable summary string.

```python
print(pyodcs.inspect(contract))
```

### `inspect_summary(contract)`

Structured summary dict (same fields as `odcs inspect --json`).

```python
summary = pyodcs.inspect_summary(contract)
# {"id", "name", "version", "apiVersion", "kind", "status",
#  "schemaCount", "qualityCount"}
```

### `quality_rules_count(contract)`

Count nested quality rules across all schema objects and properties.

```python
count = pyodcs.quality_rules_count(contract)
```

## CLI

The `pyodcs` console script mirrors the Rust `odcs` CLI:

```bash
pyodcs validate examples/minimal.odcs.yaml
pyodcs validate consumer.yaml --dep provider.yaml
pyodcs validate consumer.yaml --registry ./contracts/
pyodcs inspect examples/minimal.odcs.yaml --json
pyodcs diagnostics examples/minimal.odcs.yaml
pyodcs diff old.yaml new.yaml
pyodcs registry index ./contracts/
pyodcs registry lookup ./contracts/ provider-contract
pyodcs schema
pyodcs schema --json
pyodcs schema --url-only
pyodcs version
pyodcs version --json
```

Exit codes match the Rust CLI: `0` valid, `1` validation error, `2` parse/I/O failure.

## Error handling pattern

```python
import pyodcs
import sys

try:
    result = pyodcs.parse_file("contract.yaml")
except (FileNotFoundError, OSError, ValueError) as e:
    print(e, file=sys.stderr)
    sys.exit(2)

report = pyodcs.validate_result(result)
if not pyodcs.is_valid(report):
    for d in report["diagnostics"]:
        print(f"{d['id']}: {d['message']}")
    sys.exit(1)
```

## Rust parity

| Python | Rust |
|--------|------|
| `parse()` | `parse()` |
| `parse_file()` | `parse_file()` |
| `validate()` | `validate()` |
| `parse_and_validate()` | `parse_and_validate()` |
| `diagnostic_codes()` / `CODES` | Diagnostic code constants |
| `validation_phases()` / `VALIDATION_PHASES` | Validation phase name constants (since 0.6.0) |
| `parse_and_validate_paths()` | `parse_and_validate_set_with_registry()` |
| `diff()` | `diff()` |
| `registry_*()` | `index_registry`, `load_registry`, `Registry` |
| `pinned_schema()` | `odcs schema` |

See also [cli.md](cli.md) and [diagnostics.md](diagnostics.md).