datacube-ows 1.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. datacube_ows/__init__.py +12 -0
  2. datacube_ows/_version.py +21 -0
  3. datacube_ows/band_utils.py +241 -0
  4. datacube_ows/cfg_parser_impl.py +388 -0
  5. datacube_ows/config_toolkit.py +52 -0
  6. datacube_ows/config_utils.py +1056 -0
  7. datacube_ows/data.py +281 -0
  8. datacube_ows/feature_info.py +325 -0
  9. datacube_ows/gunicorn_config.py +17 -0
  10. datacube_ows/http_utils.py +136 -0
  11. datacube_ows/index/__init__.py +10 -0
  12. datacube_ows/index/api.py +214 -0
  13. datacube_ows/index/driver.py +55 -0
  14. datacube_ows/index/postgis/__init__.py +5 -0
  15. datacube_ows/index/postgis/api.py +198 -0
  16. datacube_ows/index/postgis/product_ranges.py +281 -0
  17. datacube_ows/index/postgres/__init__.py +5 -0
  18. datacube_ows/index/postgres/api.py +154 -0
  19. datacube_ows/index/postgres/mv_index.py +181 -0
  20. datacube_ows/index/postgres/product_ranges.py +244 -0
  21. datacube_ows/index/sql.py +109 -0
  22. datacube_ows/legend_generator.py +60 -0
  23. datacube_ows/legend_utils.py +33 -0
  24. datacube_ows/loading.py +434 -0
  25. datacube_ows/ogc.py +262 -0
  26. datacube_ows/ogc_exceptions.py +123 -0
  27. datacube_ows/ogc_utils.py +258 -0
  28. datacube_ows/ows_cfg_example.py +2104 -0
  29. datacube_ows/ows_configuration.py +1616 -0
  30. datacube_ows/protocol_versions.py +101 -0
  31. datacube_ows/query_profiler.py +44 -0
  32. datacube_ows/resource_limits.py +292 -0
  33. datacube_ows/sql/postgis/ows_schema/create/001_create_schema.sql +3 -0
  34. datacube_ows/sql/postgis/ows_schema/create/002_create_product_rng.sql +17 -0
  35. datacube_ows/sql/postgis/ows_schema/grants/read_only/001_grant_usage_requires_role.sql +3 -0
  36. datacube_ows/sql/postgis/ows_schema/grants/read_only/002_grant_range_read_requires_role.sql +3 -0
  37. datacube_ows/sql/postgis/ows_schema/grants/read_only/003_grant_odc_user_requires_role.sql +3 -0
  38. datacube_ows/sql/postgis/ows_schema/grants/read_write/001_grant_usage_requires_role.sql +3 -0
  39. datacube_ows/sql/postgis/ows_schema/grants/read_write/002_grant_writetables_requires_role.sql +3 -0
  40. datacube_ows/sql/postgis/ows_schema/grants/read_write/003_grant_odc_user_requires_role.sql +3 -0
  41. datacube_ows/sql/postgres/extent_views/create/001_postgis_extension.sql +3 -0
  42. datacube_ows/sql/postgres/extent_views/create/002_timezone.sql +3 -0
  43. datacube_ows/sql/postgres/extent_views/create/003_create_view_owner_role_ignore_duplicates.sql +3 -0
  44. datacube_ows/sql/postgres/extent_views/create/004_grant_agdc_user_to_view_owner_role.sql +3 -0
  45. datacube_ows/sql/postgres/extent_views/create/005_ows_read_to_view_owner_role.sql +3 -0
  46. datacube_ows/sql/postgres/extent_views/create/006_odc_read_to_view_owner_role.sql +3 -0
  47. datacube_ows/sql/postgres/extent_views/create/010_create_new_time_view.sql +46 -0
  48. datacube_ows/sql/postgres/extent_views/create/011_create_new_space_view.sql +102 -0
  49. datacube_ows/sql/postgres/extent_views/create/012_create_new_spacetime_view.sql +5 -0
  50. datacube_ows/sql/postgres/extent_views/create/020_create_index_1.sql +5 -0
  51. datacube_ows/sql/postgres/extent_views/create/021_create_index_2.sql +5 -0
  52. datacube_ows/sql/postgres/extent_views/create/022_create_index_3.sql +5 -0
  53. datacube_ows/sql/postgres/extent_views/create/023_create_index_4.sql +5 -0
  54. datacube_ows/sql/postgres/extent_views/create/030_rename_old_space_time_view.sql +4 -0
  55. datacube_ows/sql/postgres/extent_views/create/031_rename_new_space_time_view.sql +4 -0
  56. datacube_ows/sql/postgres/extent_views/create/032_drop_old_space_time_view.sql +3 -0
  57. datacube_ows/sql/postgres/extent_views/create/040_drop_old_space_view.sql +3 -0
  58. datacube_ows/sql/postgres/extent_views/create/041_drop_old_time_view.sql +3 -0
  59. datacube_ows/sql/postgres/extent_views/create/042_rename_new_space_view.sql +4 -0
  60. datacube_ows/sql/postgres/extent_views/create/043_rename_new_time_view.sql +4 -0
  61. datacube_ows/sql/postgres/extent_views/create/050_rename_index_1.sql +4 -0
  62. datacube_ows/sql/postgres/extent_views/create/051_rename_index_2.sql +4 -0
  63. datacube_ows/sql/postgres/extent_views/create/052_rename_index_3.sql +4 -0
  64. datacube_ows/sql/postgres/extent_views/create/053_rename_index_4.sql +4 -0
  65. datacube_ows/sql/postgres/extent_views/grants/read_only/001_grant_read_requires_role.sql +3 -0
  66. datacube_ows/sql/postgres/extent_views/grants/refresh_owner/001_grant_space_view_perms_to_owner_role.sql +3 -0
  67. datacube_ows/sql/postgres/extent_views/grants/refresh_owner/010_set_owner_time_view.sql +3 -0
  68. datacube_ows/sql/postgres/extent_views/grants/refresh_owner/011_set_owner_space_view.sql +3 -0
  69. datacube_ows/sql/postgres/extent_views/grants/refresh_owner/012_set_owner_spacetime_view.sql +3 -0
  70. datacube_ows/sql/postgres/extent_views/grants/write_refresh/001_grant_refresh_requires_role.sql +3 -0
  71. datacube_ows/sql/postgres/extent_views/refresh/001_timezone.sql +3 -0
  72. datacube_ows/sql/postgres/extent_views/refresh/002_refresh_time.sql +3 -0
  73. datacube_ows/sql/postgres/extent_views/refresh/003_refresh_space.sql +3 -0
  74. datacube_ows/sql/postgres/extent_views/refresh/004_refresh_spacetime.sql +3 -0
  75. datacube_ows/sql/postgres/ows_schema/cleanup/001_drop_space_time_view.sql +3 -0
  76. datacube_ows/sql/postgres/ows_schema/cleanup/002_drop_time_view.sql +3 -0
  77. datacube_ows/sql/postgres/ows_schema/cleanup/003_drop_space_view.sql +3 -0
  78. datacube_ows/sql/postgres/ows_schema/cleanup/010_drop_subproduct_range.sql +3 -0
  79. datacube_ows/sql/postgres/ows_schema/cleanup/011_drop_multiproduct_range.sql +3 -0
  80. datacube_ows/sql/postgres/ows_schema/cleanup/012_drop_product_range.sql +3 -0
  81. datacube_ows/sql/postgres/ows_schema/create/001_create_schema.sql +3 -0
  82. datacube_ows/sql/postgres/ows_schema/create/002_create_product_rng.sql +17 -0
  83. datacube_ows/sql/postgres/ows_schema/grants/read_only/001_grant_usage_requires_role.sql +3 -0
  84. datacube_ows/sql/postgres/ows_schema/grants/read_only/002_grant_range_read_requires_role.sql +3 -0
  85. datacube_ows/sql/postgres/ows_schema/grants/read_only/003_grant_agdc_user_requires_role.sql +3 -0
  86. datacube_ows/sql/postgres/ows_schema/grants/read_write/001_grant_usage_requires_role.sql +3 -0
  87. datacube_ows/sql/postgres/ows_schema/grants/read_write/002_grant_writetables_requires_role.sql +3 -0
  88. datacube_ows/sql/postgres/ows_schema/grants/read_write/003_grant_agdc_user_requires_role.sql +3 -0
  89. datacube_ows/startup_utils.py +244 -0
  90. datacube_ows/styles/__init__.py +13 -0
  91. datacube_ows/styles/api/__init__.py +14 -0
  92. datacube_ows/styles/api/base.py +200 -0
  93. datacube_ows/styles/base.py +667 -0
  94. datacube_ows/styles/colormap.py +446 -0
  95. datacube_ows/styles/component.py +182 -0
  96. datacube_ows/styles/expression.py +141 -0
  97. datacube_ows/styles/hybrid.py +88 -0
  98. datacube_ows/styles/ramp.py +587 -0
  99. datacube_ows/templates/html_feature_info.html +63 -0
  100. datacube_ows/templates/index.html +118 -0
  101. datacube_ows/templates/ogc_error.xml +26 -0
  102. datacube_ows/templates/ping.html +19 -0
  103. datacube_ows/templates/test_client.html +88 -0
  104. datacube_ows/templates/wcs_capabilities.xml +133 -0
  105. datacube_ows/templates/wcs_desc_coverage.xml +131 -0
  106. datacube_ows/templates/wms_capabilities.xml +298 -0
  107. datacube_ows/templates/wmts_capabilities.xml +234 -0
  108. datacube_ows/tile_matrix_sets.py +149 -0
  109. datacube_ows/time_utils.py +196 -0
  110. datacube_ows/update_ranges_impl.py +201 -0
  111. datacube_ows/utils.py +173 -0
  112. datacube_ows/wcs1.py +130 -0
  113. datacube_ows/wcs1_utils.py +472 -0
  114. datacube_ows/wcs2.py +311 -0
  115. datacube_ows/wcs2_utils.py +407 -0
  116. datacube_ows/wcs_scaler.py +254 -0
  117. datacube_ows/wcs_utils.py +23 -0
  118. datacube_ows/wms.py +56 -0
  119. datacube_ows/wms_utils.py +523 -0
  120. datacube_ows/wmts.py +205 -0
  121. datacube_ows/wsgi.py +36 -0
  122. datacube_ows-1.9.1.dist-info/METADATA +150 -0
  123. datacube_ows-1.9.1.dist-info/RECORD +166 -0
  124. datacube_ows-1.9.1.dist-info/WHEEL +5 -0
  125. datacube_ows-1.9.1.dist-info/entry_points.txt +8 -0
  126. datacube_ows-1.9.1.dist-info/licenses/LICENSE +14 -0
  127. datacube_ows-1.9.1.dist-info/top_level.txt +3 -0
  128. hack_cfg/__init__.py +0 -0
  129. hack_cfg/ows_cfg_local.py +6824 -0
  130. local_cfg/__init__.py +7 -0
  131. local_cfg/africa_ls_sr_local_cfg/__init__.py +7 -0
  132. local_cfg/africa_ls_sr_local_cfg/bands.py +90 -0
  133. local_cfg/africa_ls_sr_local_cfg/layers.py +130 -0
  134. local_cfg/africa_ls_sr_local_cfg/styles.py +1152 -0
  135. local_cfg/africa_s2_local_cfg/__init__.py +7 -0
  136. local_cfg/africa_s2_local_cfg/bands.py +95 -0
  137. local_cfg/africa_s2_local_cfg/lambdas.py +20 -0
  138. local_cfg/africa_s2_local_cfg/layers.py +41 -0
  139. local_cfg/africa_s2_local_cfg/styles.py +1130 -0
  140. local_cfg/c3_local_cfg/__init__.py +23 -0
  141. local_cfg/c3_local_cfg/bands.py +53 -0
  142. local_cfg/c3_local_cfg/layers.py +229 -0
  143. local_cfg/c3_local_cfg/legends.py +119 -0
  144. local_cfg/c3_local_cfg/resource_limits.py +33 -0
  145. local_cfg/c3_local_cfg/styles.py +735 -0
  146. local_cfg/c3_mosaic_cfg/__init__.py +7 -0
  147. local_cfg/c3_mosaic_cfg/utils.py +16 -0
  148. local_cfg/dep_utils.py +9 -0
  149. local_cfg/lccs_local_cfg/__init__.py +7 -0
  150. local_cfg/lccs_local_cfg/lc_class_desc.py +155 -0
  151. local_cfg/lccs_local_cfg/lccs_cfg.py +681 -0
  152. local_cfg/lccs_local_cfg/lccs_delta.py +19 -0
  153. local_cfg/ows_cfg_africa.py +1458 -0
  154. local_cfg/ows_cfg_local.py +7981 -0
  155. local_cfg/ows_cfg_local_backup_20200821.py +7267 -0
  156. local_cfg/ows_cfg_local_geomedian.py +1341 -0
  157. local_cfg/ows_cfg_local_inherit.py +7240 -0
  158. local_cfg/ows_cfg_stripped.py +369 -0
  159. local_cfg/ows_pacific_cfg.py +1118 -0
  160. local_cfg/s2_c3_local_cfg/__init__.py +7 -0
  161. local_cfg/s2_c3_local_cfg/bands.py +29 -0
  162. local_cfg/s2_c3_local_cfg/layers.py +131 -0
  163. local_cfg/s2_c3_local_cfg/pure_styles.py +130 -0
  164. local_cfg/s2_c3_local_cfg/resource_limits.py +19 -0
  165. local_cfg/s2_c3_local_cfg/styles.py +688 -0
  166. local_cfg/s2_c3_local_cfg/utils.py +19 -0
