Skip to main content

Traversing and manipulating hierarchical info sets (JSON) using pythonic JSON Path-like expressions

Project description

Traversing and manipulating hierarchical data (JSON) using pythonic JSON Path-like expressions

Importing

>>> from aweson import JP, find_all, find_all_duplicate, find_all_unique, find_next

Iterating over hierarchical data

>>> content = {"employees": [
...     {"name": "Doe, John", "age": 32, "account": "johndoe"},
...     {"name": "Doe, Jane", "age": -23, "account": "janedoe"},
...     {"name": "Deer, Jude", "age": 42, "account": "judedeer"},
... ]}
>>> list(find_all(content, JP.employees[:].name))
['Doe, John', 'Doe, Jane', 'Deer, Jude']

Note that the JSON Path-like expression JP.employees[:].name is not a string, it’s a Python expression, i.e. your IDE will be of actual help.

Furthermore, to address all items in a list, Pythonic slice expression [:] is used. Naturally, other indexing and slice expressions also work in the conventional Pythonic way:

>>> list(find_all(content, JP.employees[-1].name))
['Deer, Jude']
>>> list(find_all(content, JP.employees[:2].name))
['Doe, John', 'Doe, Jane']

Selecting list items by boolean expressions

List items can also be selected by simple boolean expressions evaluated within the context of a list item, for instance

>>> list(find_all(content, JP.employees[JP.age > 35]))
[{'name': 'Deer, Jude', 'age': 42, 'account': 'judedeer'}]

Only simple comparisons are supported, and only these operators: - ==, !=, - <, <=, - >, >=.

Both operands can be dict keys in a list item, e.g. expressions like JP.years[JP.planned_budget < JP.realized_budget] are supported.

In addition to this, existence of a sub-item or path also be used as a list item selector, e.g. JP.years[JP.planned_budget] would select only the years where the key planned_budget exists.

Paths to items iterated

You may be interested in the actual path of an item being returned, just like you get an index alongside items when using enumerate(). For instance, you may want to verify ages being non-negative, and report accurately the path of failure items:

>>> path, item = next(tup for tup in find_all(content, JP.employees[:].age, with_path=True) if tup[1] < 0)
>>> item
-23

The offending path, then, in human-readable format:

>>> str(path)
'$.employees[1].age'

The enclosing record, using .parent attribute of the path obtained for the offending age:

>>> next(find_all(content, path.parent))
{'name': 'Doe, Jane', 'age': -23, 'account': 'janedoe'}

Note, with argument with_path=True passed, find_all() yields tuples instead of single items.

Selecting sub-items

You can select sub-items of iterated items, comes handy into turning one structure into another, like a list of records into a dict:

>>> {tup[0]: tup[1] for tup in find_all(content, JP.employees[:](JP.account, JP.name))}
{'johndoe': 'Doe, John', 'janedoe': 'Doe, Jane', 'judedeer': 'Deer, Jude'}

or, to make your processing logic within the same comprehension expression more readable:

>>> {account: name for account, name in find_all(content, JP.employees[:](JP.account, JP.name))}
{'johndoe': 'Doe, John', 'janedoe': 'Doe, Jane', 'judedeer': 'Deer, Jude'}

You can also make a sub-items selection produce named tuples by explicitly naming sub-paths:

>>> list(find_all(content, JP.employees[:](account=JP.account, name=JP.name)))
[SubSelect(account='johndoe', name='Doe, John'), SubSelect(account='janedoe', name='Doe, Jane'), SubSelect(account='judedeer', name='Deer, Jude')]

Now, the processing code could be elsewhere than the find_all() invocation, as named tuples will carry the field names with them. The produced named tuples will all be called SubSelect but they will be different named tuples for each invocation.

Variable field name selection

The forms JP["field_name"] and JP.field_name are equivalent:

>>> from functools import reduce
>>> def my_sum(content, field_name, initial):
...     return reduce(
...         lambda x, y: x + y,
...         find_all(content, JP.employees[:][field_name]),
...         initial
...     )
>>> my_sum(content, "age", 0)
51
>>> my_sum(content, "account", "")
'johndoejanedoejudedeer'

Field name by regular expressions

Sometimes you have a JSON format where types are represented by JSON objects, e.g.

>>> content = {
...     "apple": [{"name": "red delicious"}, {"name": "punakaneli"}],
...     "pear": [{"name": "wilhelm"}, {"name": "conference"}]
... }

