# 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]",
}
],
}
```
| `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
| `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).