Metadata-Version: 2.4
Name: ouroboros_json
Version: 1.0.0
Summary: Simple, safe and dependency-free JSON utility library for automation frameworks and Python applications.
Author: Flavio Brandolini
License: Copyright (c) 2026 Flavio Brandolini
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
Keywords: json,utility,toolkit,pathlib,automation,serialization,ouroboros
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: License :: OSI Approved :: MIT License
Classifier: Typing :: Typed
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Provides-Extra: dev
Requires-Dist: ouroboros_json[test]; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Dynamic: license-file

# ouroboros_json

Simple, safe and dependency-free JSON utility library for Python.

`ouroboros_json` provides a thin, well-typed wrapper around Python's standard `json` module with pragmatic helpers for automation frameworks, test suites and small applications.

---

## Features

| Area | Capabilities |
|------|-------------|
| **I/O** | Load, write and safe-default reads with automatic parent-directory creation |
| **Key / Value** | Add, extract, check and remove top-level keys from JSON files |
| **Nested Access** | Dot-notation get, set, has and delete on arbitrarily deep dicts |
| **Transform** | Deep merge, flatten / unflatten, filter / exclude, structural diff, recursive key sort |
| **Comparison** | Structural *subset* comparison between JSON objects |
| **Serialization** | String encode / decode, validity check, pretty-print |
| **Matrix** | 2-D matrix read & write helpers |
| **Introspection** | List keys (flat or nested), count keys, find values by predicate |
| **Typed** | PEP 561 `py.typed` marker, `JSONType` and `PathLike` aliases |
| **Zero deps** | Only Python stdlib |

---

## Installation

```bash
pip install ouroboros_json
```

---

## Python Compatibility

Supported: **Python 3.9 - 3.14**

---

## Architecture

```
ouroboros_json/
+-- __init__.py          # Public API exports + __version__
+-- _types.py            # JSONType, PathLike aliases + internal helpers
+-- _io.py               # IOMixin: load_json, write_json, load_or_default
+-- _keyvalue.py         # KeyValueMixin: add, extract, check, remove keys
+-- _nested.py           # NestedMixin: get/set/has/delete via dot-notation
+-- _transform.py        # TransformMixin: merge, flatten, compare, diff, sort, find
+-- _serialization.py    # SerializationMixin: to/from string, matrix, pretty-print
+-- base.py              # BaseJSON facade (composes all mixins)
+-- exceptions.py        # Full exception hierarchy
+-- py.typed             # PEP 561 typed marker
tests/
+-- conftest.py          # Shared fixtures
+-- test_io.py           # I/O tests
+-- test_keyvalue.py     # Key/value tests
+-- test_nested.py       # Nested access tests
+-- test_transform.py    # Transform tests
+-- test_serialization.py # Serialization tests
+-- test_base.py         # Facade / integration tests
```

---

## Quick Start

```python
from ouroboros_json import BaseJSON

# Load / write files
data = BaseJSON.load_json("config.json")
BaseJSON.write_json("output.json", {"status": "ok"})
cfg = BaseJSON.load_or_default("optional.json", default={})

# Add or update a key in a file
BaseJSON.add_value_to_json("./tmp/data.json", "env", "dev")

# Extract a value
branch = BaseJSON.extract_value_from_json("./tmp/data.json", "branch")

# Check if a value exists
has_token = BaseJSON.check_value_in_json("./tmp/data.json", value="abc123")
```

---

## Nested Access (dot-notation)

```python
cfg = {"db": {"host": "localhost", "port": 5432}}

# Get
host = BaseJSON.get_nested(cfg, "db.host")        # "localhost"
name = BaseJSON.get_nested(cfg, "db.name", None)   # None (default)

# Set (creates intermediates automatically)
d: dict = {}
BaseJSON.set_nested(d, "db.host", "localhost")
# d == {"db": {"host": "localhost"}}

# Check existence
BaseJSON.has_nested(cfg, "db.host")   # True
BaseJSON.has_nested(cfg, "db.name")   # False

# Delete
d = {"a": {"b": 1, "c": 2}}
BaseJSON.delete_nested(d, "a.b")
# d == {"a": {"c": 2}}

# Custom separator
BaseJSON.get_nested(cfg, "db/host", separator="/")
```

---

## Deep Merge

```python
base = {"db": {"host": "localhost", "port": 5432}}
over = {"db": {"port": 3306}, "debug": True}
merged = BaseJSON.deep_merge(base, over)
# {"db": {"host": "localhost", "port": 3306}, "debug": True}
```

---

## Flatten / Unflatten

```python
flat = BaseJSON.flatten({"a": {"b": 1, "c": {"d": 2}}})
# {"a.b": 1, "a.c.d": 2}

nested = BaseJSON.unflatten({"a.b": 1, "a.c.d": 2})
# {"a": {"b": 1, "c": {"d": 2}}}
```

---

## Diff

```python
a = {"x": 1, "y": 2, "z": 3}
b = {"x": 1, "y": 99, "w": 4}
result = BaseJSON.diff(a, b)
# {"added": ["w"], "removed": ["z"], "changed": ["y"]}
```

---

## Filter / Exclude / Remove

```python
BaseJSON.filter_keys({"a": 1, "b": 2, "c": 3}, ["a", "c"])   # {"a": 1, "c": 3}
BaseJSON.exclude_keys({"a": 1, "b": 2, "c": 3}, ["b"])        # {"a": 1, "c": 3}
BaseJSON.remove_key({"a": 1, "b": 2}, "b")                     # {"a": 1}
BaseJSON.remove_keys({"a": 1, "b": 2, "c": 3}, ["b", "c"])     # {"a": 1}
```

