json permutation library
Project description
J-Perm
A composable JSON transformation DSL with a powerful, extensible architecture.
J-Perm lets you describe data transformations as executable specifications โ a list of steps that can be applied to input documents. It supports
JSON Pointer addressing, template interpolation with ${...} syntax, special constructs ($ref, $eval, $and, $or, $not), and a rich set of built-in operations.
Key features:
- ๐ฏ Declarative โ transformations are data, not code
- ๐ Pluggable architecture โ stages, handlers, matchers form a composable pipeline
- ๐ณ Hierarchical registries โ organize operations and value processors in trees
- ๐ฆ Self-contained โ zero dependencies on external registries
- ๐จ Shorthand syntax โ write concise scripts that expand to full operations
Quick Example
from j_perm import build_default_engine
engine = build_default_engine()
# Source data
source = {
"users": [
{"name": "Alice", "age": "17"},
{"name": "Bob", "age": "22"}
]
}
# Transformation spec (using shorthands)
spec = {
"op": "foreach",
"in": "/users",
"do": {
"/adults[]": {
"$eval": [
{"/name": "/item/name", "/age": "${int:/item/age}"},
{"op": "if", "path": "/age", "cond": "${?dest.age >= `18`}", "else": {"~delete": "/age"}}
]
}
}
}
result = engine.apply(spec, source=source, dest={})
# โ {"adults": [{"name": "Bob", "age": 22}]}
Installation
pip install j-perm
(or copy the package into your project)
Architecture Overview
J-Perm is built on a pipeline architecture with two main levels:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ spec (user input) โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ STAGES (batch preprocessing, priority order) โ โ
โ โ โข ShorthandExpansion โ expand ~delete, etc โ โ
โ โ โข YourCustomStage โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ List[step] โ
โ โ โ
โ โผ for each step: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ MIDDLEWARES (per-step, priority order) โ โ
โ โ โข Validation, logging, etc. โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ REGISTRY (hierarchical dispatch tree) โ โ
โ โ โข SetHandler, CopyHandler, ForeachHandler, ... โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โ handlers call ctx.engine.process_value(...) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ VALUE PIPELINE (stabilization loop) โ โ
โ โ โข SpecialResolveHandler ($ref, $eval) โ โ
โ โ โข TemplSubstHandler (${...}) โ โ
โ โ โข RecursiveDescentHandler (containers) โ โ
โ โ โข IdentityHandler (scalars) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Core Components
| Component | Purpose |
|---|---|
| Engine | Orchestrates pipelines, manages context, runs stabilization loop |
| Pipeline | Runs stages โ middlewares โ registry dispatch for each step |
| StageRegistry | Tree of batch preprocessors (run-all, priority order) |
| ActionTypeRegistry | Tree of action handlers (first-match or run-all) |
| ValueResolver | Abstraction for addressing (JSON Pointer implementation) |
Core API
Building an Engine
from j_perm import build_default_engine
# Default engine with all built-ins
engine = build_default_engine()
# Custom specials (None = use defaults: $ref, $eval, $and, $or, $not)
engine = build_default_engine(
specials={"$ref": my_ref_handler, "$custom": my_handler},
casters={"int": lambda x: int(x), "json": lambda x: json.loads(x)},
jmes_options=jmespath.Options(custom_functions=CustomFunctions())
)
Applying Transformations
result = engine.apply(
spec, # DSL script (dict or list)
source=source, # Source context (for pointers, templates)
dest=dest, # Initial destination (default: {})
)
Returns: Deep copy of the final dest after all transformations.
Features
1. JSON Pointer Addressing
J-Perm uses RFC 6901 JSON Pointer with extensions:
from j_perm import PointerResolver
resolver = PointerResolver()
# Basic pointers
resolver.get("/users/0/name", data) # โ "Alice"
# Root references (work on scalars too!)
resolver.get(".", 42) # โ 42
resolver.get("/", "text") # โ "text"
# Parent navigation
resolver.get("/a/b/../c", data) # โ data["a"]["c"]
# List slices
resolver.get("/items[1:3]", data) # โ [item1, item2]
# Append notation
resolver.set("/items/-", data, "new") # Append to list
Key feature: Unlike standard JSON Pointer, PointerResolver works on any type (scalars, lists, dicts) for root references.
2. Template Interpolation (${...})
Templates are resolved by TemplSubstHandler in the value pipeline.
JSON Pointer lookup
"${/user/name}" # โ Resolve pointer from source
Type casters (built-in)
"${int:/age}" # โ int(value)
"${float:/price}"
"${bool:/flag}" # โ bool(int(value)) if int/str, else bool(value)
"${str:/id}"
JMESPath queries
"${?source.items[?price > `10`].name}" # โ Query source with JMESPath
"${?dest.total}" # โ Query destination
"${?add(dest.x, source.y)}" # โ Mix source and dest
Built-in JMESPath functions: add(a, b), subtract(a, b)
Data structure: JMESPath expressions use explicit namespaces:
source.*โ access source documentdest.*โ access destination document
Nested templates
"${${/path_to_field}}" # โ Resolve inner template first
Escaping
$${ โ ${ (literal)
$$ โ $ (literal)
3. Special Constructs
Special values are resolved by SpecialResolveHandler.
$ref โ Reference resolution
{
"$ref": "/path/to/value",
"$default": "fallback"
}
- Resolves pointer from source context
- Returns deep copy (no aliasing)
- Supports
$defaultfallback
$eval โ Nested evaluation
{
"$eval": [
{
"op": "set",
"path": "/x",
"value": 1
}
],
"$select": "/x"
}
- Executes nested DSL with
dest={} - Optionally selects sub-path from result
$and โ Logical AND with short-circuit
{
"$and": [
[{"op": "assert", "path": "/x", "return": true}],
[{"op": "copy", "from": "/y", "path": "/"}]
]
}
- Executes actions in order with empty
destfor each - Returns last result if all are truthy
- Short-circuits and returns first falsy result
$or โ Logical OR with short-circuit
{
"$or": [
[{"op": "assert", "path": "/x", "return": true}],
[{"op": "copy", "from": "/y", "path": "/"}]
]
}
- Executes actions in order with empty
destfor each - Returns first truthy result
- Returns last result if all are falsy
$not โ Logical negation
{
"$not": [{"op": "assert", "path": "/missing", "return": true}]
}
- Executes action with empty
dest - Returns logical negation of the result
4. Shorthand Syntax
Shorthands are expanded by priority-ordered StageProcessors before execution.
~assert / ~assertD
{
"~assert": {
"/x": 10,
"/y": 20
}
}
Expands to:
[
{
"op": "assert",
"path": "/x",
"equals": 10
},
{
"op": "assert",
"path": "/y",
"equals": 20
}
]
~delete
{
"~delete": [
"/tmp",
"/cache"
]
}
Expands to:
[
{
"op": "delete",
"path": "/tmp"
},
{
"op": "delete",
"path": "/cache"
}
]
Append notation (field[])
{
"/items[]": 123
}
Expands to:
{
"op": "set",
"path": "/items/-",
"value": 123
}
Pointer assignment
{
"/name": "/user/fullName"
}
Expands to:
{
"op": "copy",
"from": "/user/fullName",
"path": "/name",
"ignore_missing": true
}
Literal assignment
{
"/status": "active"
}
Expands to:
{
"op": "set",
"path": "/status",
"value": "active"
}
Priority order: ~assert (100) โ ~delete (50) โ assign/copy (0)
Built-in Operations
All operations are registered as ActionHandler instances in the main registry.
set
Write value to destination path.
{
"op": "set",
"path": "/target",
"value": "...",
"create": true,
// Auto-create parents (default: true)
"extend": true
// Extend lists on append (default: true)
}
Special: path ending with /- appends to list.
copy
Copy value from source to destination.
{
"op": "copy",
"from": "/source/path",
"path": "/dest/path",
"ignore_missing": false,
// Skip if missing (default: false)
"default": "..."
// Fallback value
}
copyD
Copy within destination (self-copy).
{
"op": "copyD",
"from": "/dest/src",
"path": "/dest/target"
}
delete
Remove value at path.
{
"op": "delete",
"path": "/remove",
"ignore_missing": true
// Don't error if missing (default: true)
}
foreach
Iterate over array/mapping.
{
"op": "foreach",
"in": "/items",
"as": "item",
// Variable name (default: "item")
"do": [
...
],
// Nested actions
"skip_empty": true,
// Skip if empty (default: true)
"default": []
// Fallback if missing
}
Note: If source is a dict, iterates over (key, value) tuples.
while
Loop while condition holds.
Path mode:
{
"op": "while",
"path": "/counter",
"equals": 0,
// Or "exists": true
"do": [
...
],
"do_while": false
// Execute at least once (default: false)
}
Expression mode:
{
"op": "while",
"cond": "${?dest.counter < `10`}",
"do": [
...
]
}
Note: Condition is checked against destination state. Use do_while: true to execute body at least once before checking condition.
if
Conditional execution.
Path mode:
{
"op": "if",
"path": "/check",
"equals": "value",
// Optional
"exists": true,
// Optional
"then": [
...
],
// Success branch
"else": [
...
]
// Failure branch
}
Expression mode:
{
"op": "if",
"cond": "${?source.age >= `18`}",
"then": [
...
]
}
exec
Execute nested script.
From source:
{
"op": "exec",
"from": "/script",
"merge": false
// Replace dest (default) or merge into it
}
Inline:
{
"op": "exec",
"actions": [
...
]
}
update
Merge mapping into target.
{
"op": "update",
"path": "/obj",
"value": {
"b": 2
},
// Or "from": "/source/obj"
"deep": false
// Recursive merge (default: false)
}
distinct
Remove duplicates from list.
{
"op": "distinct",
"path": "/items",
"key": "/id"
// Optional: compare by nested field
}
replace_root
Replace entire destination.
{
"op": "replace_root",
"value": {
"new": "root"
}
}
assert / assertD
Assert value existence/equality.
Basic usage:
{
"op": "assert",
// Check source
"path": "/required",
"equals": "value"
// Optional
}
With direct value:
{
"op": "assert",
"value": "${?source.computed}",
// Check computed value instead of path
"equals": "expected"
}
With return mode:
{
"op": "assert",
"path": "/optional",
"return": true,
// Return value instead of raising error
"to_path": "/result"
// Optional: write result to destination
}
return: trueโ returns value on success,falseon failure (instead of raising error)to_pathโ destination path for return valuevalueโ alternative topath, checks direct valueassertDchecks destination instead of source
Extending J-Perm
Custom Operations
Create a new ActionHandler and register it:
from j_perm import ActionHandler, ActionNode, OpMatcher, ExecutionContext
class MyOpHandler(ActionHandler):
def execute(self, step, ctx: ExecutionContext):
# Your logic here
return ctx.dest
# Register in main registry (in build_default_engine or custom factory)
registry.register(ActionNode(
name="my_op",
priority=10,
matcher=OpMatcher("my_op"),
handler=MyOpHandler(),
))
Custom Special Constructs
Add a SpecialFn to the specials dict:
def my_special(node, ctx):
value = ctx.engine.process_value(node["$mySpecial"], ctx)
return value.upper()
engine = build_default_engine(specials={
"$ref": ref_handler,
"$eval": eval_handler,
"$mySpecial": my_special,
})
Custom Stages
Create a StageProcessor for batch preprocessing:
from j_perm import StageProcessor, StageNode, StageRegistry
class ValidateStage(StageProcessor):
def apply(self, steps, ctx):
# Validate/transform steps
return steps
# Register in main pipeline stages
stages = build_default_shorthand_stages()
stages.register(StageNode(
name="validate",
priority=200, # Higher = runs earlier
processor=ValidateStage(),
))
# Use in custom engine
main_pipeline = Pipeline(stages=stages, registry=main_registry)
Custom Casters
Provide custom casters to TemplSubstHandler:
from j_perm import TemplSubstHandler
custom_casters = {
"int": lambda x: int(x),
"json": lambda x: json.loads(x),
}
handler = TemplSubstHandler(casters=custom_casters)
Or use the default (built-in int, float, bool, str).
Custom Matchers
Implement ActionMatcher or StageMatcher:
from j_perm import ActionMatcher
class PrefixMatcher(ActionMatcher):
def __init__(self, prefix):
self.prefix = prefix
def matches(self, step):
return isinstance(step, dict) and
step.get("op", "").startswith(self.prefix)
Advanced Topics
Value Stabilization Loop
When handlers call ctx.engine.process_value(value, ctx), the value pipeline runs repeatedly until:
- Output equals input (stable)
value_max_depthiterations reached (default: 50)
This resolves nested templates and special constructs:
# Input: {"$ref": "/path_to_template"}
# Pass 1: {"$ref": ...} โ "${/nested}"
# Pass 2: "${/nested}" โ "final"
# Pass 3: "final" โ "final" (stable โ)
Hierarchical Registries
Both StageRegistry and ActionTypeRegistry support tree structures:
# Group related operations
math_registry = ActionTypeRegistry()
math_registry.register(ActionNode("add", 10, AddMatcher(), AddHandler()))
math_registry.register(ActionNode("sub", 10, SubMatcher(), SubHandler()))
# Mount as sub-tree
main_registry.register_group(
"math",
math_registry,
matcher=OpMatcher("math"),
priority=50,
)
Priority and Execution Order
Stages: All matching stages run in priority order (high โ low).
Actions: First matching handler executes (unless exclusive=False).
Shorthands:
AssertShorthandProcessor(100) โ extracts~assert,~assertDDeleteShorthandProcessor(50) โ extracts~deleteAssignShorthandProcessor(0) โ fallback for all remaining keys
Unescape Rules
After value stabilization, registered UnescapeRule callables strip escape sequences:
from j_perm import UnescapeRule
# Built-in: template_unescape (strips $${ โ ${, $$ โ $)
# Registered at priority 0
# Add custom unescape
engine.unescape_rules.append(
UnescapeRule(name="custom", priority=10, unescape=my_unescape_fn)
)
API Reference
Core Classes
from j_perm import (
# Core infrastructure
ExecutionContext,
ValueResolver,
Engine,
Pipeline,
# Stage system
StageProcessor,
StageMatcher,
StageNode,
StageRegistry,
# Action system
ActionHandler,
ActionMatcher,
ActionNode,
ActionTypeRegistry,
# Middleware
Middleware,
# Unescape
UnescapeRule,
)
Handlers
from j_perm import (
# Value handlers
TemplMatcher,
TemplSubstHandler,
SpecialMatcher,
SpecialResolveHandler,
ContainerMatcher,
RecursiveDescentHandler,
IdentityHandler,
# Special construct functions
ref_handler,
eval_handler,
and_handler,
or_handler,
not_handler,
# Operation handlers
SetHandler,
CopyHandler,
CopyDHandler,
DeleteHandler,
ForeachHandler,
WhileHandler,
IfHandler,
ExecHandler,
UpdateHandler,
DistinctHandler,
ReplaceRootHandler,
AssertHandler,
AssertDHandler,
)
Utilities
from j_perm import (
# Matchers
OpMatcher,
AlwaysMatcher,
# Resolver
PointerResolver,
# Shorthand stages
AssertShorthandProcessor,
DeleteShorthandProcessor,
AssignShorthandProcessor,
# Factory
build_default_engine,
build_default_shorthand_stages,
)
Examples
Example 1: Data Filtering
spec = {
"op": "foreach",
"in": "/products",
"do": {
"op": "if",
"cond": "${?source.item.price < `100`}",
"then": {"/affordable[]": "/item"}
}
}
Example 2: Conditional Copy with Default
spec = {
"/result": {
"$ref": "/maybe_missing",
"$default": "not found"
}
}
Example 3: Nested Evaluation
spec = {
"/computed": {
"$eval": [
{"op": "set", "path": "/x", "value": "${int:/a}"},
{"op": "set", "path": "/y", "value": "${int:/b}"},
{"op": "replace_root", "value": "${?add(dest.x, dest.y)}"}
]
}
}
Example 4: Mixed Shorthands
spec = {
"~assert": {"/user/id": 123},
"~delete": "/temp",
"/output": "/user/name"
}
License
MIT (or adapt to your project as needed)
Contributing
Issues and pull requests welcome!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file j_perm-1.1.0.tar.gz.
File metadata
- Download URL: j_perm-1.1.0.tar.gz
- Upload date:
- Size: 37.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c37822923217cdcaf8518a84ffd1bfc395c74cab02b722d464e547bc70a14cf8
|
|
| MD5 |
56b2916d0acbf7625df439c16794f043
|
|
| BLAKE2b-256 |
fead63cd4b889692a7a82a5b4c3271a233a6072eab4f48d14c7d3b133cb95ab4
|
File details
Details for the file j_perm-1.1.0-py3-none-any.whl.
File metadata
- Download URL: j_perm-1.1.0-py3-none-any.whl
- Upload date:
- Size: 36.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de3563ad191ebe2cb998e9b0c4f4e04b084737ab16291f3482e9a650ef0e8ea2
|
|
| MD5 |
f33190667a1c925fed524f64e7c56bb9
|
|
| BLAKE2b-256 |
fb8934ba4e1a244432ad90dbc9289e2f9e76a36cae69e54e67e7305e2b755ca2
|