@@ -0,0 +1,12 @@
1
+ # This file is part of datacube-ows, part of the Open Data Cube project.
2
+ # See https://opendatacube.org for more information.
3
+ #
4
+ # Copyright (c) 2017-2024 OWS Contributors
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ try:
8
+ from ._version import version as __version__
9
+ except ImportError:
10
+ # Default version number.
11
+ # Will only be used when running datacube-ows direct from source code (not properly installed)
12
+ __version__ = "1.9.1?"
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '1.9.1'
21
+ __version_tuple__ = version_tuple = (1, 9, 1)
@@ -0,0 +1,241 @@
1
+ # This file is part of datacube-ows, part of the Open Data Cube project.
2
+ # See https://opendatacube.org for more information.
3
+ #
4
+ # Copyright (c) 2017-2024 OWS Contributors
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ import numpy
8
+
9
+ # Style index functions
10
+
11
+
12
+ def scale_data(imgband_data, scale_from, scale_to):
13
+ sc_min, sc_max = scale_from
14
+ tc_min, tc_max = scale_to
15
+ clipped = imgband_data.clip(sc_min, sc_max)
16
+ normalised = (clipped - sc_min) / (sc_max - sc_min)
17
+ scaled = normalised * (tc_max - tc_min)
18
+ return scaled + tc_min
19
+
20
+
21
+ def scalable(undecorated):
22
+ def decorated(*args, **kwargs):
23
+ scale_from = kwargs.pop("scale_from", None)
24
+ scale_to = kwargs.pop("scale_to", None)
25
+ if scale_from is not None and scale_to is None:
26
+ scale_to = [0, 255]
27
+ unscaled = undecorated(*args, **kwargs)
28
+ if scale_from:
29
+ return scale_data(unscaled, scale_from, scale_to)
30
+ return unscaled
31
+
32
+ return decorated
33
+
34
+
35
+ def band_modulator(undecorated):
36
+ def decorated(data, *args, **kwargs):
37
+ band_mapper = kwargs.get("band_mapper", None)
38
+ mult_band = kwargs.pop("mult_band", None)
39
+ raw_data = undecorated(data, *args, **kwargs)
40
+ if mult_band and band_mapper:
41
+ mult_band = band_mapper(mult_band)
42
+ if mult_band:
43
+ return data[mult_band] * raw_data
44
+ return raw_data
45
+ return decorated
46
+
47
+ def pre_scaled_band(data, band, scale, offset):
48
+ # Pre-scale a band as `data[band] * scale + offset`
49
+ return data[band] * scale + offset
50
+
51
+ def sum_bands(data, band1, band2, band_mapper=None):
52
+ if band_mapper:
53
+ band1 = band_mapper(band1)
54
+ band2 = band_mapper(band2)
55
+ return data[band1] + data[band2]
56
+
57
+ def pre_scaled_sum_bands(
58
+ data, band1, band2,
59
+ scale1=1.0, offset1=0.0,
60
+ scale2=1.0, offset2=0.0,
61
+ band_mapper=None):
62
+ # Calculate the sum of two bands, after pre-scaling them with a scale and offset
63
+ if band_mapper:
64
+ band1 = band_mapper(band1)
65
+ band2 = band_mapper(band2)
66
+ # Pre-scaled bands data
67
+ data1 = pre_scaled_band(data, band1, scale1, offset1)
68
+ data2 = pre_scaled_band(data, band2, scale2, offset2)
69
+ return data1 + data2
70
+
71
+
72
+ def delta_bands(data, band1, band2, band_mapper=None):
73
+ if band_mapper:
74
+ band1 = band_mapper(band1)
75
+ band2 = band_mapper(band2)
76
+ typ1 = data[band1].dtype
77
+ typ2 = data[band2].dtype
78
+ if typ1.name.startswith('uint'):
79
+ nodata = data[band1].nodata
80
+ data[band1] = data[band1].astype('int32')
81
+ data[band1].attrs["nodata"] = nodata
82
+ if typ2.name.startswith('uint'):
83
+ nodata = data[band2].nodata
84
+ data[band2] = data[band2].astype('int32')
85
+ data[band2].attrs["nodata"] = nodata
86
+ # if typ1.name.startswith('uint') or typ2.name.startswith('uint'):
87
+ # data = data.astype('int32', copy=False)
88
+ return data[band1] - data[band2]
89
+
90
+ def pre_scaled_delta_bands(
91
+ data, band1, band2,
92
+ scale1=1.0, offset1=0.0,
93
+ scale2=1.0, offset2=0.0,
94
+ band_mapper=None):
95
+ # Calculate the difference between two bands, after pre-scaling them with a scale
96
+ # and offset
97
+ if band_mapper:
98
+ band1 = band_mapper(band1)
99
+ band2 = band_mapper(band2)
100
+ typ1 = data[band1].dtype
101
+ typ2 = data[band2].dtype
102
+ if typ1.name.startswith('uint'):
103
+ nodata = data[band1].nodata
104
+ data[band1] = data[band1].astype('int32')
105
+ data[band1].attrs["nodata"] = nodata
106
+ if typ2.name.startswith('uint'):
107
+ nodata = data[band2].nodata
108
+ data[band2] = data[band2].astype('int32')
109
+ data[band2].attrs["nodata"] = nodata
110
+ # if typ1.name.startswith('uint') or typ2.name.startswith('uint'):
111
+ # data = data.astype('int32', copy=False)
112
+ # Pre-scaled bands data
113
+ data1 = pre_scaled_band(data, band1, scale1, offset1)
114
+ data2 = pre_scaled_band(data, band2, scale2, offset2)
115
+ return data1 - data2
116
+
117
+ # N.B. Modifying scale_to would be dangerous - don't do it.
118
+ # pylint: disable=dangerous-default-value
119
+ @scalable
120
+ def norm_diff(data, band1, band2, band_mapper=None):
121
+ # Calculate a normalised difference index.
122
+ return delta_bands(data, band1, band2, band_mapper) / sum_bands(data, band1, band2, band_mapper)
123
+
124
+ @scalable
125
+ def pre_scaled_norm_diff(data, band1, band2,
126
+ scale1=1.0, offset1=0.0,
127
+ scale2=1.0, offset2=0.0,
128
+ band_mapper=None,):
129
+ # Calculate a normalised difference index, after scaling the input bands with a
130
+ # scale and offset
131
+ return (pre_scaled_delta_bands(
132
+ data, band1, band2, scale1, offset1, scale2, offset2, band_mapper) /
133
+ pre_scaled_sum_bands(
134
+ data, band1, band2, scale1, offset1, scale2, offset2, band_mapper)
135
+ )
136
+ @scalable
137
+ def constant(data, band, const, band_mapper=None):
138
+ # Covert an xarray for a flat constant.
139
+ # Useful for displaying mask extents as a flat colour and debugging.
140
+ # params is assumed to be a tuple containing a constant value and a band name/alias.
141
+
142
+ if band_mapper:
143
+ band = band_mapper(band)
144
+ return data[band] * 0.0 + const
145
+
146
+
147
+ @scalable
148
+ def single_band(data, band, band_mapper=None):
149
+ # Use the raw value of a band directly as the index function.
150
+
151
+ if band_mapper:
152
+ band = band_mapper(band)
153
+ return data[band]
154
+
155
+
156
+ @scalable
157
+ def band_quotient(data, band1, band2, band_mapper=None):
158
+ if band_mapper:
159
+ band1 = band_mapper(band1)
160
+ band2 = band_mapper(band2)
161
+ return data[band1] / data[band2]
162
+
163
+
164
+ @scalable
165
+ def band_quotient_sum(data, band1a, band1b, band2a, band2b, band_mapper=None):
166
+ return band_quotient(data, band1a, band1b, band_mapper) + band_quotient(data, band2a, band2b, band_mapper)
167
+
168
+
169
+ @scalable
170
+ def sentinel2_ndci(data, b_red_edge, b_red, b_green, b_swir, band_mapper=None):
171
+ red_delta = delta_bands(data, b_red_edge, b_red, band_mapper)
172
+ red_sum = sum_bands(data, b_red_edge, b_red, band_mapper)
173
+ mndwi = norm_diff(data, b_green, b_swir, band_mapper)
174
+
175
+ return red_delta / red_sum.where(mndwi > 0.1)
176
+
177
+
178
+ def multi_date_delta(data, time_direction=-1):
179
+ data1, data2 = (data.sel(time=dt) for dt in data.coords["time"].values)
180
+
181
+ # data1, data2 = data.values.item(0), data.values.item(1)
182
+ if time_direction >= 0:
183
+ return data1 - data2
184
+ else:
185
+ return data2 - data1
186
+
187
+ def multi_date_pass(data):
188
+ return data
189
+
190
+ def multi_date_raw_example(data, band1, band2, band_mapper=None):
191
+ if band_mapper:
192
+ band1 = band_mapper(band1)
193
+ band2 = band_mapper(band2)
194
+ data1, data2 = (data.sel(time=dt) for dt in data.coords["time"].values)
195
+ return (
196
+ (data2[band1] - data1[band1]) - (data2[band2] - data1[band2])
197
+ ) / (
198
+ data1[band1] + data1[band2] + data2[band1] + data2[band2]
199
+ )
200
+
201
+
202
+ @band_modulator
203
+ @scalable
204
+ def single_band_log(data, band, scale_factor, exponent, band_mapper=None):
205
+ if band_mapper:
206
+ band = band_mapper(band)
207
+ d = data[band]
208
+ return scale_factor * ((d ** exponent) - 1.0)
209
+
210
+
211
+ @band_modulator
212
+ @scalable
213
+ def single_band_arcsec(data, band, band_mapper=None):
214
+ if band_mapper:
215
+ band = band_mapper(band)
216
+ d = data[band]
217
+ return numpy.arccos(1 / (d + 1))
218
+
219
+
220
+ @band_modulator
221
+ @scalable
222
+ def single_band_offset_log(data, band, scale=1.0, offset=None, band_mapper=None):
223
+ if band_mapper:
224
+ band = band_mapper(band)
225
+ d = data[band]
226
+ if offset is not None:
227
+ d = data[band] * scale + offset
228
+ unscaled = numpy.log(d)
229
+ else:
230
+ unscaled = numpy.log1p(d * scale)
231
+ return unscaled
232
+
233
+
234
+ @scalable
235
+ def radar_vegetation_index(data, band_hv, band_hh, band_mapper=None):
236
+ if band_mapper:
237
+ band_hv = band_mapper(band_hv)
238
+ band_hh = band_mapper(band_hh)
239
+ hv_sq = data[band_hv] * data[band_hv]
240
+ hh_sq = data[band_hh] * data[band_hh]
241
+ return (hv_sq * 4.0) / (hh_sq + hv_sq)
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env python3
2
+ # This file is part of datacube-ows, part of the Open Data Cube project.
3
+ # See https://opendatacube.org for more information.
4
+ #
5
+ # Copyright (c) 2017-2024 OWS Contributors
6
+ # SPDX-License-Identifier: Apache-2.0
7
+
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ from typing import cast
13
+
14
+ import click
15
+ from babel.messages.pofile import write_po
16
+ from datacube import Datacube
17
+ from deepdiff import DeepDiff
18
+
19
+ from datacube_ows import __version__
20
+ from datacube_ows.config_utils import ConfigException
21
+ from datacube_ows.ows_configuration import (OWSConfig, OWSFolder, OWSLayer,
22
+ OWSNamedLayer, read_config)
23
+
24
+
25
+ @click.group(invoke_without_command=True)
26
+ @click.option(
27
+ "--version", is_flag=True, default=False, help="Show OWS version number and exit"
28
+ )
29
+ def main(version: bool) -> int:
30
+ # --version
31
+ if version:
32
+ click.echo(f"Open Data Cube Open Web Services (datacube-ows) version {__version__}")
33
+ return 0
34
+
35
+
36
+ @main.command()
37
+ @click.argument("paths", nargs=-1)
38
+ @click.option(
39
+ "-p",
40
+ "--parse-only",
41
+ is_flag=True,
42
+ default=False,
43
+ help="Only parse the syntax of the config file - do not validate against database",
44
+ )
45
+ @click.option(
46
+ "-f",
47
+ "--folders",
48
+ is_flag=True,
49
+ default=False,
50
+ help="Print the folder/layer heirarchy(ies) to stdout.",
51
+ )
52
+ @click.option(
53
+ "-s",
54
+ "--styles",
55
+ is_flag=True,
56
+ default=False,
57
+ help="Print the styles for each layer to stdout (format depends on --folders flag).",
58
+ )
59
+ @click.option(
60
+ "-i",
61
+ "--input-file",
62
+ help="Provide a file path for the input inventory json file to be compared with config file",
63
+ )
64
+ @click.option(
65
+ "-o",
66
+ "--output-file",
67
+ help="Provide an output inventory file name with extension .json",
68
+ )
69
+ def check(parse_only: bool, folders: bool, styles: bool, input_file: str, output_file: str, paths: list[str]) -> int:
70
+ """Check configuration files
71
+
72
+ Takes a list of configuration specifications which are each loaded and validated in turn,
73
+ with each specification being interpreted as per the $DATACUBE_OWS_CFG environment variable.
74
+
75
+ If no specification is provided, the $DATACUBE_OWS_CFG environment variable is used.
76
+ """
77
+ if parse_only and (folders or styles):
78
+ click.echo(
79
+ "The --folders (-f) and --styles (-s) flags cannot be used in conjunction with the --parser-only (-p) flag."
80
+ )
81
+ sys.exit(1)
82
+ all_ok = True
83
+ if not paths:
84
+ if parse_path(None, parse_only, folders, styles, input_file, output_file):
85
+ return 0
86
+ else:
87
+ sys.exit(1)
88
+ for path in paths:
89
+ if not parse_path(path, parse_only, folders, styles, input_file, output_file):
90
+ all_ok = False
91
+
92
+ if not all_ok:
93
+ sys.exit(1)
94
+ return 0
95
+
96
+
97
+ def parse_path(path: str | None, parse_only: bool, folders: bool, styles: bool, input_file: str, output_file: str) -> bool:
98
+ try:
99
+ raw_cfg = read_config(path)
100
+ cfg = OWSConfig(refresh=True, cfg=raw_cfg)
101
+ if not parse_only:
102
+ with Datacube() as dc:
103
+ cfg.make_ready(dc)
104
+ except ConfigException as e:
105
+ click.echo(f"Config exception for path {str(e)}")
106
+ return False
107
+ click.echo("Configuration parsed OK")
108
+ click.echo(f"Configured message file location: {cfg.msg_file_name}")
109
+ click.echo(f"Configured translations directory location: {cfg.translations_dir}")
110
+ if folders:
111
+ click.echo()
112
+ click.echo("Folder/Layer Hierarchy")
113
+ click.echo("======================")
114
+ print_layers(cfg.layers, styles, depth=0)
115
+ click.echo()
116
+ elif styles:
117
+ click.echo()
118
+ click.echo("Layers and Styles")
119
+ click.echo("=================")
120
+ for lyr in cfg.layer_index.values():
121
+ click.echo(f"{lyr.name} [{','.join(lyr.product_names)}]")
122
+ print_styles(lyr)
123
+ click.echo()
124
+ if input_file or output_file:
125
+ layers_report(cfg.layer_index, input_file, output_file)
126
+ return True
127
+
128
+
129
+ @main.command()
130
+ @click.option(
131
+ "-c",
132
+ "--cfg-only",
133
+ is_flag=True,
134
+ default=False,
135
+ help="Read metadata from config only - ignore configured metadata message file.",
136
+ )
137
+ @click.option(
138
+ "-m",
139
+ "--msg-file",
140
+ default="messages.po",
141
+ help="Write to a message file with the translatable metadata from the configuration. (Defaults to 'messages.po')"
142
+ )
143
+ @click.argument("path", nargs=1, required=False)
144
+ def extract(path: str, cfg_only: bool, msg_file: str) -> int:
145
+ """Extract metadata from existing configuration into a message file template.
146
+
147
+ Takes a configuration specification which is loaded as per the $DATACUBE_OWS_CFG environment variable.
148
+
149
+ If no specification is provided, the $DATACUBE_OWS_CFG environment variable is used.
150
+ """
151
+ try:
152
+ raw_cfg = read_config(path)
153
+ cfg = OWSConfig(refresh=True, cfg=raw_cfg, ignore_msgfile=cfg_only)
154
+ with Datacube() as dc:
155
+ cfg.make_ready(dc)
156
+ except ConfigException as e:
157
+ click.echo(f"Config exception for path {str(e)}")
158
+ return False
159
+ click.echo("Configuration parsed OK")
160
+ click.echo(f"Configured message file location: {cfg.msg_file_name}")
161
+ click.echo(f"Configured translations directory location: {cfg.translations_dir}")
162
+ write_msg_file(msg_file, cfg)
163
+ click.echo(f"Message file {msg_file} written")
164
+ return 0
165
+
166
+
167
+ @main.command()
168
+ @click.option(
169
+ "-n",
170
+ "--new",
171
+ is_flag=True,
172
+ default=False,
173
+ help="Create a new translation template. (Default is to update an existing one.)"
174
+ )
175
+ @click.option(
176
+ "-m",
177
+ "--msg-file",
178
+ default=None,
179
+ help="Use this message file as the template for translation files. (defaults to message filename from configuration)"
180
+ )
181
+ @click.option(
182
+ "-d",
183
+ "--translations-dir",
184
+ default=None,
185
+ help="Path to the output translations directory. Defaults to value from configuration"
186
+ )
187
+ @click.option(
188
+ "-D",
189
+ "--domain",
190
+ default=None,
191
+ help="The domain of the translation files. Defaults to value from configuration"
192
+ )
193
+ @click.option(
194
+ "-c",
195
+ "--cfg",
196
+ default=None,
197
+ help="Configuration specification to use to determine translations directory and domain (defaults to environment $DATACUBE_OWS_CFG)"
198
+ )
199
+ @click.argument("languages", nargs=-1)
200
+ def translation(languages: list[str], msg_file: str | None, new: bool,
201
+ domain: str | None, translations_dir: str | None, cfg: str | None) -> int:
202
+ """Generate a new translations catalog based on the specified message file.
203
+
204
+ Takes a list of languages to generate catalogs for. "all" can be included as a shorthand
205
+ for all languages listed as supported in the configuration.
206
+ """
207
+ if len(languages) == 0:
208
+ click.echo("No language(s) specified.")
209
+ sys.exit(1)
210
+ if msg_file is None or domain is None or translations_dir is None or "all" in languages:
211
+ try:
212
+ raw_cfg = read_config(cfg)
213
+ config = OWSConfig(refresh=True, cfg=raw_cfg)
214
+ except ConfigException as e:
215
+ click.echo(f"Config exception for path: {str(e)}")
216
+ sys.exit(1)
217
+ if domain is None:
218
+ click.echo(f"Using message domain '{config.message_domain}' from configuration")
219
+ domain = config.message_domain
220
+ if translations_dir is None and config.translations_dir is None:
221
+ click.echo("No translations directory was supplied or is configured")
222
+ sys.exit(1)
223
+ elif translations_dir is None:
224
+ click.echo(f"Using translations directory '{config.translations_dir}' from configuration")
225
+ translations_dir = config.translations_dir
226
+ if msg_file is None and config.msg_file_name is None:
227
+ click.echo("No message file name was supplied or is configured")
228
+ sys.exit(1)
229
+ elif msg_file is None:
230
+ click.echo(f"Using message file location '{config.msg_file_name}' from configuration")
231
+ msg_file = config.msg_file_name
232
+ all_langs = config.locales
233
+ else:
234
+ all_langs = []
235
+ assert msg_file is not None # For type checker
236
+ assert domain is not None # For type checker
237
+ assert translations_dir is not None # For type checker
238
+ try:
239
+ fp = open(msg_file, "rb")
240
+ fp.close()
241
+ except OSError:
242
+ click.echo("Message file {msg_file} does not exist or cannot be read.")
243
+ sys.exit(1)
244
+ for language in languages:
245
+ if language == "all":
246
+ for supp_lang in all_langs:
247
+ if new:
248
+ create_translation(msg_file, translations_dir, domain, supp_lang)
249
+ else:
250
+ update_translation(msg_file, translations_dir, domain, supp_lang)
251
+ else:
252
+ if new:
253
+ create_translation(msg_file, translations_dir, domain, language)
254
+ else:
255
+ update_translation(msg_file, translations_dir, domain, language)
256
+ click.echo("Language templates created.")
257
+ return 0
258
+
259
+
260
+ def create_translation(msg_file: str, translations_dir: str, domain: str, locale: str) -> bool:
261
+ click.echo(f"Creating template for language: {locale}")
262
+ os.system(f"pybabel init -i {msg_file} -d {translations_dir} -D {domain} -l {locale}")
263
+ return True
264
+
265
+
266
+ def update_translation(msg_file: str, translations_dir: str, domain: str, locale: str) -> bool:
267
+ click.echo(f"Updating template for language: {locale}")
268
+ os.system(f"pybabel update --no-fuzzy-matching --ignore-obsolete "
269
+ f"-i {msg_file} -d {translations_dir} -D {domain} -l {locale}")
270
+ return True
271
+
272
+
273
+ @main.command(name="compile")
274
+ @click.option(
275
+ "-d",
276
+ "--translations-dir",
277
+ default=None,
278
+ help="Path to the output translations directory. Defaults to value from configuration"
279
+ )
280
+ @click.option(
281
+ "-D",
282
+ "--domain",
283
+ default=None,
284
+ help="The domain of the translation files. Defaults to value from configuration"
285
+ )
286
+ @click.option(
287
+ "-c",
288
+ "--cfg",
289
+ default=None,
290
+ help="Configuration specification to use to determine translations directory and domain (defaults to environment $DATACUBE_OWS_CFG)"
291
+ )
292
+ @click.argument("languages", nargs=-1)
293
+ def compile_cmd(languages: list[str], domain: str | None, translations_dir: str | None, cfg: str | None) -> int:
294
+ """Compile completed translation files.
295
+
296
+ Takes a list of languages to generate catalogs for. "all" can be included as a shorthand
297
+ for all languages listed as supported in the configuration.
298
+ """
299
+ if len(languages) == 0:
300
+ click.echo("No language(s) specified.")
301
+ sys.exit(1)
302
+ if domain is None or translations_dir is None or "all" in languages:
303
+ try:
304
+ raw_cfg = read_config(cfg)
305
+ config = OWSConfig(refresh=True, cfg=raw_cfg)
306
+ except ConfigException as e:
307
+ click.echo(f"Config exception for path: {str(e)}")
308
+ sys.exit(1)
309
+ if domain is None:
310
+ click.echo(f"Using message domain '{config.message_domain}' from configuration")
311
+ domain = config.message_domain
312
+ if translations_dir is None and config.translations_dir is None:
313
+ click.echo("No translations directory was supplied or is configured")
314
+ sys.exit(1)
315
+ elif translations_dir is None:
316
+ click.echo(f"Using translations directory '{config.translations_dir}' from configuration")
317
+ translations_dir = config.translations_dir
318
+ all_langs = config.locales
319
+ else:
320
+ all_langs = []
321
+ assert translations_dir is not None
322
+ for language in languages:
323
+ if language == "all":
324
+ for supp_lang in all_langs:
325
+ compile_translation(translations_dir, domain, supp_lang)
326
+ else:
327
+ compile_translation(translations_dir, domain, language)
328
+ click.echo("Language templates created.")
329
+ return 0
330
+
331
+
332
+ def compile_translation(translations_dir: str, domain: str, language: str) -> bool:
333
+ click.echo(f"Compiling template for language: {language}")
334
+ os.system(f"pybabel compile -d {translations_dir} -D {domain} -l {language}")
335
+ return True
336
+
337
+
338
+ def write_msg_file(msg_file: str, cfg: OWSConfig) -> None:
339
+ with open(msg_file, "wb") as fp:
340
+ write_po(fp, cfg.export_metadata())
341
+
342
+
343
+ def layers_report(config_values: dict[str, OWSNamedLayer], input_file: str, output_file: str) -> None:
344
+ report = {"total_layers_count": len(config_values.values()), "layers": []}
345
+ for lyr in config_values.values():
346
+ layer = {
347
+ "layer": lyr.name,
348
+ "product": list(lyr.product_names),
349
+ "styles_count": len(lyr.styles),
350
+ "styles_list": [styl.name for styl in lyr.styles],
351
+ }
352
+ cast(list[dict], report["layers"]).append(layer)
353
+ if input_file:
354
+ with open(input_file) as f:
355
+ input_file_data = json.load(f)
356
+ ddiff = DeepDiff(input_file_data, report, ignore_order=True)
357
+ if len(ddiff) != 0:
358
+ click.echo(ddiff)
359
+ sys.exit(1)
360
+ elif output_file:
361
+ with open(output_file, 'w') as reportfile:
362
+ json.dump(report, reportfile, indent=4)
363
+
364
+
365
+ def print_layers(layers: list[OWSLayer], styles: bool, depth: int) -> None:
366
+ for lyr in layers:
367
+ if isinstance(lyr, OWSFolder):
368
+ indent(depth)
369
+ click.echo(f"* {lyr.title}")
370
+ click.echo(f"{lyr.child_layers} {styles} {depth + 1}")
371
+ else:
372
+ indent(depth)
373
+ click.echo(f"{lyr.name} [{','.join(lyr.product_names)}]")
374
+ if styles:
375
+ click.echo(f"{lyr} {depth}")
376
+
377
+
378
+ def print_styles(lyr: OWSNamedLayer, depth: int = 0) -> None:
379
+ for styl in lyr.styles:
380
+ indent(0, for_styles=True)
381
+ print(f". {styl.name}")
382
+
383
+
384
+ def indent(depth: int, for_styles: bool = False) -> None:
385
+ for i in range(depth):
386
+ click.echo(" ", nl=False)
387
+ if for_styles:
388
+ click.echo(" ", nl=False)