---

## Sort Keys Recursively

```python
BaseJSON.sort_keys_deep({"b": 2, "a": {"d": 4, "c": 3}})
# {"a": {"c": 3, "d": 4}, "b": 2}
```

---

## Find Values

```python
d = {"a": 1, "b": {"c": "hello", "d": 42}}
BaseJSON.find_values(d, lambda v: isinstance(v, int))
# {"a": 1, "b.d": 42}
```

---

## Structural Subset Comparison

```python
json1 = {"a": 1, "b": {"c": 2, "d": [1, 2, 3]}}
json2 = {"b": {"d": [2]}}
assert BaseJSON.are_json_equal(json1, json2)  # True (json2 is a subset of json1)
```

---

## Serialization Helpers

```python
s = BaseJSON.to_string({"ok": True})            # '{"ok": true}'
obj = BaseJSON.from_string('{"ok": true}')      # {"ok": True}

# Validity check
BaseJSON.is_valid_json('{"a": 1}')  # True
BaseJSON.is_valid_json('{bad}')     # False

# Pretty-print
print(BaseJSON.pretty_print({"b": 2, "a": 1}, sort_keys=True))
```

---

## Introspection

```python
d = {"a": 1, "b": {"c": 2, "d": 3}}

BaseJSON.list_keys(d)                # ["a", "b"]
BaseJSON.list_keys(d, nested=True)   # ["a", "b.c", "b.d"]

BaseJSON.count_keys(d)               # 3 (leaf keys)
BaseJSON.count_keys(d, nested=False) # 2 (top-level)
```

---

## Matrix Helpers

```python
matrix = [[1, 2], [3, 4]]
BaseJSON.save_matrix_to_json(matrix, "./tmp/matrix.json")
loaded = BaseJSON.load_matrix_from_json("./tmp/matrix.json")
assert loaded == matrix
```

---

## Exception Hierarchy

```
JSONError
+-- JSONFileNotFoundError
+-- JSONDecodeError
+-- JSONSerializationError
+-- JSONKeyError
+-- JSONPathError
+-- JSONMergeError
+-- JSONValidationError
```

All exceptions inherit from `JSONError` for convenient broad catches.

---

## API Reference

### `BaseJSON` - I/O

| Method | Description |
|--------|-------------|
| `load_json(path)` | Read and parse a JSON file |
| `load_or_default(path, default=None)` | Load JSON or return default if file missing |
| `write_json(path, data, *, indent=4, ensure_ascii=False)` | Write data to a JSON file (creates parents) |

### `BaseJSON` - Key / Value

| Method | Description |
|--------|-------------|
| `add_value_to_json(file_path, attribute, value)` | Add or update a top-level key in a JSON file |
| `extract_value_from_json(file_path, attribute)` | Get the value of a top-level key |
| `check_value_in_json(file_path, value)` | Check if a value exists among JSON values |
| `remove_key(data, key)` | Return a copy without *key* |
| `remove_keys(data, keys)` | Return a copy without the specified *keys* |

### `BaseJSON` - Nested Access

| Method | Description |
|--------|-------------|
| `get_nested(data, path, default=SENTINEL, *, separator=".")` | Get value via dot-notation |
| `set_nested(data, path, value, *, separator=".")` | Set value via dot-notation (creates intermediates) |
| `has_nested(data, path, *, separator=".")` | Check if a dot-notation path exists |
| `delete_nested(data, path, *, separator=".")` | Delete a key at a dot-notation path |

### `BaseJSON` - Transform

| Method | Description |
|--------|-------------|
| `deep_merge(base, override)` | Recursively merge two dicts (override wins) |
| `flatten(data, *, separator=".")` | Flatten nested dict to dotted keys |
| `unflatten(data, *, separator=".")` | Reconstruct nested dict from flat keys |
| `filter_keys(data, keys)` | Keep only specified keys |
| `exclude_keys(data, keys)` | Remove specified keys |
| `are_json_equal(json1, json2)` | Structural subset comparison |
| `list_keys(data, *, nested=False, separator=".")` | List keys (top-level or all leaf paths) |
| `count_keys(data, *, nested=True)` | Count keys |
| `diff(original, modified)` | Compute added/removed/changed keys |
| `sort_keys_deep(data)` | Recursively sort all dict keys |
| `find_values(data, predicate)` | Find leaf values matching a predicate |

### `BaseJSON` - Serialization

| Method | Description |
|--------|-------------|
| `to_string(data, *, indent=None, ensure_ascii=False, sort_keys=False)` | Serialize to JSON string |
| `from_string(text)` | Parse a JSON string |
| `is_valid_json(text)` | Check if string is valid JSON |
| `pretty_print(data, *, indent=2, sort_keys=False)` | Human-readable formatted JSON |
| `save_matrix_to_json(matrix, file_path)` | Save 2-D matrix to JSON file |
| `load_matrix_from_json(file_path)` | Load 2-D matrix from JSON file |

### Type Aliases

| Name | Definition |
|------|-----------|
| `JSONType` | `Union[Dict[str, Any], List[Any], str, int, float, bool, None]` |
| `PathLike` | `Union[str, Path]` |

---

## Development

```bash
pip install -e ".[dev]"

# Run tests
python -m pytest tests/ -v

# Lint
ruff check .

# Type check
mypy ouroboros_json/
```

---

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

---

## Authors

Flavio Brandolini
