Skip to content

Commit e435d72

Browse files
committed
Issue #278: refactor _pg logic in DataCube/VectorCube/MlModel to common base class
1 parent d0e4f2a commit e435d72

File tree

5 files changed

+117
-143
lines changed

5 files changed

+117
-143
lines changed

openeo/rest/_datacube.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
3+
.. data:: THIS
4+
5+
Symbolic reference to the current data cube, to be used as argument in
6+
:py:meth:`DataCube.process()` and related calls
7+
8+
"""
9+
import json
10+
import logging
11+
import typing
12+
from typing import Optional
13+
14+
from openeo.internal.graph_building import PGNode, _FromNodeMixin
15+
from openeo.util import legacy_alias
16+
17+
if hasattr(typing, 'TYPE_CHECKING') and typing.TYPE_CHECKING:
18+
# Imports for type checking only (circular import issue at runtime). `hasattr` is Python 3.5 workaround #210
19+
from openeo.rest.connection import Connection
20+
21+
log = logging.getLogger(__name__)
22+
23+
# Sentinel object to refer to "current" cube in chained cube processing expressions.
24+
THIS = object()
25+
26+
27+
class _ProcessGraphAbstraction(_FromNodeMixin):
28+
"""
29+
Base class for client-side abstractions/wrappers
30+
for structures that are represented by a openEO process graph:
31+
raster data cubes, vector cubes, ML models, ...
32+
"""
33+
34+
def __init__(self, pgnode: PGNode, connection: "Connection"):
35+
self._pg = pgnode
36+
self._connection = connection
37+
38+
def __str__(self):
39+
return "{t}({pg})".format(t=self.__class__.__name__, pg=self._pg)
40+
41+
def flat_graph(self) -> dict:
42+
"""
43+
Get the process graph in flat dict representation
44+
45+
.. note:: This method is mainly for internal use, subject to change and not recommended for general usage.
46+
Instead, use :py:meth:`to_json()` to get a JSON representation of the process graph.
47+
"""
48+
# TODO: wrap in {"process_graph":...} by default/optionally?
49+
return self._pg.flat_graph()
50+
51+
flatten = legacy_alias(flat_graph, name="flatten")
52+
53+
def to_json(self, indent=2, separators=None) -> str:
54+
"""
55+
Get JSON representation of (flat dict) process graph.
56+
"""
57+
pg = {"process_graph": self.flat_graph()}
58+
return json.dumps(pg, indent=indent, separators=separators)
59+
60+
@property
61+
def _api_version(self):
62+
return self._connection.capabilities().api_version_check
63+
64+
@property
65+
def connection(self) -> "Connection":
66+
return self._connection
67+
68+
def from_node(self):
69+
return self._pg
70+
71+
def _build_pgnode(self, process_id: str, arguments: dict, namespace: Optional[str], **kwargs) -> PGNode:
72+
"""
73+
Helper to build a PGNode from given argument dict and/or kwargs,
74+
and possibly resolving the `THIS` reference.
75+
"""
76+
arguments = {**(arguments or {}), **kwargs}
77+
for k, v in arguments.items():
78+
if v is THIS:
79+
arguments[k] = self
80+
# TODO: also necessary to traverse lists/dictionaries?
81+
return PGNode(process_id=process_id, arguments=arguments, namespace=namespace)

openeo/rest/datacube.py

+7-51
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727
import openeo.processes
2828
from openeo.api.process import Parameter
2929
from openeo.imagecollection import ImageCollection
30-
from openeo.internal.graph_building import PGNode, ReduceNode, _FromNodeMixin
30+
from openeo.internal.graph_building import PGNode, ReduceNode
3131
from openeo.metadata import CollectionMetadata, Band, BandDimension
3232
from openeo.processes import ProcessBuilder
3333
from openeo.rest import BandMathException, OperatorException, OpenEoClientException
34+
from openeo.rest._datacube import _ProcessGraphAbstraction, THIS
3435
from openeo.rest.job import RESTJob
3536
from openeo.rest.mlmodel import MlModel
3637
from openeo.rest.service import Service
@@ -47,11 +48,8 @@
4748

4849
log = logging.getLogger(__name__)
4950

50-
# Sentinel object to refer to "current" cube in chained cube processing expressions.
51-
THIS = object()
5251

53-
54-
class DataCube(_FromNodeMixin):
52+
class DataCube(_ProcessGraphAbstraction):
5553
"""
5654
Class representing a openEO Data Cube. Data loaded from the backend is returned as an object of this class.
5755
Various processing methods can be invoked to build a complete workflow.
@@ -61,14 +59,9 @@ class DataCube(_FromNodeMixin):
6159
"""
6260

6361
def __init__(self, graph: PGNode, connection: 'openeo.Connection', metadata: CollectionMetadata = None):
64-
# Process graph
65-
self._pg = graph
66-
self._connection = connection
62+
super().__init__(pgnode=graph, connection=connection)
6763
self.metadata = CollectionMetadata.get_or_create(metadata)
6864

69-
def __str__(self):
70-
return "DataCube({pg})".format(pg=self._pg)
71-
7265
@property
7366
@deprecated(reason="Use :py:meth:`DataCube.flat_graph()` instead.", version="0.9.0")
7467
def graph(self) -> dict:
@@ -80,37 +73,6 @@ def graph(self) -> dict:
8073
# TODO: is it feasible to just remove this property?
8174
return self.flat_graph()
8275

83-
def flat_graph(self) -> dict:
84-
"""
85-
Get the process graph in flat dict representation
86-
87-
.. note:: This method is mainly for internal use, subject to change and not recommended for general usage.
88-
Instead, use :py:meth:`DataCube.to_json()` to get a JSON representation of the process graph.
89-
"""
90-
# TODO: wrap in {"process_graph":...} by default/optionally?
91-
return self._pg.flat_graph()
92-
93-
flatten = legacy_alias(flat_graph, name="flatten")
94-
95-
def to_json(self, indent=2, separators=None) -> str:
96-
"""
97-
Get JSON representation of (flat dict) process graph.
98-
"""
99-
pg = {"process_graph": self.flat_graph()}
100-
return json.dumps(pg, indent=indent, separators=separators)
101-
102-
@property
103-
def _api_version(self):
104-
return self._connection.capabilities().api_version_check
105-
106-
@property
107-
def connection(self) -> 'openeo.Connection':
108-
return self._connection
109-
110-
def from_node(self) -> PGNode:
111-
# _FromNodeMixin API
112-
return self._pg
113-
11476
def process(
11577
self,
11678
process_id: str,
@@ -128,15 +90,8 @@ def process(
12890
:param namespace: optional: process namespace
12991
:return: new DataCube instance
13092
"""
131-
arguments = {**(arguments or {}), **kwargs}
132-
for k, v in arguments.items():
133-
if v is THIS:
134-
arguments[k] = self
135-
return self.process_with_node(PGNode(
136-
process_id=process_id,
137-
arguments=arguments,
138-
namespace=namespace,
139-
), metadata=metadata)
93+
pg = self._build_pgnode(process_id=process_id, arguments=arguments, namespace=namespace, **kwargs)
94+
return DataCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata)
14095