i.e. it’s not a union list of records with a type discriminator [{"type": "apple", "name": ...}, ...]. Iterating over all the fruits, regardless of their type, in our example content above can be achieved by:

>>> list(find_all(content, JP["apple|pear"][:].name))
['red delicious', 'punakaneli', 'wilhelm', 'conference']

if you are only interested in apples and pears, or

>>> list(find_all(content, JP[".*"][:].name))
['red delicious', 'punakaneli', 'wilhelm', 'conference']

if you are interested in fruits other than apples and pears.

Note, that the expression JP["*"] is also supported, but that’s not a regular expression, merely a conventional JSON Path notation, and equivalent to JP[:]:

>>> list(find_all([5, 42, 137], JP["*"]))
[5, 42, 137]

Suppressing indexing errors and key errors

By default, path expressions are strict, e.g. for list indexes:

>>> list(find_all([0, 1], JP[2]))
Traceback (most recent call last):
    ...
IndexError: list index out of range

and for dict keys:

>>> list(find_all({"hello": 42}, JP.hi))
Traceback (most recent call last):
    ...
KeyError: 'hi'

You can suppress these errors and simply have nothing yielded, for list indexes:

>>> list(find_all([0, 1], JP[2], lenient=True))
[]

and for dict keys:

>>> list(find_all({"hello": 42}, JP.hi, lenient=True))
[]

Utility find_next()

Often, you just need a first value, roughly equivalent to a next(find_all(...)) invocation. You can use find_next() for this, for instance

>>> find_next([{"hello": 5}, {"hello": 42}], JP[:].hello)
5
>>> find_next([{"hello": 5}, {"hello": 42}], JP[1].hello)
42

You can also ask for the path of the value returned, in the style of with_path=True above

>>> path, value = find_next([{"hello": 5}, {"hello": 42}], JP[-1].hello, with_path=True)
>>> str(path)
'$[1].hello'
>>> value
42

You can also supply a default value for find_next(), just like for next():

>>> find_next([{"hello": 5}, {"hello": 42}], JP[3].hello, default=17)
17

Utilities: finding unique and duplicate items

A common task is to find only unique items in data, e.g.

>>> content = [{"hi": 1}, {"hi": 2}, {"hi": 1}, {"hi": 3}, {"hi": -22}, {"hi": 3}]
>>> list(find_all_unique(content, JP[:].hi))
[1, 2, 3, -22]

and of course you can ask for the paths, too

>>> content = [{"hi": 1}, {"hi": 2}, {"hi": 1}, {"hi": 3}, {"hi": -22}, {"hi": 3}]
>>> [(str(path), item) for path, item in find_all_unique(content, JP[:].hi, with_path=True)]
[('$[0].hi', 1), ('$[1].hi', 2), ('$[3].hi', 3), ('$[4].hi', -22)]

A related common task is to find duplicates, e.g.

>>> content = {
...     "apple": [{"name": "red delicious", "id": 123}, {"name": "punakaneli", "id": 234}],
...     "pear": [{"name": "wilhelm", "id": 345}, {"name": "conference", "id": 123}]
... }
>>> [f"Duplicate ID: {item} at {path.parent}" for path, item in find_all_duplicate(content, JP["apple|pear"][:].id, with_path=True)]
['Duplicate ID: 123 at $.pear[1]']

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aweson-2.2.0.tar.gz (19.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aweson-2.2.0-py3-none-any.whl (10.1 kB view details)

Uploaded Python 3

File details

Details for the file aweson-2.2.0.tar.gz.

File metadata

  • Download URL: aweson-2.2.0.tar.gz
  • Upload date:
  • Size: 19.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.18

File hashes

Hashes for aweson-2.2.0.tar.gz
Algorithm Hash digest
SHA256 b08311659b471a08153116617999ff721a22cb9e266c1c6d8f7abbe9db3dab87
MD5 85afa9d349123501f3ec2c4b502c45ec
BLAKE2b-256 1d7f004cc37e6feb6afcc48e6f70ac79376c5e7dfccbcd5be52e6a6c9993deaa

See more details on using hashes here.

File details

Details for the file aweson-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: aweson-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 10.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.18

File hashes

Hashes for aweson-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d70d5c490c0ed4262ab260f9c224e41ced1b54fda37ce5d9293e227968d62712
MD5 49f44a276b8d18c842abbbed2d713f4b
BLAKE2b-256 71fe22f40320d6105fadac0fbfcaa2dfcec02f34ce5d9034ddc5c906ca707958

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page