Skip to content

Commit 2ae9d47

Browse files
committed
add support for UDFs; remove invalid condense method from WCPSExpr
1 parent 837ae6c commit 2ae9d47

2 files changed

Lines changed: 107 additions & 62 deletions

File tree

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ from io import BytesIO
4646
Image.open(BytesIO(result.value)).show()
4747

4848
# alternatively, save the content of the response into a file
49-
service.download(query, output_file='vegetation.png')
49+
service.download(query, output_file='tci.png')
5050
```
5151

5252
## Band Math
@@ -282,6 +282,34 @@ plt.ylabel('Average')
282282
plt.show()
283283
```
284284

285+
## User-Defined Functions (UDF)
286+
287+
UDFs can be executed with the
288+
[Udf](https://rasdaman.github.io/wcps-python-client/autoapi/wcps/model/index.html#wcps.model.Udf)
289+
object:
290+
291+
```python
292+
from wcps.service import Service
293+
from wcps.model import Datacube, Udf
294+
295+
cov = Datacube("S2_L2A_32631_B04_10m")[
296+
"ansi" : "2021-04-09",
297+
"E" : 670000 : 680000,
298+
"N" : 4990220 : 5000220 ]
299+
300+
# Apply the image.stretch(cov) UDF to stretch the values of
301+
# cov in the [0-255] range, so it can be encoded in JPEG
302+
stretched = Udf('image.stretch', [cov]).encode("JPEG")
303+
304+
# execute the query on the server and get back a WCPSResult
305+
service = Service("https://ows.rasdaman.org/rasdaman/ows")
306+
result = service.execute(stretched)
307+
308+
# show the returned image
309+
from PIL import Image
310+
from io import BytesIO
311+
Image.open(BytesIO(result.value)).show()
312+
```
285313

286314
# Contributing
287315

wcps/model.py

Lines changed: 78 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
2. Chaining methods on :class:`WCPSExpr` objects, e.g. `Datacube("cube").sum()`
88
99
Each subclass defines the ``__str__`` method, so that executing
10-
``str(Sum(Datacube("cube"))`` returns a valid WCPS query string
10+
``str(Sum(Datacube("cube")))`` returns a valid WCPS query string
1111
that can be sent to a WCPS server.
1212
"""
1313

@@ -32,7 +32,7 @@ class WCPSExpr:
3232
an expression by chaining them, e.g
3333
``Sum(Datacube("cube1") + Datacube("cube2"))``
3434
is the same as
35-
``Datacube("cube1").add(Datacube("cube2").sum()``. Notable exceptions are :class:`Switch` and
35+
``Datacube("cube1").add(Datacube("cube2").sum())``. Notable exceptions are :class:`Switch` and
3636
:class:`Coverage`.
3737
3838
Various builtin operators are overloaded to allow writing expressions more naturally,
@@ -49,7 +49,7 @@ class WCPSExpr:
4949
"""
5050

5151
def __init__(self, operands: Optional[OperandType | list[OperandType]] = None):
52-
self.parent: WCPSExpr = None
52+
self.parent: Optional[WCPSExpr] = None
5353
"""
5454
A :class:`WCPSExpr` of which this expression is an operand; ``None`` if this is the root expression.
5555
E.g. in if this expression is the :class:`Datacube` object in ``Datacube("test") * 5``,
@@ -960,11 +960,11 @@ def subset(self, axes) -> Subset:
960960
:param axes: specifies a spatio-temporal subset as:
961961
962962
1. a single :class:`Axis` object: ``Axis(axis_name, low, high?, crs?)``
963-
2. a tuple of multiple :class:`Axis` objects: ``(Axis(..), Axis(..))``
963+
2. a tuple of multiple :class:`Axis` objects: ``(Axis(...), Axis(...))``
964964
3. a tuple specifying the axis subset in place: ``(axis_name, low, high?, crs?)``
965-
4. a tuple of axis subset tuples (see 3.): ``((axis_name, low, high?, crs?), (...), ..)``
966-
5. a list of :class:`Axis` objects: `[Axis(..), Axis(..), ..]`
967-
6. a list of axis subset tuples (see 3.): ``[(axis_name, low, high?, crs?), (...), ..]``
965+
4. a tuple of axis subset tuples (see 3.): ``((axis_name, low, high?, crs?), (...), ...)``
966+
5. a list of :class:`Axis` objects: `[Axis(...), Axis(...), ...]`
967+
6. a list of axis subset tuples (see 3.): ``[(axis_name, low, high?, crs?), (...), ...]``
968968
969969
:return: An instance of the :class:`Subset` class
970970
@@ -982,16 +982,16 @@ def subset(self, axes) -> Subset:
982982
def __getitem__(self, axes) -> Subset:
983983
"""
984984
Extract a spatio-temporal subset from this object as specified by the list of ``axes``
985-
with an index operator ``[..]``.
985+
with an index operator ``[...]``.
986986
987987
:param axes: specifies a spatio-temporal subset as:
988988
989989
1. a single :class:`Axis` object: ``Axis(axis_name, low, high?, crs?)``
990-
2. a tuple of multiple :class:`Axis` objects: ``(Axis(..), Axis(..))``
990+
2. a tuple of multiple :class:`Axis` objects: ``(Axis(...), Axis(...))``
991991
3. a tuple specifying the axis subset in place: ``(axis_name, low, high?, crs?)``
992-
4. a tuple of axis subset tuples (see 3.): ``((axis_name, low, high?, crs?), (...), ..)``
993-
5. a list of :class:`Axis` objects: `[Axis(..), Axis(..), ..]`
994-
6. a list of axis subset tuples (see 3.): ``[(axis_name, low, high?, crs?), (...), ..]``
992+
4. a tuple of axis subset tuples (see 3.): ``((axis_name, low, high?, crs?), (...), ...)``
993+
5. a list of :class:`Axis` objects: `[Axis(...), Axis(...), ...]`
994+
6. a list of axis subset tuples (see 3.): ``[(axis_name, low, high?, crs?), (...), ...]``
995995
996996
:return: An instance of the :class:`Subset` class
997997
@@ -1014,11 +1014,11 @@ def extend(self, axes) -> Extend:
10141014
:param axes: specifies a spatio-temporal subset as:
10151015
10161016
1. a single :class:`Axis` object: ``Axis(axis_name, low, high?, crs?)``
1017-
2. a tuple of multiple :class:`Axis` objects: ``(Axis(..), Axis(..))``
1017+
2. a tuple of multiple :class:`Axis` objects: ``(Axis(...), Axis(...))``
10181018
3. a tuple specifying the axis subset in place: ``(axis_name, low, high?, crs?)``
1019-
4. a tuple of axis subset tuples (see 3.): ``((axis_name, low, high?, crs?), (...), ..)``
1020-
5. a list of :class:`Axis` objects: `[Axis(..), Axis(..), ..]`
1021-
6. a list of axis subset tuples (see 3.): ``[(axis_name, low, high?, crs?), (...), ..]``
1019+
4. a tuple of axis subset tuples (see 3.): ``((axis_name, low, high?, crs?), (...), ...)``
1020+
5. a list of :class:`Axis` objects: `[Axis(...), Axis(...), ...]`
1021+
6. a list of axis subset tuples (see 3.): ``[(axis_name, low, high?, crs?), (...), ...]``
10221022
10231023
:return: An instance of the :class:`Subset` class
10241024
@@ -1081,6 +1081,8 @@ def reproject(self, target_crs: str, interpolation_method: str = None,
10811081
:param target_crs: the new CRS, e.g. "EPSG:4326"
10821082
:param interpolation_method: an optional interpolation method, one of the constants
10831083
defined by :class:`ResampleAlg`, e.g. :const:`ResampleAlg.BILINEAR`
1084+
:param axis_resolutions: optional list of target axis resolutions to
1085+
maintain in the reprojected result
10841086
:param axis_subsets: crop the result by the specified axis subsets (same syntax as for ``subset(axes)``)
10851087
:param domain_of_coverage: crop the result to the geo domain of another coverage object
10861088
@@ -1202,28 +1204,6 @@ def some(self) -> Some:
12021204
"""
12031205
return Some(self)
12041206

1205-
def condense(self, condense_op: CondenseOp) -> Condense:
1206-
"""
1207-
Condense the cell values of the current operand with the ``condense_op``.
1208-
Iterator variables can be specified with the ``over()`` method, filtering of values
1209-
with ``where()``, and the aggregation expression with ``using()``.
1210-
1211-
:param condense_op: a condense operator, one of the constants defined in
1212-
:class:`CondenseOp`, e.g. :const:`CondenseOp.PLUS`.
1213-
:return: An instance of the :class:`Condense` class; at least ``over()`` and the
1214-
``using()`` methods must be called subsequently on the returned value.
1215-
1216-
Examples:
1217-
1218-
```
1219-
cov = Datacube("testcube")
1220-
pt_var = AxisIter('$pt', 'time').of_grid_axis(cov)
1221-
pt_ref = pt_var.ref()
1222-
cov.condense(CondenseOp.PLUS).over(pt_var).where().where(cov["time", pt_ref] > 100).using(cov["time", pt_ref])
1223-
```
1224-
"""
1225-
return Condense(self, condense_op)
1226-
12271207
def encode(self, data_format: str = None, format_params: str = None) -> Encode:
12281208
"""
12291209
Encode a coverage to some ``data_format``. The data format must be specified
@@ -1884,7 +1864,7 @@ def __init__(self, bands: dict):
18841864

18851865
def __str__(self):
18861866
bands = [f'{k}: {v}' for k, v in self.bands.items()]
1887-
return f'{super().__str__()}{{{'; '.join(bands)}}}'
1867+
return f'{super().__str__()}{{{"; ".join(bands)}}}'
18881868

18891869

18901870
# ---------------------------------------------------------------------------------
@@ -1911,7 +1891,7 @@ def __str__(self):
19111891
ret += f':"{self.crs}"'
19121892
operands = [str(op) for op in self.operands]
19131893
operands = [op if op != '"*"' else '*' for op in operands]
1914-
ret += f'({':'.join(operands)})'
1894+
ret += f'({":".join(operands)})'
19151895
return ret
19161896

19171897
@staticmethod
@@ -1923,12 +1903,12 @@ def get_axis_list(axes: Union[Axis, slice, tuple[Axis], AxisTuple, tuple[AxisTup
19231903
19241904
- a single Axis, e.g. ``Axis("X", 0, 100.5, "EPSG:4326")``
19251905
- a single slice, e.g. ``"X":1``, or ``"X":1:15.3``
1926-
- a tuple of Axis objects, e.g. ``(Axis(..), Axis(..), ..)``
1906+
- a tuple of Axis objects, e.g. ``(Axis(...), Axis(...), ...)``
19271907
- a single in-place axis tuple, e.g. ``("X", 0, 100.5, "EPSG:4326")``
1928-
- a tuple of axis tuples, e.g. ``(("X", 0, 100.5), (..), ..)``
1908+
- a tuple of axis tuples, e.g. ``(("X", 0, 100.5), (...), ...)``
19291909
- a tuple of slices, e.g. ``("X":1, "Y":0:100.5)``
1930-
- a list of Axis objects, e.g. ``[Axis(..), Axis(..), ..]``
1931-
- a list of axis tuples, e.g. ``[("X", 0, 100.5), (..), ..]``
1910+
- a list of Axis objects, e.g. ``[Axis(...), Axis(...), ...]``
1911+
- a list of axis tuples, e.g. ``[("X", 0, 100.5), (...), ...]``
19321912
19331913
:raise: a :class:`WCPSClientException` in case ``axes`` is in invalid shape.
19341914
"""
@@ -1971,8 +1951,7 @@ def __init__(self, op: WCPSExpr, axes):
19711951
super().__init__(operands=[op] + Axis.get_axis_list(axes))
19721952

19731953
def __str__(self):
1974-
axis_subsets = [str(op) for op in self.operands[1:]]
1975-
axis_subsets_str = ', '.join(axis_subsets)
1954+
axis_subsets_str = _list_to_str(self.operands[1:], ', ')
19761955
return f'{super().__str__()}{self.operands[0]}[{axis_subsets_str}]'
19771956

19781957

@@ -1985,8 +1964,7 @@ def __init__(self, op: WCPSExpr, axes):
19851964
super().__init__(operands=[op] + Axis.get_axis_list(axes))
19861965

19871966
def __str__(self):
1988-
axis_subsets = [str(op) for op in self.operands[1:]]
1989-
axis_subsets_str = ', '.join(axis_subsets)
1967+
axis_subsets_str = _list_to_str(self.operands[1:], ', ')
19901968
return f'{super().__str__()}extend({self.operands[0]}, {{ {axis_subsets_str} }})'
19911969

19921970

@@ -2023,15 +2001,13 @@ def __str__(self):
20232001
ret = f'{super().__str__()}scale({self.operands[0]}, {{ '
20242002

20252003
if self.axis_subsets is not None:
2026-
axis_subsets = [str(op) for op in self.operands[1:]]
2027-
ret += ', '.join(axis_subsets)
2004+
ret += _list_to_str(self.operands[1:], ', ')
20282005
elif self.another_coverage is not None:
20292006
ret += f'imageCrsDomain({self.another_coverage})'
20302007
elif self.scale_factor is not None:
20312008
return f'{super().__str__()}scale({self.operands[0]}, {self.scale_factor})'
20322009
elif self.scale_factors is not None:
2033-
axis_subsets = [str(op) for op in self.operands[1:]]
2034-
ret += ', '.join(axis_subsets)
2010+
ret += _list_to_str(self.operands[1:], ', ')
20352011

20362012
ret += ' })'
20372013
return ret
@@ -2135,8 +2111,7 @@ def __str__(self):
21352111
axis_subsets_str = ', '.join(axis_subsets)
21362112
ret += f', {{ {axis_subsets_str} }}'
21372113
if self.axis_subsets is not None:
2138-
axis_subsets = [str(axis) for axis in self.axis_subsets]
2139-
axis_subsets_str = ', '.join(axis_subsets)
2114+
axis_subsets_str = _list_to_str(self.axis_subsets, ', ')
21402115
ret += f', {{ {axis_subsets_str} }}'
21412116
elif self.subset_domain is not None:
21422117
ret += f', {{ domain({self.subset_domain}) }}'
@@ -2526,7 +2501,7 @@ def __str__(self):
25262501
:raise: :class:`WCPSClientException` if no iterator variables or a using expression have been set.
25272502
"""
25282503
self._validate()
2529-
over = ', '.join(str(axis_iter) for axis_iter in self.iter_vars)
2504+
over = _list_to_str(self.iter_vars, ', ')
25302505
ret = f'{super().__str__()}(condense {self.condense_op} over {over}'
25312506
if self.where_where is not None:
25322507
ret += f' where {self.where_where}'
@@ -2640,7 +2615,7 @@ def __str__(self):
26402615
"""
26412616
self._validate()
26422617
ret = super().__str__()
2643-
over = ', '.join(str(axis_iter) for axis_iter in self.iter_vars)
2618+
over = _list_to_str(self.iter_vars, ', ')
26442619
ret += f'(coverage {self.name} over {over} values {self.values_clause})'
26452620
return ret
26462621

@@ -2775,6 +2750,34 @@ def default(self, default_expr: WCPSExpr) -> Switch:
27752750
return self
27762751

27772752

2753+
# ---------------------------------------------------------------------------------
2754+
# user-defined functions
2755+
2756+
class Udf(WCPSExpr):
2757+
"""
2758+
Execute a user-defined function (UDF), or any other WCPS function for which
2759+
no concrete class exists yet.
2760+
2761+
:param function_name: the UDF name, e.g. image.stretch
2762+
:param operands: a list of operands that the UDF expects
2763+
2764+
Examples: ::
2765+
2766+
stretch = Udf('stretch', Datacube('cov1'))
2767+
"""
2768+
2769+
def __init__(self, function_name: str, operands: list[OperandType]):
2770+
super().__init__(operands=operands)
2771+
self.function_name = function_name
2772+
2773+
def __str__(self):
2774+
ret = super().__str__()
2775+
ret += f'{self.function_name}('
2776+
ret += _list_to_str(self.operands, ', ')
2777+
ret += ')'
2778+
return ret
2779+
2780+
27782781
# ---------------------------------------------------------------------------------
27792782
# data encode/decode
27802783

@@ -2784,6 +2787,10 @@ class Encode(WCPSExpr):
27842787
with the :meth:`to` method if it isn't provided here. Format parameters can
27852788
be specified with the :meth:`params` method.
27862789
2790+
:param op: the coverage expression to encode.
2791+
:param data_format: the data format, e.g. "GTiff"
2792+
:param format_params: additional format parameters the influence the encoding
2793+
27872794
Examples:
27882795
27892796
- ``Encode(Datacube("test")).to("GTiff")``
@@ -2792,11 +2799,6 @@ class Encode(WCPSExpr):
27922799
"""
27932800

27942801
def __init__(self, op: WCPSExpr, data_format: str = None, format_params: str = None):
2795-
"""
2796-
:param op: the coverage expression to encode.
2797-
:param data_format: the data format, e.g. "GTiff"
2798-
:param format_params: additional format parameters the influence the encoding
2799-
"""
28002802
super().__init__(operands=[op])
28012803
self.data_format = data_format
28022804
self.format_params = format_params
@@ -2833,3 +2835,18 @@ class WCPSClientException(Exception):
28332835
"""
28342836
An exception thrown by this library.
28352837
"""
2838+
2839+
2840+
def _list_to_str(lst: list, sep: str) -> str:
2841+
"""
2842+
Convert a list of items into a single string. Each item is converted to a string
2843+
and separated by a specified separator in the result.
2844+
2845+
:param lst: The list of items to be joined into a string. Each item in the list
2846+
will be converted to a string before joining.
2847+
:param sep: The separator to use between each item in the resulting string.
2848+
2849+
:return: A single string containing all items from the list, separated by the
2850+
specified separator.
2851+
"""
2852+
return sep.join([str(item) for item in lst])

0 commit comments

Comments
 (0)