Metadata-Version: 2.4
Name: jolt-python
Version: 0.1.4
Summary: Python re-implementation of Bazaarvoice Jolt (JSON-to-JSON transformation)
License: Apache-2.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE-jolt-upstream
License-File: NOTICE
Dynamic: license-file

# joltpy

A JSON-to-JSON transformation library for Python. `Chainr` runs an ordered
list of declarative operations (`shift`, `default`, `remove`, `sort`,
`modify-*`) against a JSON document, driven entirely by a JSON spec - no
code needed to describe the transformation itself.

## Status

| Operation | Status | Test coverage |
|---|---|---|
| `remove` (Removr) | Implemented | 11/11 fixtures pass |
| `default` (Defaultr) | Implemented | 13/13 fixtures pass |
| `sort` (Sortr) | Implemented | 2/2 fixtures pass |
| `shift` (Shiftr) | Implemented | 62/62 fixtures pass (literal/`*`/`&`/`@`/`$`/`#`/`[]`/transpose, all of it) |
| `modify-overwrite-beta` / `-default-beta` / `-define-beta` (Modifier) | Implemented | 87/87 fixtures pass (full stock function registry: Strings/Math/Objects/Lists, `^`/`@` context+self lookups, custom function registration) |
| Chainr (operation runner) | Implemented for all of the above | Partial `(from, to)` execution API and class-loading-based custom operations not supported |

Total: **177/177 tests passing.**

## Project layout

```
joltpy/
  chainr.py              # Chainr: runs a list of {"operation", "spec"} steps in order
  sortr.py               # Sortr
  removr/__init__.py     # Removr + RemovrLeafSpec/RemovrCompositeSpec
  defaultr/__init__.py   # Defaultr + Key/MapKey/ArrayKey
  traversr.py            # generic Map/List tree walker+writer (auto-creates containers, expands arrays)
  shiftr/__init__.py     # Shiftr + ShiftrLeafSpec/ShiftrCompositeSpec/ShiftrWriter/ShiftrTraversr + ExecutionStrategy
  modifier/
    __init__.py            # OpMode/DataType + ModifierSpec/Leaf/Composite + Overwritr/Definr/Defaultr + Modifier's ExecutionStrategy
    functions.py            # Strings/Math/Objects/Lists stock function registry + dispatch helpers
  common/
    pathelement.py         # Literal/Star*/Amp/At/Dollar/Hash/Array/Transpose path elements (LHS match + RHS evaluate)
    reference.py            # &/$/# sugar-syntax parsing (&, &1, &(1,2), ...)
    spec_string.py           # dot-notation RHS parsing, LHS key -> PathElement factory, function-arg parsing
    path_eval.py              # PathEvaluatingTraversal base + TransposeReader + prepend_root() (shared root-wrapping trick)
    tree.py                    # WalkedPath/PathStep/MatchedElement - the parallel-tree-walk bookkeeping
    optional.py                 # ABSENT sentinel (3-state optional: absent vs present-null vs present-value)
tests/
  fixtures/              # JSON fixtures for shiftr/modifier/chainr/removr/defaultr/sortr/cardinality
  _fixture_utils.py       # loads {input, spec, expected} JSON5-ish fixtures (strips // comments)
  test_removr.py, test_defaultr.py, test_sortr.py, test_chainr.py, test_shiftr.py, test_modifier.py
```

`tests/fixtures/cardinality` is included but unused - Cardinality (`cardinality`
Chainr operation) was never needed by any real spec seen so far, so it's
deliberately not implemented.

## Installation

```bash
pip install jolt-python
```

## Usage

The package installs as `jolt-python` but imports as `joltpy`. The main entry
point is `Chainr`, which runs an ordered list of `{"operation", "spec"}` steps
against an input document:

```python
from joltpy import Chainr

chainr_spec = [
    {
        "operation": "shift",
        "spec": {
            "name": "customer.name",
            "email": "customer.email",
        },
    }
]

chainr = Chainr.from_spec(chainr_spec)
result = chainr.transform({"name": "Ada", "email": "ada@example.com"})
# {"customer": {"name": "Ada", "email": "ada@example.com"}}
```

Errors raise `joltpy.exceptions.JoltpyException` (or its subclasses,
`SpecException` for a malformed spec and `TransformException` for a runtime
failure) instead of failing silently:

```python
from joltpy import Chainr
from joltpy.exceptions import JoltpyException

try:
    result = chainr.transform(input_doc)
except JoltpyException:
    logger.exception("Jolt transform failed")
    raise
```

A `Chainr` built from a spec is immutable and safe to reuse across many
`transform()` calls - build it once (e.g. at app startup, from a spec file
bundled with your service) rather than per-request:

```python
import json
from pathlib import Path
from joltpy import Chainr

with open(Path("resources/jolt_spec.json"), encoding="utf-8") as f:
    chainr_spec = json.load(f)

chainr = Chainr.from_spec(chainr_spec)  # build once, reuse
```

## Running tests

```bash
python -m venv venv
venv\Scripts\activate      # Windows
pip install -e . pytest
pytest -q
```

## License

Apache 2.0 (see `LICENSE` and `NOTICE`).

## Notable implementation details

- Every RHS output path (Shiftr) and every `@`/`^` argument path (Modifier)
  is internally evaluated against a document silently wrapped in a `"root"`
  key, then unwrapped via `output.get("root")` / `{"root": context}` at the
  end. This is what makes a blank RHS (`""`) work as a pure identity write,
  and what makes `^context.path` lookups work at all.
- Traversal reads must preserve "not found" as distinct from "found, value
  is null" (a 3-state Optional, represented here as an `ABSENT` sentinel) -
  collapsing it to `None` breaks the transpose path element's "bail out
  cleanly if the lookup target doesn't exist" behavior.
- Literal path elements must implement `evaluate()` (return their own raw
  key) since RHS dot-paths are made of literal segments too, not just
  `&`/`$`.
- Modifier's composite-spec element application must unwrap `ABSENT` to
  `None` at entry (same reason as above) - otherwise a missing top-level key
  in the input is never created, and the whole branch silently no-ops
  instead of building the missing container.
- Modifier's `ALL_LITERALS` execution strategy (unlike Shiftr's
  `AVAILABLE_LITERALS`) must call `.apply()` for **every** literal child even
  when the key/index doesn't exist in the input yet - that's what lets it
  create map keys / expand arrays beyond current bounds. Skipping absent
  keys (like Shiftr does) breaks all of Modifier's "fill in defaults" use
  cases.

## Extensibility

`Overwritr`/`Defaultr`/`Definr` all accept an optional second constructor
arg, a `functions_map` dict (`name -> callable(*args) -> ABSENT | value`),
to register custom functions on top of (or instead of) `STOCK_FUNCTIONS`.
