Skip to content

Commit 8cc2ae0

Browse files
Ludwig Schneidernh916
andauthored
Self-Contained JSON file format (#412)
* test failure too * accept kwargs * wrote docs for `get_self_contained_json` * formatted core.py with black * added type hint of return type str * renamed expanded json function changed from `get_self_contained_json` to `get_expanded_json` * refactored `get_expanded_json` test to work correctly with new name * added comments * renamed variables to be more self documenting and obvious * added type hinting where it would help * renamed test to be more self documenting and make more sense * wrote docstrings for `test_expanded_json` * added notes to `get_expanded_json` * added link to GitHub discussions on deserialization * added `linenums` to .cspell.json * updated `get_expanded_json` code examples with better name * changed name from `mybigsmiles` that triggered spelling error CI to something that would not trigger it and is more explicit and better anyways. * replaced it with `my material 1 bigsmiles` and `my material 2 bigsmiles` * fixed extra parenthesis syntax error for doctest after merge --------- Co-authored-by: nh916 <[email protected]>
1 parent d97eb4f commit 8cc2ae0

File tree

3 files changed

+184
-1
lines changed

3 files changed

+184
-1
lines changed

.trunk/configs/.cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"BCDB",
121121
"doctest",
122122
"Doctest",
123-
"Doctests"
123+
"Doctests",
124+
"linenums"
124125
]
125126
}

src/cript/nodes/core.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,164 @@ def json(self):
248248

249249
return json_string
250250

