jsom permutation library
Project description
J-Perm
A small, composable JSON-transformation DSL implemented in Python.
The library lets you describe transformations as data (a list of steps) and then apply them to an input document. It supports JSON Pointer paths, custom JMESPath expressions, interpolation with ${...} syntax, special reference/evaluation values, and a rich set of built-in operations.
Features
-
JSON Pointer read/write with support for:
- root pointers (
"","/",".") - relative
..segments - list slices like
/items[1:3]
- root pointers (
-
Interpolation templates:
${/path/to/node}— JSON Pointer lookup${int:/path}/${float:/path}/${bool:/path}— simple type casters${? some.jmespath(expression) }— JMESPath with custom functions
-
Special values:
$ref— reference into the source document$eval— nested DSL evaluation with optional$select
-
Built-in operations:
set,copy,copyD,delete,assertforeach,if,distinctreplace_root,exec,update
-
Shorthand syntax for concise scripts (
~delete,~assert,field[], pointer assignments) -
Schema helper: approximate JSON Schema generation for a given DSL script.
Core API
apply_actions
from j_perm import apply_actions
result = apply_actions(actions, dest, source)
Signature:
apply_actions(
actions: Any,
*,
dest: MutableMapping[str, Any] | List[Any],
source: Mapping[str, Any] | List[Any],
) -> Mapping[str, Any]
actions— DSL script (list or mapping). Internally normalized vianormalize_actions()and shorthand expansion.dest— initial destination document to transform (typically a dict or list).source— source document or context; available to pointers, interpolation,$ref,$eval, and nested operations.- Returns a deep copy of the final
dest.
Basic usage
from j_perm import apply_actions
source = {
"users": [
{"name": "Alice", "age": 17},
{"name": "Bob", "age": 22}
]
}
actions = [
# Start with empty list
{"op": "replace_root", "value": []},
# For each user - build a simplified object
{
"op": "foreach",
"in": "/users",
"as": "u",
"do": [
{
"op": "set",
"path": "/-",
"value": {
"name": "${/u/name}",
"is_adult": {
"$eval": [
{"op": "replace_root", "value": False},
{
"op": "if",
"cond": "${?`${/u/age}` >= `18`}",
"then": [{"op": "replace_root", "value": True}]
}
]
}
}
}
]
}
]
result = apply_actions(actions, dest={}, source=source)
Interpolation & expression system (${...})
Interpolation is handled by the substitution utility used throughout operations such as set, copy, exec, update, schema building, etc.
1. JSON Pointer interpolation
- Form:
${/path/to/value} - Meaning: resolve
/path/to/valueagainst the current source context (the originalsourceplus any loop variables, etc.) and inject the value.
Example:
{
"op": "set",
"path": "/user/name",
"value": "Hello, ${/user/first_name}!"
}
2. Casters
${int:/age}→ cast value at/agetoint${float:/height}→ cast tofloat${bool:/flag}→ cast tobool
If the cast fails, an exception is raised.
3. JMESPath expressions
- Form:
${? <expression> } - Evaluated against the source context, with access to any custom JMESPath functions wired into the engine.
Example:
{
"op": "set",
"path": "/expensiveNames",
"value": "${? items[?price > `10`].name }"
}
4. Multiple templates in one string
Any string can contain multiple ${...} segments, which are resolved left-to-right.
Special values: $ref and $eval
Special values are resolved by resolve_special() before normal interpolation/substitution in operations like set, update, exec, replace_root.
$ref
{"$ref": "/path"} means “use the value at this path from the source”.
Example:
{
"op": "set",
"path": "/user",
"value": { "$ref": "/rawUser" }
}
This behaves similarly to a copy from /rawUser, but can be nested into larger structures and combined with $eval and interpolation.
$eval
{"$eval": [...]} means “execute this nested DSL script and use its result here”.
Example:
{
"op": "set",
"path": "/flag",
"value": {
"$eval": [
{ "op": "replace_root", "value": false },
{
"op": "if",
"cond": "${? some.expression }",
"then": [{ "op": "replace_root", "value": true }]
}
]
}
}
The nested script has the same source context as the outer script, and its final result becomes the value injected at /flag.
Shorthand syntax (shortcuts)
Shorthand is expanded by normalize_actions() into explicit operation steps.
How shorthand works internally
-
actionsmay be:- a list of steps,
- a mapping without
"op"— in that case it’s treated as shorthand and expanded.
Expansion is done by _expand_shorthand().
1. Delete shorthand: ~delete
{ "~delete": "/path" }
If value is a list:
{ "~delete": ["/a", "/b"] }
Expands into:
{ "op": "delete", "path": "/a" }
{ "op": "delete", "path": "/b" }
2. Assert shorthand: ~assert
If value is a mapping:
{ "~assert": { "/x": 10, "/y": 20 } }
Expands into:
{ "op": "assert", "path": "/x", "equals": 10 }
{ "op": "assert", "path": "/y", "equals": 20 }
If value is a string or list of strings:
{ "~assert": "/x" }
# or
{ "~assert": ["/x", "/y"] }
Expands into assertions that only check existence at those paths.
3. Append shorthand: field[]
A key ending with [] means “append to list at this path”.
{ "items[]": 123 }
Expands into:
{ "op": "set", "path": "/items/-", "value": 123 }
4. Pointer assignment shorthand
If a value is a string that starts with /, it’s treated as a pointer and expanded into a copy operation:
{ "name": "/user/fullName" }
Expands into:
{
"op": "copy",
"path": "/name",
"from": "/user/fullName",
"ignore_missing": true
}
(See _is_pointer_string() and _expand_shorthand() for details. )
Built-in operations: signatures & parameters
Below are all core operations with:
- the step shape,
- required vs optional parameters,
- default values.
All defaults are taken directly from the Python implementations.
set
Set or append a value at a JSON Pointer path in dest.
Signature
{
"op": "set", // required
"path": "/pointer", // required
"value": <any>, // required
"create": true, // optional, default: true
"extend": true // optional, default: true
}
Parameters
-
path(required) JSON Pointer path where the value is written.- If the path ends with
"/-", the value is appended to a list.
- If the path ends with
-
value(required) Any value, including special$ref/$evalobjects and interpolated strings. Resolved byresolve_special()and thensubstitute(). -
create(optional, defaulttrue) Iftrue, missing parent containers are created. -
extend(optional, defaulttrue) Ifpathis"/-"andvalueis a list:extend=true⇒ list is extended with elements ofvalueextend=false⇒valueis appended as a single item (nested list)
copy
Copy a value from source (or extended source context) into dest. Internally uses set.
Signature
{
"op": "copy", // required
"from": "/source/pointer", // required
"path": "/target/pointer", // required
"create": true, // optional, default: true
"extend": true, // optional, default: true
"ignore_missing": false, // optional, default: false
"default": <any> // optional
}
Parameters
from(required) JSON Pointer into the source context (after interpolation).path(required) Destination path; same semantics as inset.create(optional, defaulttrue) Passed through toset— create missing parents.extend(optional, defaulttrue) Passed through toset— controls list extension on appends.ignore_missing(optional, defaultfalse) Iftrueandfromcannot be resolved, the operation is a no-op.default(optional) Used whenfromcannot be resolved andignore_missingis not set. If provided,defaultis copied intopathinstead of raising.
copyD
Copy a value from the current dest (self) into another path in dest. Internally uses set.
This is useful for rearranging or duplicating data that has already been built in dest, without going back to the source.
Signature
{
"op": "copyD", // required
"from": "/source/pointer", // required
"path": "/target/pointer", // required
"create": true, // optional, default: true
"ignore_missing": false, // optional, default: false
"default": <any> // optional
}
Parameters
from(required) JSON Pointer evaluated againstdest(notsource). The pointer itself is first interpolated withsrc(source context), but resolution usesdest.path(required) Destination path indest, same semantics as inset.create(optional, defaulttrue) Passed toset— whether to create missing parent containers.ignore_missing(optional, defaultfalse) Iftrueand thefrompointer cannot be resolved indest, the operation becomes a no-op.default(optional) Used whenfromcannot be resolved andignore_missingis not set. If provided, that default is deep-copied intopathinstead of raising.
delete
Delete a node at a JSON Pointer path in dest.
Signature
{
"op": "delete", // required
"path": "/pointer", // required
"ignore_missing": true // optional, default: true
}
Parameters
path(required) JSON Pointer to the node to delete. Must not end with"-".ignore_missing(optional, defaulttrue) Iffalse, a missing path raises an error; iftrue, missing path is silently ignored.
assert
Assert node existence and optional equality in dest.
Signature
{
"op": "assert", // required
"path": "/pointer", // required
"equals": <any> // optional
}
Parameters
path(required) JSON Pointer to check indest. If it does not exist, anAssertionErroris raised.equals(optional) If provided, the value atpathis compared withequals; mismatch raisesAssertionError.
foreach
Iterate over an array (or mapping) in the source context and execute nested actions.
Signature
{
"op": "foreach", // required
"in": "/array/path", // required
"do": [ ... ], // required
"as": "item", // optional, default: "item"
"default": [], // optional, default: []
"skip_empty": true // optional, default: true
}
Parameters
in(required) JSON Pointer to the array in the source context (after interpolation).do(required) Nested DSL script (list or single op) executed for each element.as(optional, default"item") Name of the variable bound to the current element in the extended source context.default(optional, default[]) Used when the pointer in"in"cannot be resolved.skip_empty(optional, defaulttrue) Iftrueand the resolved array is empty, the loop is skipped.
Additional behavior:
- If the resolved object is a dict, it’s converted to a list of
(key, value)pairs. - On exception in the body,
destis restored from a deep copy snapshot.
if
Conditionally execute nested actions.
Signature (path-based condition)
{
"op": "if", // required
"path": "/pointer", // required in this mode
"equals": <any>, // optional
"exists": true, // optional
"then": [ ... ], // optional
"else": [ ... ], // optional
"do": [ ... ] // optional (fallback success branch)
}
Signature (expression-based condition)
{
"op": "if", // required
"cond": "${?...}", // required in this mode
"then": [ ... ], // optional
"else": [ ... ], // optional
"do": [ ... ] // optional (fallback success branch)
}
Parameters
At least one of path or cond must be supplied:
-
path(required in path-mode)- If combined with
equals, condition isdest[path] == equalsand path must exist. - If combined with
exists(truthy), condition is “path exists”. - Else, condition is
bool(dest[path])(path must exist).
- If combined with
-
equals(optional, path-mode only) Expected value for equality check. -
exists(optional, path-mode only) If truthy, check is presence of path only. -
cond(required in expression-mode) Arbitrary interpolated value or expression;bool(cond)is used.
Branches:
then(optional) Executed when condition is true.else(optional) Executed when condition is false.do(optional) If condition is true and"then"is missing,"do"is used as the success branch.
If no branch is present for the chosen condition result, dest is returned unchanged.
Error handling:
- Before running branch actions, a deep copy snapshot of
destis taken. - On exception,
destis restored to the snapshot.
distinct
Remove duplicates in a list at the given path, preserving order.
Signature
{
"op": "distinct", // required
"path": "/list/path", // required
"key": "/key/pointer" // optional
}
Parameters
-
path(required) JSON Pointer to a list indest. If target is not a list, aTypeErroris raised. -
key(optional) JSON Pointer evaluated per item to compute the deduplication key.- If provided: uniqueness is based on
jptr_get(item, key_path). - If omitted: the whole
itemis used as the key.
- If provided: uniqueness is based on
replace_root
Replace the whole dest root value with a new one.
Signature
{
"op": "replace_root", // required
"value": <any> // required
}
Parameters
-
value(required) Value to become the new root.- Special values (
$ref,$eval) are resolved viaresolve_special(). - Strings/lists/dicts are then passed through interpolation
substitute(). - The final value is deep-copied.
- Special values (
Result of replace_root is the new dest.
exec
Execute a nested DSL script held inline or at a pointer.
Exactly one of from or actions must be provided.
Signature (script from pointer)
{
"op": "exec", // required
"from": "/script/path", // required in this mode
"default": <any>, // optional
"merge": false // optional, default: false
}
Signature (inline script)
{
"op": "exec", // required
"actions": [ ... ], // required in this mode
"merge": false // optional, default: false
}
Parameters
-
from(required in pointer-mode) Pointer (possibly interpolated) to the actions in the source context. If resolution fails:- and
defaultis present ⇒ usedefaultas the script (after resolving specials and interpolation if it’s str/list/dict), - else ⇒ raise
ValueError.
- and
-
actions(required in inline-mode) Inline DSL script (list or mapping). Special values and templates are resolved. -
merge(optional, defaultfalse)merge=false(default): nested script runs withdest={}, result replaces currentdest.merge=true: nested script runs on current dest and the result is returned (like a sub-call toapply_actionson samedest).
-
default(optional, pointer-mode only) Fallback script iffromcannot be resolved.
update
Update a mapping at the given path using either inline value or source mapping.
Exactly one of from or value must be provided.
Signature
{
"op": "update", // required
"path": "/target/path", // required
"from": "/source/path", // required in from-mode
"value": { ... }, // required in value-mode
"default": { ... }, // optional (from-mode only)
"create": true, // optional, default: true
"deep": false // optional, default: false
}
Parameters
-
path(required) JSON Pointer path to the mapping to update. -
from(required in from-mode) Pointer to mapping in source; deep-copied. If resolution fails:- and
defaultis provided ⇒ usedefault, - else ⇒ raise.
- and
-
value(required in value-mode) Inline mapping; resolved throughresolve_specialandsubstituteif needed. -
default(optional, from-mode only) Mapping used whenfromcannot be resolved. -
create(optional, defaulttrue) Iftrue, missing containers atpathare created (including the leaf if needed). -
deep(optional, defaultfalse)false: shallow update viadict.update().true: recursive deep-merge of nested mappings; leaves and non-mappings are overwritten via deep copy.
Constraints:
- The resolved update value must be a mapping; otherwise
TypeError. - The target at
pathmust be a mutable mapping; otherwiseTypeError.
Schema generation
from j_perm import build_schema
schema = build_schema(script)
schema is a JSON-Schema-like structure inferred by scanning the script:
- Tracks
replace_root,set,update, nested$eval, etc. - Infers basic JSON types from literal values.
Extending with custom operations
from j_perm import register_op
@register_op("my_op")
def my_op(step, dest, src):
# implement your logic
return dest
Any registered operation can be used in scripts via:
{ "op": "my_op", ... }
License
This package is provided as-is; feel free to adapt it to your project structure.
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-0.1.3.1.tar.gz.
File metadata
- Download URL: j_perm-0.1.3.1.tar.gz
- Upload date:
- Size: 21.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6c235e1a94f79d3ee1141376d772856f476dff5abea09c7cc1268cad0e206e2
|
|
| MD5 |
2d53ead4aa2f071668060c098b2f8153
|
|
| BLAKE2b-256 |
1c2483bc2430afb8aae95d342124dfdae78e671e1d557e9ec831f700cd54cffe
|
File details
Details for the file j_perm-0.1.3.1-py3-none-any.whl.
File metadata
- Download URL: j_perm-0.1.3.1-py3-none-any.whl
- Upload date:
- Size: 22.2 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 |
becc598b1b1cb40033b6715f68698de7a6c27988af192c52f11cf5ab86e2e559
|
|
| MD5 |
93befd3174b64ca7a80359f9a2997725
|
|
| BLAKE2b-256 |
a29d4407ca6d7a647abb5eb60b702b09a4628b1dd0d1edfa92906065c198cdb6
|