14196
graph_add_node = legacy_alias(process, "graph_add_node")
14297

@@ -150,6 +105,7 @@ def process_with_node(self, pg: PGNode, metadata: Optional[CollectionMetadata] =
150105
"""
151106
# TODO: deep copy `self.metadata` instead of using same instance?
152107
# TODO: cover more cases where metadata has to be altered
108+
# TODO: deprecate `process_with_node``: little added value over just calling DataCube() directly
153109
return DataCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata)
154110

155111
@classmethod

openeo/rest/mlmodel.py

+6-41
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,28 @@
1-
import json
1+
22
import pathlib
3-
from typing import Union, Optional
43
import typing
4+
from typing import Union, Optional
55

66
from openeo.internal.graph_building import PGNode
7+
from openeo.rest._datacube import _ProcessGraphAbstraction
78
from openeo.rest.job import RESTJob
8-
from openeo.util import legacy_alias
9-
109

1110
if hasattr(typing, 'TYPE_CHECKING') and typing.TYPE_CHECKING:
1211
# Imports for type checking only (circular import issue at runtime). `hasattr` is Python 3.5 workaround #210
1312
from openeo import Connection
1413

1514

16-
class MlModel:
15+
class MlModel(_ProcessGraphAbstraction):
1716
"""
1817
A machine learning model accompanied with STAC metadata, including the ml-model extension.
1918
"""
2019
def __init__(self, graph: PGNode, connection: 'Connection'):
21-
super().__init__()
22-
self._pg = graph
23-
self._connection = connection
24-
25-
def __str__(self):
26-
return "MlModel({pg})".format(pg=self._pg)
27-
28-
@property
29-
def graph(self) -> dict:
30-
"""Get the process graph in flat dict representation"""
31-
return self.flat_graph()
32-
33-
def flat_graph(self) -> dict:
34-
"""Get the process graph in flat dict representation"""
35-
return self._pg.flat_graph()
36-
37-
flatten = legacy_alias(flat_graph, name="flatten")
38-
39-
def to_json(self, indent=2, separators=None) -> str:
40-
"""
41-
Get JSON representation of (flat dict) process graph.
42-
"""
43-
pg = {"process_graph": self.flat_graph()}
44-
return json.dumps(pg, indent=indent, separators=separators)
45-
46-
@property
47-
def _api_version(self):
48-
return self._connection.capabilities().api_version_check
49-
50-
@property
51-
def connection(self):
52-
return self._connection
20+
super().__init__(pgnode=graph, connection=connection)
5321

5422
def save_ml_model(self, options: Optional[dict] = None):
5523
pgnode = PGNode(
5624
process_id="save_ml_model",
57-
arguments={
58-
"data": {'from_node': self._pg},
59-
"options": options or {}
60-
}
25+
arguments={"data": self, "options": options or {}}
6126
)
6227
return MlModel(graph=pgnode, connection=self._connection)
6328

openeo/rest/vectorcube.py

+22-50
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import json
22
import pathlib
3-
from typing import Union
3+
from typing import Union, Optional
44
import typing
55

6-
from openeo.internal.graph_building import PGNode
6+
from openeo.internal.graph_building import PGNode, _FromNodeMixin
77
from openeo.metadata import CollectionMetadata
8+
from openeo.rest._datacube import _ProcessGraphAbstraction, THIS
89
from openeo.rest.job import RESTJob
910
from openeo.util import legacy_alias
1011

11-
1212
if hasattr(typing, 'TYPE_CHECKING') and typing.TYPE_CHECKING:
1313
# Imports for type checking only (circular import issue at runtime). `hasattr` is Python 3.5 workaround #210
1414
from openeo import Connection
1515

1616

17-
class VectorCube:
17+
class VectorCube(_ProcessGraphAbstraction):
1818
"""
1919
A Vector Cube, or 'Vector Collection' is a data structure containing 'Features':
2020
https://www.w3.org/TR/sdw-bp/#dfn-feature
@@ -24,53 +24,26 @@ class VectorCube:
2424
"""
2525

2626
def __init__(self, graph: PGNode, connection: 'Connection', metadata: CollectionMetadata = None):
27-
super().__init__()
28-
# Process graph
29-
self._pg = graph
30-
self._connection = connection
27+
super().__init__(pgnode=graph, connection=connection)
28+
# TODO: does VectorCube need CollectionMetadata?
3129
self.metadata = metadata
3230

33-
def __str__(self):
34-
return "DataCube({pg})".format(pg=self._pg)
35-
36-
@property
37-
def graph(self) -> dict:
38-
"""Get the process graph in flat dict representation"""
39-
return self.flat_graph()
40-
41-
def flat_graph(self) -> dict:
42-
"""Get the process graph in flat dict representation"""
43-
return self._pg.flat_graph()
44-
45-
flatten = legacy_alias(flat_graph, name="flatten")
46-
47-
def to_json(self, indent=2, separators=None) -> str:
48-
"""
49-
Get JSON representation of (flat dict) process graph.
50-
"""
51-
pg = {"process_graph": self.flat_graph()}
52-
return json.dumps(pg, indent=indent, separators=separators)
53-
54-
@property
55-
def _api_version(self):
56-
return self._connection.capabilities().api_version_check
57-
58-
@property
59-
def connection(self):
60-
return self._connection
61-
62-
def process(self, process_id: str, args: dict = None, metadata: CollectionMetadata = None, **kwargs) -> 'VectorCube':
31+
def process(
32+
self,
33+
process_id: str,
34+
arguments: dict = None,
35+
metadata: Optional[CollectionMetadata] = None,
36+
namespace: Optional[str] = None,
37+
**kwargs) -> 'VectorCube':
6338
"""
6439
Generic helper to create a new DataCube by applying a process.
6540
6641
:param process_id: process id of the process.
6742
:param args: argument dictionary for the process.
6843
:return: new DataCube instance
6944
"""
70-
return self.process_with_node(PGNode(
71-
process_id=process_id,
72-
arguments=args, **kwargs
73-
), metadata=metadata)
45+
pg = self._build_pgnode(process_id=process_id, arguments=arguments, namespace=namespace, **kwargs)
46+
return VectorCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata)
7447

7548
def process_with_node(self, pg: PGNode, metadata: CollectionMetadata = None) -> 'VectorCube':
7649
"""
@@ -80,22 +53,21 @@ def process_with_node(self, pg: PGNode, metadata: CollectionMetadata = None) ->
8053
:param metadata: (optional) metadata to override original cube metadata (e.g. when reducing dimensions)
8154
:return: new DataCube instance
8255
"""
83-
from openeo.rest.datacube import DataCube, THIS
8456
arguments = pg.arguments
8557
for k, v in arguments.items():
86-
if isinstance(v, DataCube) or isinstance(v, VectorCube):
87-
arguments[k] = {"from_node": v._pg}
88-
elif v is THIS:
89-
arguments[k] = {"from_node": self._pg}
58+
# TODO: it's against intended flow to resolve THIS and _FromNodeMixin at this point (should be done before building PGNode)
59+
if v is THIS:
60+
v = self
61+
if isinstance(v, _FromNodeMixin):
62+
arguments[k] = {"from_node": v.from_node()}
9063
# TODO: deep copy `self.metadata` instead of using same instance?
91-
# TODO: cover more cases where metadata has to be altered
9264
return VectorCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata)
9365

9466
def save_result(self, format: str = "GeoJson", options: dict = None):
9567
return self.process(
9668
process_id="save_result",
97-
args={
98-
"data": {"from_node": self._pg},
69+
arguments={
70+
"data": self,
9971
"format": format,
10072
"options": options or {}
10173
}

tests/rest/datacube/test_vectorcube.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def test_raster_to_vector(con100):
99
vector_cube = img.raster_to_vector()
1010
vector_cube_tranformed = vector_cube.process_with_node(openeo.UDF("python source code", "Python"))
1111

12-
assert vector_cube_tranformed.graph == {
12+
assert vector_cube_tranformed.flat_graph() == {
1313
'loadcollection1': {
1414
'arguments': {
1515
'id': 'S2',

0 commit comments

Comments
 (0)