251+
def get_expanded_json(self, **kwargs) -> str:
252+
"""
253+
Generates a long-form JSON representation of the current node and its hierarchy.
254+
255+
The long-form JSON includes complete details of the node, eliminating the need for
256+
references to UUIDs to nodes stored in the CRIPT database. This comprehensive representation
257+
is useful for offline storage of CRIPT nodes, transferring nodes between different CRIPT instances,
258+
or for backup purposes.
259+
260+
The generated long-form JSON can be reloaded into the SDK using
261+
[`cript.load_nodes_from_json()`](../../../utility_functions/#cript.nodes.util.load_nodes_from_json),
262+
ensuring consistency and completeness of the node data.
263+
However, it's important to note that this long-form JSON might not comply directly with the JSON schema
264+
required for POST or PATCH requests to the CRIPT API.
265+
266+
Optional keyword arguments (`kwargs`) are supported and are passed directly to `json.dumps()`.
267+
These arguments allow customization of the JSON output, such as formatting for readability
268+
or pretty printing.
269+
270+
Parameters
271+
----------
272+
**kwargs : dict, optional
273+
Additional keyword arguments for `json.dumps()` to customize the JSON output, such as `indent`
274+
for pretty-printing.
275+
276+
Returns
277+
-------
278+
str
279+
A comprehensive JSON string representing the current node and its entire hierarchy in long-form.
280+
281+
Notes
282+
-----
283+
The `get_expanded_json()` method differs from the standard [`json`](./#cript.nodes.core.BaseNode.json)
284+
property or method, which might provide a more condensed version of the node's data.
285+
286+
> For more information on condensed JSON and deserialization, please feel free to reference our discussion
287+
> on [deserializing Python nodes to JSON](https://github.com/C-Accel-CRIPT/Python-SDK/discussions/177)
288+
289+
Examples
290+
--------
291+
>>> import cript
292+
>>> # ============= Create all needed nodes =============
293+
>>> my_project = cript.Project(name=f"my_Project")
294+
>>> my_collection = cript.Collection(name="my collection")
295+
>>> my_material_1 = cript.Material(
296+
... name="my material 1", identifier=[{"bigsmiles": "my material 1 bigsmiles"}]
297+
... )
298+
>>> my_material_2 = cript.Material(
299+
... name="my material 2", identifier=[{"bigsmiles": "my material 2 bigsmiles"}]
300+
... )
301+
>>> my_inventory = cript.Inventory(
302+
... name="my inventory", material=[my_material_1, my_material_2]
303+
... )
304+
>>> # ============= Assemble nodes =============
305+
>>> my_project.collection = [my_collection]
306+
>>> my_project.collection[0].inventory = [my_inventory]
307+
>>> # ============= Get long form JSON =============
308+
>>> long_form_json = my_project.get_expanded_json(indent=4)
309+
310+
???+ info "Short JSON VS Long JSON"
311+
# Default Short JSON
312+
> This is the JSON when `my_project.json` is called
313+
314+
```json linenums="1"
315+
{
316+
"node":[
317+
"Project"
318+
],
319+
"uid":"_:d0d1b3c9-d552-4d4f-afd2-76f01538b87a",
320+
"uuid":"d0d1b3c9-d552-4d4f-afd2-76f01538b87a",
321+
"name":"my_Project",
322+
"collection":[
323+
{
324+
"node":[
325+
"Collection"
326+
],
327+
"uid":"_:07765ac8-862a-459e-9d99-d0439d6a6a09",
328+
"uuid":"07765ac8-862a-459e-9d99-d0439d6a6a09",
329+
"name":"my collection",
330+
"inventory":[
331+
{
332+
"node":[
333+
"Inventory"
334+
],
335+
"uid":"_:4cf2bbee-3dc0-400b-8269-709f99d89d9f",
336+
"uuid":"4cf2bbee-3dc0-400b-8269-709f99d89d9f",
337+
"name":"my inventory",
338+
"material":[
339+
{
340+
"uuid":"0cf14572-4da2-43f2-8cb9-e8374086368e"
341+
},
342+
{
343+
"uuid":"6302a8b0-4265-4a3a-a40f-bbcbb7293046"
344+
}
345+
]
346+
}
347+
]
348+
}
349+
]
350+
}
351+
```
352+
353+
# Long JSON
354+
> This is what is created when `my_project.get_expanded_json()()`
355+
356+
```json linenums="1"
357+
{
358+
"node":[
359+
"Project"
360+
],
361+
"uid":"_:afe4bb2f-fa75-4736-b692-418a5143e6f5",
362+
"uuid":"afe4bb2f-fa75-4736-b692-418a5143e6f5",
363+
"name":"my_Project",
364+
"collection":[
365+
{
366+
"node":[
367+
"Collection"
368+
],
369+
"uid":"_:8b5c8125-c956-472a-9d07-8cb7b402b101",
370+
"uuid":"8b5c8125-c956-472a-9d07-8cb7b402b101",
371+
"name":"my collection",
372+
"inventory":[
373+
{
374+
"node":[
375+
"Inventory"
376+
],
377+
"uid":"_:1bd3c966-cb35-494d-85cd-3515cde570f3",
378+
"uuid":"1bd3c966-cb35-494d-85cd-3515cde570f3",
379+
"name":"my inventory",
380+
"material":[
381+
{
382+
"node":[
383+
"Material"
384+
],
385+
"uid":"_:07bc3e4f-757f-4ac7-ae8a-7a0c68272531",
386+
"uuid":"07bc3e4f-757f-4ac7-ae8a-7a0c68272531",
387+
"name":"my material 1",
388+
"bigsmiles":"my material 1 bigsmiles"
389+
},
390+
{
391+
"node":[
392+
"Material"
393+
],
394+
"uid":"_:64565687-5707-4d67-860f-5ee4a057a45f",
395+
"uuid":"64565687-5707-4d67-860f-5ee4a057a45f",
396+
"name":"my material 2",
397+
"bigsmiles":"my material 2 bigsmiles"
398+
}
399+
]
400+
}
401+
]
402+
}
403+
]
404+
}
405+
```
406+
"""
407+
return self.get_json(handled_ids=None, known_uuid=None, suppress_attributes=None, is_patch=False, condense_to_uuid={}, **kwargs).json
408+
251409
def get_json(
252410
self,
253411
handled_ids: Optional[Set[str]] = None,
@@ -272,6 +430,8 @@ def get_json(
272430
User facing access to get the JSON of a node.
273431
Opposed to the also available property json this functions allows further control.
274432
Additionally, this function does not call `self.validate()` but the property `json` does.
433+
We also accept `kwargs`, that are passed on to the JSON decoding via `json.dumps()` this can be used for example to prettify the output.
434+
275435
276436
Returns named tuple with json and handled ids as result.
277437
"""

tests/test_node_util.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,25 @@ def test_invalid_project_graphs(simple_project_node, simple_material_node, simpl
323323

324324
cript.add_orphaned_nodes_to_project(project, project.collection[0].experiment[0], 10)
325325
project.validate()
326+
327+
328+
def test_expanded_json(complex_project_node):
329+
"""
330+
Tests the generation and deserialization of expanded JSON for a complex project node.
331+
332+
This test verifies 2 key aspects:
333+
1. A complex project node can be serialized into an expanded JSON string, without UUID placeholders.
334+
2. The expanded JSON can be deserialized into a node that is equivalent to the original node.
335+
"""
336+
expanded_project_json: str = complex_project_node.get_expanded_json()
337+
deserialized_project_node: cript.Project = cript.load_nodes_from_json(expanded_project_json)
338+
339+
# assert the expanded JSON was correctly deserialized to project node
340+
assert deserialized_project_node == complex_project_node
341+
342+
short_json: str = complex_project_node.json
343+
344+
# since short JSON has UUID it will not be able to deserialize correctly and will
345+
# raise CRIPTJsonDeserializationError
346+
with pytest.raises(cript.nodes.exceptions.CRIPTJsonDeserializationError):
347+
cript.load_nodes_from_json(short_json)

0 commit comments

Comments
 (0)