Skip to content

Commit 30c6a1c

Browse files
committed
Issue #324 add DataCube.print_json() and discourage flat_graph() stronger for general use
Also add `to_json`/`print_json` usage docs
1 parent 1290234 commit 30c6a1c

File tree

6 files changed

+135
-32
lines changed

6 files changed

+135
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Allow passing raw JSON string, JSON file path or URL to `Connection.download()`,
1313
`Connection.execute()` and `Connection.create_job()`
1414
- Add support for reverse math operators on DataCube in `apply` mode ([#323](https://github.com/Open-EO/openeo-python-client/issues/323))
15+
- Add `DataCube.print_json()` to simplify exporting process graphs in Jupyter or other interactive environments ([#324](https://github.com/Open-EO/openeo-python-client/issues/324))
1516

1617

1718
### Changed

docs/cookbook/tricks.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,58 @@ Miscellaneous tips and tricks
33
===============================
44

55

6+
.. _process_graph_export:
7+
8+
Export a process graph
9+
-----------------------
10+
11+
You can export the underlying process graph of
12+
a :py:class:`~openeo.rest.datacube.DataCube`, :py:class:`~openeo.rest.vectorcube.VectorCube`, etc,
13+
to a standardized JSON format, which allows interoperability with other openEO tools.
14+
15+
For example, use :py:meth:`~openeo.rest.datacube.DataCube.print_json()` to directly print the JSON representation
16+
in your interactive Jupyter or Python session:
17+
18+
.. code-block:: pycon
19+
20+
>>> dump = cube.print_json()
21+
{
22+
"process_graph": {
23+
"loadcollection1": {
24+
"process_id": "load_collection",
25+
...
26+
27+
Or save it to a file, by getting the JSON representation first as a string
28+
with :py:meth:`~openeo.rest.datacube.DataCube.to_json()`:
29+
30+
.. code-block:: python
31+
32+
# Export as JSON string
33+
dump = cube.to_json()
34+
35+
# Write to file in `pathlib` style
36+
export_path = pathlib.Path("path/to/export.json")
37+
export_path.write_text(dump, encoding="utf8")
38+
39+
# Write to file in `open()` style
40+
with open("path/to/export.json", encoding="utf8") as f:
41+
f.write(dump)
42+
43+
44+
.. warning::
45+
46+
Avoid using methods like :py:meth:`~openeo.rest.datacube.DataCube.flat_graph()`,
47+
which are mainly intended for internal use.
48+
Not only are these methods subject to change, they also lead to representations
49+
with interoperability and reuse issues.
50+
For example, naively printing or automatic (``repr``) rendering of
51+
:py:meth:`~openeo.rest.datacube.DataCube.flat_graph()` output will roughly look like JSON,
52+
but is in fact invalid: it uses single quotes (instead of double quotes)
53+
and booleans values are title-case (instead of lower case).
54+
55+
56+
57+
658
Execute a process graph directly from raw JSON
759
-----------------------------------------------
860

openeo/internal/graph_building.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def _deep_copy(x):
114114
return _deep_copy(self)
115115

116116
def flat_graph(self) -> dict:
117-
"""Get the process graph in flat dict representation."""
117+
"""Get the process graph in internal flat dict representation."""
118118
return GraphFlattener().flatten(node=self)
119119

120120
flatten = legacy_alias(flat_graph, name="flatten")
@@ -145,7 +145,7 @@ def from_flat_graph(flat_graph: dict, parameters: Optional[dict] = None) -> 'PGN
145145

146146
def as_flat_graph(x: Union[dict, Any]) -> dict:
147147
"""
148-
Convert given object to a flat dict graph representation.
148+
Convert given object to a internal flat dict graph representation.
149149
"""
150150
if isinstance(x, dict):
151151
return x

openeo/internal/processes/builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def process(cls, process_id: str, arguments: dict = None, namespace: Union[str,
5353
return cls(PGNode(process_id=process_id, arguments=arguments, namespace=namespace))
5454

5555
def flat_graph(self) -> dict:
56-
"""Get the process graph in flat dict representation"""
56+
"""Get the process graph in internal flat dict representation."""
5757
return self.pgnode.flat_graph()
5858

5959
def from_node(self) -> PGNode:

openeo/rest/_datacube.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import logging
33
import typing
4-
from typing import Optional
4+
from typing import Optional, Union, Tuple
55

66
from openeo.internal.graph_building import PGNode, _FromNodeMixin
77
from openeo.util import legacy_alias
@@ -32,23 +32,54 @@ def __str__(self):
3232

3333
def flat_graph(self) -> dict:
3434
"""
35-
Get the process graph in flat dict representation
35+
Get the process graph in internal flat dict representation.
3636
37-
.. note:: This method is mainly for internal use, subject to change and not recommended for general usage.
38-
Instead, use :py:meth:`to_json()` to get a JSON representation of the process graph.
37+
.. warning:: This method is mainly intended for internal use.
38+
It is not recommended for general use and is *subject to change*.
39+
40+
Instead, it is recommended to use
41+
:py:meth:`to_json()` or :py:meth:`print_json()`
42+
to obtain a standardized, interoperable JSON representation of the process graph.
43+
See :ref:`process_graph_export` for more information.
3944
"""
4045
# TODO: wrap in {"process_graph":...} by default/optionally?
4146
return self._pg.flat_graph()
4247

4348
flatten = legacy_alias(flat_graph, name="flatten")
4449

45-
def to_json(self, indent=2, separators=None) -> str:
50+
def to_json(self, *, indent: Union[int, None] = 2, separators: Optional[Tuple[str, str]] = None) -> str:
4651
"""
47-
Get JSON representation of (flat dict) process graph.
52+
Get interoperable JSON representation of the process graph.
53+
54+
See :py:meth:`DataCube.print_json` to directly print the JSON representation
55+
and :ref:`process_graph_export` for more usage information.
56+
57+
Also see ``json.dumps`` docs for more information on the JSON formatting options.
58+
59+
:param indent: JSON indentation level.
60+
:param separators: (optional) tuple of item/key separators.
61+
:return: JSON string
4862
"""
4963
pg = {"process_graph": self.flat_graph()}
5064
return json.dumps(pg, indent=indent, separators=separators)
5165

66+
def print_json(self, *, file=None, indent: Union[int, None] = 2, separators: Optional[Tuple[str, str]] = None):
67+
"""
68+
Print interoperable JSON representation of the process graph.
69+
70+
See :py:meth:`DataCube.to_json` to get the JSON representation as a string
71+
and :ref:`process_graph_export` for more usage information.
72+
73+
Also see ``json.dumps`` docs for more information on the JSON formatting options.
74+
75+
:param file: file-like object (stream) to print to (current ``sys.stdout`` by default).
76+
:param indent: JSON indentation level.
77+
:param separators: (optional) tuple of item/key separators.
78+
79+
.. versionadded:: 0.12.0
80+
"""
81+
print(self.to_json(indent=indent, separators=separators), file=file)
82+
5283
@property
5384
def _api_version(self):
5485
return self._connection.capabilities().api_version_check

tests/rest/datacube/test_datacube100.py

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
"""
66
import collections
7+
import io
78
import pathlib
89
import re
910
import sys
@@ -1428,32 +1429,34 @@ def test_save_result_format(con100, requests_mock):
14281429
cube.save_result(format="pNg")
14291430

14301431

1432+
EXPECTED_JSON_EXPORT_S2_NDVI = textwrap.dedent('''\
1433+
{
1434+
"process_graph": {
1435+
"loadcollection1": {
1436+
"process_id": "load_collection",
1437+
"arguments": {
1438+
"id": "S2",
1439+
"spatial_extent": null,
1440+
"temporal_extent": null
1441+
}
1442+
},
1443+
"ndvi1": {
1444+
"process_id": "ndvi",
1445+
"arguments": {
1446+
"data": {
1447+
"from_node": "loadcollection1"
1448+
}
1449+
},
1450+
"result": true
1451+
}
1452+
}
1453+
}''')
1454+
1455+
14311456
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires 'insertion ordered' dicts from python3.6 or higher")
14321457
def test_to_json(con100):
14331458
ndvi = con100.load_collection("S2").ndvi()
1434-
expected = textwrap.dedent('''\
1435-
{
1436-
"process_graph": {
1437-
"loadcollection1": {
1438-
"process_id": "load_collection",
1439-
"arguments": {
1440-
"id": "S2",
1441-
"spatial_extent": null,
1442-
"temporal_extent": null
1443-
}
1444-
},
1445-
"ndvi1": {
1446-
"process_id": "ndvi",
1447-
"arguments": {
1448-
"data": {
1449-
"from_node": "loadcollection1"
1450-
}
1451-
},
1452-
"result": true
1453-
}
1454-
}
1455-
}''')
1456-
assert ndvi.to_json() == expected
1459+
assert ndvi.to_json() == EXPECTED_JSON_EXPORT_S2_NDVI
14571460

14581461

14591462
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires 'insertion ordered' dicts from python3.6 or higher")
@@ -1465,6 +1468,22 @@ def test_to_json_compact(con100):
14651468
assert ndvi.to_json(indent=None, separators=(',', ':')) == expected
14661469

14671470

1471+
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires 'insertion ordered' dicts from python3.6 or higher")
1472+
def test_print_json_default(con100, capsys):
1473+
ndvi = con100.load_collection("S2").ndvi()
1474+
ndvi.print_json()
1475+
stdout, stderr = capsys.readouterr()
1476+
assert stdout == EXPECTED_JSON_EXPORT_S2_NDVI + "\n"
1477+
1478+
1479+
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires 'insertion ordered' dicts from python3.6 or higher")
1480+
def test_print_json_file(con100):
1481+
ndvi = con100.load_collection("S2").ndvi()
1482+
f = io.StringIO()
1483+
ndvi.print_json(file=f)
1484+
assert f.getvalue() == EXPECTED_JSON_EXPORT_S2_NDVI + "\n"
1485+
1486+
14681487
def test_sar_backscatter_defaults(con100):
14691488
cube = con100.load_collection("S2").sar_backscatter()
14701489
assert _get_leaf_node(cube) == {

0 commit comments

Comments
 (0)