From 1c22b2c0af5899546944fac07bffd62a30ba8286 Mon Sep 17 00:00:00 2001 From: Paul Haesler Date: Thu, 11 Jul 2024 14:52:13 +1000 Subject: [PATCH] Postgis driver support (#1032) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate (#1022) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/PyCQA/pylint: v3.1.0 → v3.2.3](https://github.com/PyCQA/pylint/compare/v3.1.0...v3.2.3) * Lintage for Pylint upgrade. * Non-pre-commit pylint too old. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Paul Haesler (cherry picked from commit bca8a8e205c3495f97122303f0bce38cc444aeec) * Refactor run_sql to be shared code. Fix and document permissions behaviour for integration tests. * Switch datacube-ows-update -e env option to -E env for consistency with datacube. * Build postgis test database, including migrating geodata_coast to eo3. * Add and register postgis index driver - no functionality implemented. * Schema creation and management for postgis indexes. * Postgis implementation, end-to-end testing and cleanup. * Lintage/spelling and fix docker-compose for GH running. * Attempt to get multi-db docker-compose test framework working in GHA. * Fix pg_ready args. * xargs was manging pg_isready args. * Still trying to get pg_isready args passed correctly * Ah need to replace DB_HOSTNAME. * and typo. * Update check-all-code.sh * Separate probe db? * Oh of course, datacube-ows-update can't be called until after we've schema-ed both dbs. * Haha. Oops. * Core/OWS API fixes and workaround permissions bug in core. * Lintage and mypy. * Convert 4236 before clipping to CRS when generating CRS specific bboxes. * MyPy fix. * Fix time-query handling. * Lintage. * mypy fix. * Fix postgis spatial extent method. * Improve test coverage and remove unused code. * Remove unused imports. * Remove excess progress noise from datacube-ows-update --schema type commands. * Update documentation. * Silly spelling word. * More spelling. * Cleanup, add more comments. * Cleanup, add more comments. * Fix formatting of progress messages in run_sql(). * Fix sql.py copypasta. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .env_simple | 17 +- .github/workflows/lint.yml | 1 - .pre-commit-config.yaml | 2 +- README.rst | 6 + check-code-all.sh | 47 +- datacube_ows/data.py | 16 +- datacube_ows/feature_info.py | 7 +- datacube_ows/index/api.py | 30 +- datacube_ows/index/postgis/api.py | 157 +++++ datacube_ows/index/postgis/product_ranges.py | 252 ++++++++ datacube_ows/index/postgres/api.py | 23 +- datacube_ows/index/postgres/product_ranges.py | 22 +- datacube_ows/index/{postgres => }/sql.py | 35 +- datacube_ows/loading.py | 131 ++-- datacube_ows/mv_index.py | 180 ------ datacube_ows/ows_configuration.py | 5 +- .../ows_schema/create/001_create_schema.sql | 3 + .../create/002_create_product_rng.sql | 17 + .../001_grant_usage_requires_role.sql | 3 + .../002_grant_range_read_requires_role.sql | 3 + .../003_grant_odc_user_requires_role.sql | 3 + .../001_grant_usage_requires_role.sql | 3 + .../002_grant_writetables_requires_role.sql | 3 + .../003_grant_odc_user_requires_role.sql | 3 + .../003_grant_agdc_user_requires_role.sql | 3 + datacube_ows/styles/component.py | 2 +- datacube_ows/update_ranges_impl.py | 16 +- datacube_ows/wcs1_utils.py | 7 +- datacube_ows/wcs2_utils.py | 7 +- docker-compose.db.yaml | 10 +- docker-compose.yaml | 13 +- docker/ows/wait-for-db | 4 +- docs/database.rst | 73 ++- docs/diagrams/ows_diagram1.9.png | Bin 70343 -> 136625 bytes docs/environment_variables.rst | 48 +- docs/installation.rst | 6 +- docs/usage.rst | 2 +- integration_tests/cfg/ows_test_cfg.py | 583 +++++++++++++----- .../de/LC_MESSAGES/ows_cfg.po | 31 + .../en/LC_MESSAGES/ows_cfg.po | 31 + .../translations/de/LC_MESSAGES/ows_cfg.po | 4 +- .../translations/en/LC_MESSAGES/ows_cfg.mo | Bin 755 -> 755 bytes .../translations/en/LC_MESSAGES/ows_cfg.po | 2 +- integration_tests/conftest.py | 10 +- .../metadata/COAST_100K_15_-40.yaml | 41 ++ .../metadata/COAST_100K_8_-21.yaml | 41 ++ .../metadata/metadata_importer.py | 15 +- .../metadata/product_geodata_coast_100k.yaml | 37 ++ integration_tests/test_mv_index.py | 2 +- integration_tests/test_update_ranges.py | 28 +- integration_tests/test_version.py | 2 +- integration_tests/test_wcs_server.py | 32 +- integration_tests/utils.py | 21 +- ows_cfg_report.json | 166 ++++- setup.py | 3 +- tests/test_driver_cache.py | 3 + tests/test_mv_selopts.py | 2 +- tests/test_update_ranges.py | 8 +- wordlist.txt | 2 + 59 files changed, 1650 insertions(+), 574 deletions(-) create mode 100644 datacube_ows/index/postgis/api.py create mode 100644 datacube_ows/index/postgis/product_ranges.py rename datacube_ows/index/{postgres => }/sql.py (82%) delete mode 100644 datacube_ows/mv_index.py create mode 100644 datacube_ows/sql/postgis/ows_schema/create/001_create_schema.sql create mode 100644 datacube_ows/sql/postgis/ows_schema/create/002_create_product_rng.sql create mode 100644 datacube_ows/sql/postgis/ows_schema/grants/read_only/001_grant_usage_requires_role.sql create mode 100644 datacube_ows/sql/postgis/ows_schema/grants/read_only/002_grant_range_read_requires_role.sql create mode 100644 datacube_ows/sql/postgis/ows_schema/grants/read_only/003_grant_odc_user_requires_role.sql create mode 100644 datacube_ows/sql/postgis/ows_schema/grants/read_write/001_grant_usage_requires_role.sql create mode 100644 datacube_ows/sql/postgis/ows_schema/grants/read_write/002_grant_writetables_requires_role.sql create mode 100644 datacube_ows/sql/postgis/ows_schema/grants/read_write/003_grant_odc_user_requires_role.sql create mode 100644 datacube_ows/sql/postgres/ows_schema/grants/read_write/003_grant_agdc_user_requires_role.sql create mode 100644 integration_tests/cfg/test_translations/de/LC_MESSAGES/ows_cfg.po create mode 100644 integration_tests/cfg/test_translations/en/LC_MESSAGES/ows_cfg.po create mode 100644 integration_tests/metadata/COAST_100K_15_-40.yaml create mode 100644 integration_tests/metadata/COAST_100K_8_-21.yaml create mode 100644 integration_tests/metadata/product_geodata_coast_100k.yaml diff --git a/.env_simple b/.env_simple index bdbacd3c0..ab1e83e50 100644 --- a/.env_simple +++ b/.env_simple @@ -4,12 +4,17 @@ ################ # ODC DB Config # ############## -ODC_DEFAULT_DB_URL=postgresql://opendatacubeusername:opendatacubepassword@postgres:5432/opendatacube -# Needed for docker db image. -DB_PORT=5432 -DB_USERNAME=opendatacubeusername -DB_PASSWORD=opendatacubepassword -DB_DATABASE=opendatacube +ODC_DEFAULT_DB_URL=postgresql://opendatacubeusername:opendatacubepassword@postgres:5432/odc_postgres +ODC_OWSPOSTGIS_DB_URL=postgresql://opendatacubeusername:opendatacubepassword@postgres:5432/odc_postgis + +# Needed for docker db image and db readiness probe. +POSTGRES_PORT=5432 +POSTGRES_HOSTNAME=postgres +POSTGRES_USER=opendatacubeusername +SERVER_DB_USERNAME=opendatacubeusername +POSTGRES_PASSWORD=opendatacubepassword +POSTGRES_DB="odc_postgres,odc_postgis" +READY_PROBE_DB=odc_postgis ################# # OWS CFG Config diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 28d48e0ee..353213bbc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -40,7 +40,6 @@ jobs: - name: Install dependencies and run pylint run: | pip install .[test,dev] - pip install pylint pylint -j 2 --reports no datacube_ows --disable=C,R,W,E1136 flake8: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9c7ce9be..a7f9c71ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: # hooks: # - id: bandit - repo: https://github.com/PyCQA/pylint - rev: v3.1.0 + rev: v3.2.3 hooks: - id: pylint args: ["--disable=C,R,W,E1136"] diff --git a/README.rst b/README.rst index f906ded41..7434b5d58 100644 --- a/README.rst +++ b/README.rst @@ -185,6 +185,10 @@ The following instructions are for installing on a clean Linux system. export DATACUBE_OWS_CFG=datacube_ows.ows_cfg_example.ows_cfg datacube-ows-update --write-role ubuntu --schema + # If you are not using the `default` ODC environment, you can specify the environment to create the schema in: + + datacube-ows-update -E myenv --write-role ubuntu --schema + * Create a configuration file for your service, and all data products you wish to publish in it. @@ -199,7 +203,9 @@ The following instructions are for installing on a clean Linux system. * When additional datasets are added to the datacube, the following steps will need to be run:: + # Update the materialised views (postgis index driver only - can be skipped for the postgis index driver): datacube-ows-update --views + # Update the range tables (both index drivers) datacube-ows-update * If you are accessing data on AWS S3 and running `datacube_ows` on Ubuntu you may encounter errors with ``GetMap`` diff --git a/check-code-all.sh b/check-code-all.sh index 040dd2f14..1c0fe4be2 100755 --- a/check-code-all.sh +++ b/check-code-all.sh @@ -5,30 +5,45 @@ set -ex # ensure db is ready sh ./docker/ows/wait-for-db -# Initialise ODC schema +# Initialise ODC schemas datacube system init +datacube -E owspostgis system init +datacube -E owspostgis spindex create 3577 +datacube -E owspostgis system init # Add extended metadata types datacube metadata add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/product_metadata/eo3_landsat_ard.odc-type.yaml datacube metadata add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/product_metadata/eo3_sentinel_ard.odc-type.yaml +datacube -E owspostgis metadata add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/product_metadata/eo3_landsat_ard.odc-type.yaml +datacube -E owspostgis metadata add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/product_metadata/eo3_sentinel_ard.odc-type.yaml + # Test products datacube product add ./integration_tests/metadata/s2_l2a_prod.yaml datacube product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/baseline_satellite_data/c3/ga_s2am_ard_3.odc-product.yaml datacube product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/baseline_satellite_data/c3/ga_s2bm_ard_3.odc-product.yaml datacube product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/land_and_vegetation/c3_fc/ga_ls_fc_3.odc-product.yaml +datacube -E owspostgis product add ./integration_tests/metadata/s2_l2a_prod.yaml +datacube -E owspostgis product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/baseline_satellite_data/c3/ga_s2am_ard_3.odc-product.yaml +datacube -E owspostgis product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/baseline_satellite_data/c3/ga_s2bm_ard_3.odc-product.yaml +datacube -E owspostgis product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/land_and_vegetation/c3_fc/ga_ls_fc_3.odc-product.yaml + # add flag masking products -datacube product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/sea_ocean_coast/geodata_coast_100k/geodata_coast_100k.odc-product.yaml +datacube product add ./integration_tests/metadata/product_geodata_coast_100k.yaml datacube product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/inland_water/c3_wo/ga_ls_wo_3.odc-product.yaml +datacube -E owspostgis product add ./integration_tests/metadata/product_geodata_coast_100k.yaml +datacube -E owspostgis product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/inland_water/c3_wo/ga_ls_wo_3.odc-product.yaml + # Geomedian for summary product testing datacube product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/baseline_satellite_data/geomedian-au/ga_ls8c_nbart_gm_cyear_3.odc-product.yaml +datacube -E owspostgis product add https://raw.githubusercontent.com/GeoscienceAustralia/dea-config/master/products/baseline_satellite_data/geomedian-au/ga_ls8c_nbart_gm_cyear_3.odc-product.yaml -# S2 datasets from us-west-2 (might not work) +# S2 datasets from us-west-2 and eo3ified geodata_coast MDL=./integration_tests/metadata python ${MDL}/metadata_importer.py < FlaskResponse: stacker = DataStacker(params.layer, params.geobox, params.times, params.resampling, style=params.style) qprof["zoom_factor"] = params.zf qprof.start_event("count-datasets") - n_datasets = stacker.datasets(params.layer.dc.index, mode=MVSelectOpts.COUNT) + n_datasets = stacker.n_datasets() qprof.end_event("count-datasets") qprof["n_datasets"] = n_datasets qprof["zoom_level_base"] = params.resources.base_zoom_level @@ -113,8 +112,7 @@ def get_map(args: dict[str, str]) -> FlaskResponse: stacker.resource_limited = True qprof["resource_limited"] = str(e) if qprof.active: - q_ds_dict = cast(dict[ProductBandQuery, xarray.DataArray], - stacker.datasets(params.layer.dc.index, mode=MVSelectOpts.DATASETS)) + q_ds_dict = stacker.datasets() qprof["datasets"] = [] for q, dsxr in q_ds_dict.items(): query_res: dict[str, Any] = {} @@ -129,7 +127,7 @@ def get_map(args: dict[str, str]) -> FlaskResponse: qprof["datasets"].append(query_res) if stacker.resource_limited and not params.layer.low_res_product_names: qprof.start_event("extent-in-query") - extent = cast(geom.Geometry | None, stacker.datasets(params.layer.dc.index, mode=MVSelectOpts.EXTENT)) + extent = stacker.extent(crs=params.crs) qprof.end_event("extent-in-query") if extent is None: qprof["write_action"] = "No extent: Write Empty" @@ -149,10 +147,10 @@ def get_map(args: dict[str, str]) -> FlaskResponse: else: if stacker.resource_limited: qprof.start_event("count-summary-datasets") - qprof["n_summary_datasets"] = stacker.datasets(params.layer.dc.index, mode=MVSelectOpts.COUNT) + qprof["n_summary_datasets"] = stacker.n_datasets() qprof.end_event("count-summary-datasets") qprof.start_event("fetch-datasets") - datasets = cast(dict[ProductBandQuery, xarray.DataArray], stacker.datasets(params.layer.dc.index)) + datasets = stacker.datasets() for flagband, dss in datasets.items(): if not dss.any(): _LOG.warning("Flag band %s returned no data", str(flagband)) diff --git a/datacube_ows/feature_info.py b/datacube_ows/feature_info.py index 6f2b4fbb9..af0ed73c0 100644 --- a/datacube_ows/feature_info.py +++ b/datacube_ows/feature_info.py @@ -158,7 +158,7 @@ def feature_info(args: dict[str, str]) -> FlaskResponse: stacker = DataStacker(params.layer, geo_point_geobox, params.times) # --- Begin code section requiring datacube. cfg = get_config() - all_time_datasets = cast(xarray.DataArray, stacker.datasets(params.layer.dc.index, all_time=True, point=geo_point)) + all_time_datasets = stacker.datasets_all_time(point=geo_point) # Taking the data as a single point so our indexes into the data should be 0,0 h_coord = cast(str, cfg.published_CRSs[params.crsid]["horizontal_coord"]) @@ -174,10 +174,7 @@ def feature_info(args: dict[str, str]) -> FlaskResponse: global_info_written = False feature_json["data"] = [] fi_date_index: dict[datetime, RAW_CFG] = {} - time_datasets = cast( - dict[ProductBandQuery, xarray.DataArray], - stacker.datasets(params.layer.dc.index, all_flag_bands=True, point=geo_point) - ) + time_datasets = stacker.datasets(all_flag_bands=True, point=geo_point) data = stacker.data(time_datasets, skip_corrections=True) if data is not None: for dt in data.time.values: diff --git a/datacube_ows/index/api.py b/datacube_ows/index/api.py index 1a196ab71..06ec1b896 100644 --- a/datacube_ows/index/api.py +++ b/datacube_ows/index/api.py @@ -13,7 +13,7 @@ from datacube import Datacube from datacube.index.abstract import AbstractIndex from datacube.model import Product, Dataset -from odc.geo import Geometry, CRS +from odc.geo.geom import Geometry, CRS, polygon from datacube_ows.config_utils import CFG_DICT, ConfigException @@ -128,6 +128,7 @@ def extent(self, products: Iterable[Product] | None = None, crs: CRS | None = None ) -> Geometry | None: + geom = self._prep_geom(layer, geom) if crs is None: crs = CRS("epsg:4326") ext: Geometry | None = None @@ -148,6 +149,33 @@ def extent(self, return ext.to_crs(crs) return ext + def _prep_geom(self, layer: "OWSNamedLayer", any_geom: Geometry | None) -> Geometry | None: + # Prepare a Geometry for geospatial search + # Perhaps Core can be updated so this is not needed? + if any_geom is None: + # None? Leave as None + return None + if any_geom.geom_type == "Point": + # Point? Expand to a polygon covering a single native pixel. + any_geom = any_geom.to_crs(layer.native_CRS) + x, y = any_geom.coords[0] + delta_x, delta_y = layer.cfg_native_resolution + return polygon( + ( + (x, y), + (x + delta_x, y), + (x + delta_x, y + delta_y), + (x, y + delta_y), + (x, y), + ), + crs=layer.native_CRS + ) + elif any_geom.geom_type in ("MultiPoint", "LineString", "MultiLineString"): + # Not a point, but not a polygon or multipolygon? Expand to polygon by taking convex hull + return any_geom.convex_hull + else: + # Return polygons and multipolygons as is. + return any_geom class OWSAbstractIndexDriver(ABC): @classmethod diff --git a/datacube_ows/index/postgis/api.py b/datacube_ows/index/postgis/api.py new file mode 100644 index 000000000..3e260d302 --- /dev/null +++ b/datacube_ows/index/postgis/api.py @@ -0,0 +1,157 @@ +# This file is part of datacube-ows, part of the Open Data Cube project. +# See https://opendatacube.org for more information. +# +# Copyright (c) 2017-2024 OWS Contributors +# SPDX-License-Identifier: Apache-2.0 + +import click +import datetime + +from threading import Lock +from typing import Any, Iterable, Type +from uuid import UUID + +from odc.geo import Geometry, CRS +from datacube import Datacube +from datacube.model import Product, Dataset, Range + +from datacube_ows.ows_configuration import OWSNamedLayer +from datacube_ows.index.api import OWSAbstractIndex, OWSAbstractIndexDriver, LayerSignature, LayerExtent, TimeSearchTerm +from datacube_ows.index.sql import run_sql +from .product_ranges import create_range_entry as create_range_entry_impl, get_ranges as get_ranges_impl +from ...utils import default_to_utc + + +class OWSPostgisIndex(OWSAbstractIndex): + name: str = "postgis" + + # method to delete obsolete schemas etc. + def cleanup_schema(self, dc: Datacube): + # No obsolete schema for postgis databases to clean up. + pass + + # Schema creation method + def create_schema(self, dc: Datacube): + click.echo("Creating/updating schema and tables...") + self._run_sql(dc, "ows_schema/create") + + # Permission management method + def grant_perms(self, dc: Datacube, role: str, read_only: bool = False): + if read_only: + self._run_sql(dc, "ows_schema/grants/read_only", role=role) + else: + self._run_sql(dc, "ows_schema/grants/read_write", role=role) + + # Spatiotemporal index update method (e.g. refresh materialised views) + def update_geotemporal_index(self, dc: Datacube): + # Native ODC geotemporal index used in postgis driver. + pass + + def create_range_entry(self, layer: OWSNamedLayer, cache: dict[LayerSignature, list[str]]) -> None: + create_range_entry_impl(layer, cache) + + def get_ranges(self, layer: OWSNamedLayer) -> LayerExtent | None: + return get_ranges_impl(layer) + + def _query(self, + layer: OWSNamedLayer, + times: Iterable[TimeSearchTerm] | None = None, + geom: Geometry | None = None, + products: Iterable[Product] | None = None + ) -> dict[str, Any]: + query: dict[str, Any] = {} + if geom: + query["geopolygon"] = self._prep_geom(layer, geom) + if products is not None: + query["product"] = [p.name for p in products] + if times is not None: + + def normalise_to_dtr(unnorm: datetime.datetime | datetime.date) -> tuple[datetime.datetime, datetime.datetime]: + if isinstance(unnorm, datetime.datetime): + st: datetime.datetime = default_to_utc(unnorm) + tmax = st + datetime.timedelta(seconds=1) + elif isinstance(t, datetime.date): + st = datetime.datetime(unnorm.year, unnorm.month, unnorm.day, tzinfo=datetime.timezone.utc) + tmax = st + datetime.timedelta(days=1) + else: + raise ValueError("Not a datetime object") + return st, tmax + + time_args = [] + for t in times: + if isinstance(t, (datetime.date, datetime.datetime)): + start, tmax = normalise_to_dtr(t) + time_args.append(Range(start, tmax)) + else: + st, et = t + st, _ = normalise_to_dtr(st) + et, _ = normalise_to_dtr(et) + time_args.append(Range(st, et)) + if len(time_args) > 1: + raise ValueError("Huh?") + query["time"] = time_args[0] + return query + + def ds_search(self, + layer: OWSNamedLayer, + times: Iterable[TimeSearchTerm] | None = None, + geom: Geometry | None = None, + products: Iterable[Product] | None = None + ) -> Iterable[Dataset]: + return layer.dc.index.datasets.search(**self._query(layer, times, geom, products)) + + def dsid_search(self, + layer: OWSNamedLayer, + times: Iterable[TimeSearchTerm] | None = None, + geom: Geometry | None = None, + products: Iterable[Product] | None = None + ) -> Iterable[UUID]: + for ds in layer.dc.index.datasets.search_returning(field_names=["id"], + **self._query(layer, times, geom, products)): + yield ds.id # type: ignore[attr-defined] + + def count(self, + layer: OWSNamedLayer, + times: Iterable[TimeSearchTerm] | None = None, + geom: Geometry | None = None, + products: Iterable[Product] | None = None + ) -> int: + return layer.dc.index.datasets.count(**self._query(layer, times, geom, products)) + + def extent(self, + layer: OWSNamedLayer, + times: Iterable[TimeSearchTerm] | None = None, + geom: Geometry | None = None, + products: Iterable[Product] | None = None, + crs: CRS | None = None + ) -> Geometry | None: + if crs is None: + crs = CRS("epsg:4326") + return layer.dc.index.datasets.spatial_extent( + self.dsid_search(layer, times=times, geom=geom, products=products), + crs=crs + ) + + def _run_sql(self, dc: Datacube, path: str, **params: str) -> bool: + return run_sql(dc, self.name, path, **params) + + +pgisdriverlock = Lock() + + +class OWSPostgisIndexDriver(OWSAbstractIndexDriver): + _driver = None + @classmethod + def ows_index_class(cls) -> Type[OWSAbstractIndex]: + return OWSPostgisIndex + + @classmethod + def ows_index(cls) -> OWSAbstractIndex: + with pgisdriverlock: + if cls._driver is None: + cls._driver = OWSPostgisIndex() + return cls._driver + + +def ows_index_driver_init(): + return OWSPostgisIndexDriver() diff --git a/datacube_ows/index/postgis/product_ranges.py b/datacube_ows/index/postgis/product_ranges.py new file mode 100644 index 000000000..7e43654cb --- /dev/null +++ b/datacube_ows/index/postgis/product_ranges.py @@ -0,0 +1,252 @@ +# This file is part of datacube-ows, part of the Open Data Cube project. +# See https://opendatacube.org for more information. +# +# Copyright (c) 2017-2024 OWS Contributors +# SPDX-License-Identifier: Apache-2.0 + +import logging +import math +from datetime import date, datetime, timezone +from typing import Callable +import click + +import datacube +import odc.geo +import sqlalchemy.exc +from psycopg2.extras import Json +from sqlalchemy import text + +from odc.geo.geom import CRS + +from datacube_ows.ows_configuration import OWSNamedLayer +from datacube_ows.utils import get_sqlconn +from datacube_ows.index.api import CoordRange, LayerSignature, LayerExtent + +_LOG = logging.getLogger(__name__) + + +def jsonise_bbox(bbox: odc.geo.geom.BoundingBox) -> dict[str, float]: + return { + "top": bbox.top, + "bottom": bbox.bottom, + "left": bbox.left, + "right": bbox.right, + } + + +def create_range_entry(layer: OWSNamedLayer, cache: dict[LayerSignature, list[str]]) -> None: + meta = LayerSignature(time_res=layer.time_resolution.value, + products=tuple(layer.product_names), + env=layer.local_env._name, + datasets=layer.dc.index.datasets.count(product=layer.product_names)) + + click.echo(f"Postgis Updating range for layer {layer.name}") + click.echo(f"(signature: {meta.as_json()!r})") + conn = get_sqlconn(layer.dc) + txn = conn.begin() + if meta in cache: + template = cache[meta][0] + click.echo(f"Layer {template} has same signature - reusing") + cache[meta].append(layer.name) + try: + conn.execute(text(""" + INSERT INTO ows.layer_ranges + (layer, lat_min, lat_max, lon_min, lon_max, dates, bboxes, meta, last_updated) + SELECT :layer_id, lat_min, lat_max, lon_min, lon_max, dates, bboxes, meta, last_updated + FROM ows.layer_ranges lr2 + WHERE lr2.layer = :template_id"""), + { + "layer_id": layer.name, + "template_id": template + }) + except sqlalchemy.exc.IntegrityError: + conn.execute(text(""" + UPDATE ows.layer_ranges lr1 + SET lat_min = lr2.lat_min, + lat_max = lr2.lat_max, + lon_min = lr2.lon_min, + lon_max = lr2.lon_max, + dates = lr2.dates, + bboxes = lr2.bboxes, + meta = lr2.meta, + last_updated = lr2.last_updated + FROM ows.layer_ranges lr2 + WHERE lr1.layer = :layer_id + AND lr2.layer = :template_id"""), + { + "layer_id": layer.name, + "template_id": template + }) + else: + # insert empty row if one does not already exist + conn.execute(text(""" + INSERT INTO ows.layer_ranges + (layer, lat_min, lat_max, lon_min, lon_max, dates, bboxes, meta, last_updated) + VALUES + (:p_layer, 0, 0, 0, 0, :empty, :empty, :meta, :now) + ON CONFLICT (layer) DO NOTHING + """), + { + "p_layer": layer.name, "empty": Json(""), + "meta": Json(meta.as_json()), "now": datetime.now(tz=timezone.utc) + }) + + prodids = [p.id for p in layer.products] + + # Set default timezone + conn.execute(text("""set timezone to 'Etc/UTC'""")) + + # Loop over dates + dates = set() # Should get to here! + if layer.time_resolution.is_solar(): + results = conn.execute(text( + """ + select lower(dt.search_val), upper(dt.search_val), + lower(lon.search_val), upper(lon.search_val) + from odc.dataset ds, odc.dataset_search_datetime dt, odc.dataset_search_num lon + where ds.product_ref = ANY(:prodids) + AND ds.id = dt.dataset_ref + AND ds.id = lon.dataset_ref + AND dt.search_key = :time + AND lon.search_key = :lon + """), + {"prodids": prodids, "time": "time", "lon": "lon"}) + for result in results: + dt1, dt2, ll, lu = result + lon = (ll + lu) / 2 + dt = dt1 + (dt2 - dt1) / 2 + dt = dt.astimezone(timezone.utc) + + solar_day = datacube.api.query._convert_to_solar_time(dt, lon).date() + dates.add(solar_day) + else: + results = conn.execute(text( + """ + select + array_agg(dt.search_val) + from odc.dataset_search_datetime dt, + odc.dataset ds + WHERE ds.product_ref = ANY(:prodids) + AND ds.id = dt.dataset_ref + AND dt.search_key = 'time' + """), + {"prodids": prodids} + ) + for result in results: + for dat_ran in result[0]: + dates.add(dat_ran.lower) + + if layer.time_resolution.is_subday(): + date_formatter = lambda d: d.isoformat() + else: + date_formatter = lambda d: d.strftime("%Y-%m-%d") + + dates = sorted(dates) + conn.execute(text(""" + UPDATE ows.layer_ranges + SET dates = :dates + WHERE layer= :layer_id + """), + { + "dates": Json(list(map(date_formatter, dates))), + "layer_id": layer.name + } + ) + # calculate bounding boxes + # Get extent polygon from materialised views + + base_crs = CRS(layer.native_CRS) + if base_crs not in layer.dc.index.spatial_indexes(): + click.echo(f"Native CRS for layer {layer.name} ({layer.native_CRS}) does not have a spatial index. " + "Using epsg:4326 for extent calculations.") + base_crs = CRS("EPSG:4326") + + base_extent = None + for product in layer.products: + prod_extent = layer.dc.index.products.spatial_extent(product, base_crs) + if base_extent is None: + base_extent = prod_extent + else: + base_extent = base_extent | prod_extent + assert base_extent is not None + all_bboxes = bbox_projections(base_extent, layer.global_cfg.crses) + + conn.execute(text(""" + UPDATE ows.layer_ranges + SET bboxes = :bbox, + lat_min = :lat_min, + lat_max = :lat_max, + lon_min = :lon_min, + lon_max = :lon_max + WHERE layer = :layer_id + """), { + "bbox": Json(all_bboxes), + "layer_id": layer.name, + "lat_min": all_bboxes['EPSG:4326']['bottom'], + "lat_max": all_bboxes['EPSG:4326']['top'], + "lon_min": all_bboxes['EPSG:4326']['left'], + "lon_max": all_bboxes['EPSG:4326']['right'] + }) + + cache[meta] = [layer.name] + + txn.commit() + conn.close() + + +def bbox_projections(starting_box: odc.geo.Geometry, crses: dict[str, odc.geo.CRS]) -> dict[str, dict[str, float]]: + result = {} + for crsid, crs in crses.items(): + if crs.valid_region is not None: + test_box = starting_box.to_crs("epsg:4326") + clipped_crs_region = (test_box & crs.valid_region) + if clipped_crs_region.wkt == 'POLYGON EMPTY': + continue + clipped_crs_bbox = clipped_crs_region.to_crs(crs).boundingbox + else: + clipped_crs_bbox = None + if clipped_crs_bbox is not None: + result[crsid] = jsonise_bbox(clipped_crs_bbox) + else: + projbbox = starting_box.to_crs(crs).boundingbox + result[crsid] = sanitise_bbox(projbbox) + return result + + +def sanitise_bbox(bbox: odc.geo.geom.BoundingBox) -> dict[str, float]: + def sanitise_coordinate(coord: float, fallback: float) -> float: + return coord if math.isfinite(coord) else fallback + return { + "top": sanitise_coordinate(bbox.top, float("9.999999999e99")), + "bottom": sanitise_coordinate(bbox.bottom, float("-9.999999999e99")), + "left": sanitise_coordinate(bbox.left, float("-9.999999999e99")), + "right": sanitise_coordinate(bbox.right, float("9.999999999e99")), + } + + +def get_ranges(layer: OWSNamedLayer) -> LayerExtent | None: + cfg = layer.global_cfg + conn = get_sqlconn(layer.dc) + results = conn.execute(text(""" + SELECT * + FROM ows.layer_ranges + WHERE layer=:pname"""), + {"pname": layer.name} + ) + + for result in results: + conn.close() + if layer.time_resolution.is_subday(): + dt_parser: Callable[[str], datetime | date] = lambda dts: datetime.fromisoformat(dts) + else: + dt_parser = lambda dts: datetime.strptime(dts, "%Y-%m-%d").date() + times = [dt_parser(d) for d in result.dates if d is not None] + if not times: + return None + return LayerExtent( + lat=CoordRange(min=float(result.lat_min), max=float(result.lat_max)), + lon=CoordRange(min=float(result.lon_min), max=float(result.lon_max)), + times=times, + bboxes=result.bboxes + ) + return None diff --git a/datacube_ows/index/postgres/api.py b/datacube_ows/index/postgres/api.py index 1dff0ef0a..65584f2e3 100644 --- a/datacube_ows/index/postgres/api.py +++ b/datacube_ows/index/postgres/api.py @@ -18,7 +18,7 @@ from datacube_ows.index.api import OWSAbstractIndex, OWSAbstractIndexDriver, LayerSignature, LayerExtent, TimeSearchTerm from .product_ranges import create_range_entry as create_range_entry_impl, get_ranges as get_ranges_impl from .mv_index import MVSelectOpts, mv_search -from .sql import run_sql +from datacube_ows.index.sql import run_sql class OWSPostgresIndex(OWSAbstractIndex): @@ -26,29 +26,29 @@ class OWSPostgresIndex(OWSAbstractIndex): # method to delete obsolete schemas etc. def cleanup_schema(self, dc: Datacube): - run_sql(dc, "ows_schema/cleanup") + self._run_sql(dc, "ows_schema/cleanup") # Schema creation method def create_schema(self, dc: Datacube): click.echo("Creating/updating schema and tables...") - run_sql(dc, "ows_schema/create") + self._run_sql(dc, "ows_schema/create") click.echo("Creating/updating materialised views...") - run_sql(dc, "extent_views/create") + self._run_sql(dc, "extent_views/create") click.echo("Setting ownership of materialised views...") - run_sql(dc, "extent_views/grants/refresh_owner") + self._run_sql(dc, "extent_views/grants/refresh_owner") # Permission management method def grant_perms(self, dc: Datacube, role: str, read_only: bool = False): if read_only: - run_sql(dc, "ows_schema/grants/read_only", role=role) - run_sql(dc, "extent_views/grants/read_only", role=role) + self._run_sql(dc, "ows_schema/grants/read_only", role=role) + self._run_sql(dc, "extent_views/grants/read_only", role=role) else: - run_sql(dc, "ows_schema/grants/read_write", role=role) - run_sql(dc, "extent_views/grants/write_refresh", role=role) + self._run_sql(dc, "ows_schema/grants/read_write", role=role) + self._run_sql(dc, "extent_views/grants/write_refresh", role=role) # Spatiotemporal index update method (e.g. refresh materialised views) def update_geotemporal_index(self, dc: Datacube): - run_sql(dc, "extent_views/refresh") + self._run_sql(dc, "extent_views/refresh") def create_range_entry(self, layer: OWSNamedLayer, cache: dict[LayerSignature, list[str]]) -> None: create_range_entry_impl(layer, cache) @@ -97,6 +97,9 @@ def extent(self, else: return extent.to_crs(crs) + def _run_sql(self, dc: Datacube, path: str, **params: str) -> bool: + return run_sql(dc, self.name, path, **params) + pgdriverlock = Lock() diff --git a/datacube_ows/index/postgres/product_ranges.py b/datacube_ows/index/postgres/product_ranges.py index 9b8425fc0..bac6306e9 100644 --- a/datacube_ows/index/postgres/product_ranges.py +++ b/datacube_ows/index/postgres/product_ranges.py @@ -6,8 +6,9 @@ import logging import math +import click from datetime import date, datetime, timezone -from typing import cast, Callable, Iterable +from typing import cast, Callable import datacube import odc.geo @@ -17,24 +18,14 @@ from odc.geo.geom import Geometry -from datacube_ows.ows_configuration import OWSConfig, OWSNamedLayer, get_config -from datacube_ows.mv_index import MVSelectOpts, mv_search +from datacube_ows.ows_configuration import OWSNamedLayer +from datacube_ows.index.postgres.mv_index import MVSelectOpts, mv_search from datacube_ows.utils import get_sqlconn from datacube_ows.index.api import CoordRange, LayerSignature, LayerExtent _LOG = logging.getLogger(__name__) -def get_crsids(cfg: OWSConfig | None = None) -> Iterable[str]: - if not cfg: - cfg = get_config() - return cfg.internal_CRSs.keys() - - -def get_crses(cfg: OWSConfig | None = None) -> dict[str, odc.geo.CRS]: - return {crsid: odc.geo.CRS(crsid) for crsid in get_crsids(cfg)} - - def jsonise_bbox(bbox: odc.geo.geom.BoundingBox) -> dict[str, float]: if isinstance(bbox, dict): return bbox @@ -53,12 +44,13 @@ def create_range_entry(layer: OWSNamedLayer, cache: dict[LayerSignature, list[st env=layer.local_env._name, datasets=layer.dc.index.datasets.count(product=layer.product_names)) - print(f"Updating range for layer {layer.name}") + click.echo(f"Postgres Updating range for layer {layer.name}") + click.echo(f"(signature: {meta.as_json()!r})") conn = get_sqlconn(layer.dc) txn = conn.begin() if meta in cache: template = cache[meta][0] - print(f"Layer {template} has same signature - reusing") + click.echo(f"Layer {template} has same signature - reusing") cache[meta].append(layer.name) try: conn.execute(text(""" diff --git a/datacube_ows/index/postgres/sql.py b/datacube_ows/index/sql.py similarity index 82% rename from datacube_ows/index/postgres/sql.py rename to datacube_ows/index/sql.py index 258713278..88f099756 100644 --- a/datacube_ows/index/postgres/sql.py +++ b/datacube_ows/index/sql.py @@ -25,13 +25,13 @@ def get_sqlconn(dc: Datacube) -> sqlalchemy.Connection: return dc.index._db._engine.connect() # type: ignore[attr-defined] -def run_sql(dc: Datacube, path: str, **params: str) -> bool: - if not importlib.resources.files("datacube_ows").joinpath(f"sql/postgres/{path}").is_dir(): +def run_sql(dc: Datacube, driver_name: str, path: str, **params: str) -> bool: + if not importlib.resources.files("datacube_ows").joinpath(f"sql/{driver_name}/{path}").is_dir(): print("Cannot find SQL resource directory - check your datacube-ows installation") return False files = sorted( - importlib.resources.files("datacube_ows").joinpath(f"sql/postgres/{path}").iterdir() # type: ignore[type-var] + importlib.resources.files("datacube_ows").joinpath(f"sql/{driver_name}/{path}").iterdir() # type: ignore[type-var] ) filename_req_pattern = re.compile(r"\d+[_a-zA-Z0-9]+_requires_(?P[_a-zA-Z0-9]+)\.sql") @@ -50,36 +50,39 @@ def run_sql(dc: Datacube, path: str, **params: str) -> bool: reqs = req_match.group("reqs").split("_") else: reqs = [] - ref = importlib.resources.files("datacube_ows").joinpath(f"sql/postgres/{path}/{f}") + if reqs: + try: + kwargs = {v: params[v] for v in reqs} + except KeyError as e: + click.echo(f"Required parameter {e} for file {f} not supplied - skipping") + all_ok = False + continue + else: + kwargs = {} + ref = importlib.resources.files("datacube_ows").joinpath(f"sql/{driver_name}/{path}/{f}") with ref.open("rb") as fp: sql = "" first = True for line in fp: sline = str(line, "utf-8") if first and sline.startswith("--"): - click.echo(f" - Running {sline[2:]}") + if reqs: + click.echo(f" - Running {sline[2:].format(**kwargs)}") + else: + click.echo(f" - Running {sline[2:]}") else: sql = sql + "\n" + sline first = False if reqs: - try: - kwargs = {v: params[v] for v in reqs} - except KeyError as e: - click.echo(f"Required parameter {e} for file {f} not supplied - skipping") - all_ok = False - continue sql = sql.format(**kwargs) try: result = conn.execute(sqlalchemy.text(sql)) - click.echo(f" ... succeeded(?) with {result!r} rowcount {result.rowcount}") - if result.returns_rows: - for r in result: - click.echo(f" ... succeeded(?) with {r!r}") + click.echo(f" ... succeeded(?) with rowcount {result.rowcount}") except sqlalchemy.exc.ProgrammingError as e: if isinstance(e.orig, psycopg2.errors.InsufficientPrivilege): click.echo( - f"Insufficient Privileges (user {dc.index.environment.db_username}). Schema altering actions should be run by a role with admin privileges" + f"Insufficient Privileges (user {dc.index.environment.db_username}). Schema altering actions should be run by a role with admin privileges" ) raise AbortRun() from None elif isinstance(e.orig, psycopg2.errors.DuplicateObject): diff --git a/datacube_ows/loading.py b/datacube_ows/loading.py index f6addba94..ee7c67892 100644 --- a/datacube_ows/loading.py +++ b/datacube_ows/loading.py @@ -14,11 +14,9 @@ import numpy import xarray from odc.geo.geobox import GeoBox -from odc.geo.geom import Geometry +from odc.geo.geom import Geometry, CRS from odc.geo.warp import Resampling -from sqlalchemy.engine import Row -from datacube_ows.mv_index import MVSelectOpts, mv_search from datacube_ows.ogc_exceptions import WMSException from datacube_ows.ows_configuration import OWSNamedLayer from datacube_ows.startup_utils import CredentialManager @@ -161,36 +159,77 @@ def __init__(self, def needed_bands(self) -> list[str]: return self._needed_bands - @log_call - def n_datasets(self, - index: datacube.index.Index, - all_time: bool = False, - point: Geometry | None = None) -> int: - return cast(int, self.datasets(index, - all_time=all_time, point=point, - mode=MVSelectOpts.COUNT)) + def n_datasets(self) -> int: + if self.style: + # we have a style - lets go with that. + queries = ProductBandQuery.style_queries(self.style) + else: + # Just take needed bands. + queries = [ProductBandQuery.simple_layer_query(self._layer, self.needed_bands())] + geom = self._geobox.extent + for query in queries: + if query.ignore_time: + qry_times = None + else: + qry_times = self._times + return self._layer.ows_index().count(self._layer, times=qry_times, geom=geom, products=query.products) + return 0 - def datasets(self, index: datacube.index.Index, - all_flag_bands: bool = False, - all_time: bool = False, - point: Geometry | None = None, - mode: MVSelectOpts = MVSelectOpts.DATASETS) -> (int - | Iterable[Row] - | Iterable[UUID] - | xarray.DataArray - | Geometry - | None - | dict[ProductBandQuery, PerPBQReturnType]): - if mode == MVSelectOpts.EXTENT or all_time: - # Not returning datasets - use main product only - queries = [ - ProductBandQuery.simple_layer_query( + def extent(self, crs: CRS | None = None) -> Geometry | None: + query = ProductBandQuery.simple_layer_query( + self._layer, + self.needed_bands(), + self.resource_limited + ) + geom = self._geobox.extent + if query.ignore_time: + times = None + else: + times = self._times + return self._layer.ows_index().extent(self._layer, times=times, geom=geom, products=query.products, crs=crs) + + def dsids(self) -> dict[ProductBandQuery, Iterable[UUID]]: + if self.style: + # we have a style - lets go with that. + queries = ProductBandQuery.style_queries(self.style) + else: + # Just take needed bands. + queries = [ProductBandQuery.simple_layer_query(self._layer, self.needed_bands())] + results: list[tuple[ProductBandQuery, Iterable[UUID]]] = [] + for query in queries: + if query.ignore_time: + qry_times = None + else: + qry_times = self._times + result = self._layer.ows_index().dsid_search(self._layer, times=qry_times, geom=self._geobox.extent, + products=query.products) + results.append((query, result)) + return OrderedDict(results) + + def datasets_all_time(self, point: Geometry | None = None) -> xarray.DataArray: + query = ProductBandQuery.simple_layer_query( self._layer, self.needed_bands(), self.resource_limited) + if point: + geom = point + else: + geom = self._geobox.extent + result = self._layer.ows_index().ds_search( + layer=self._layer, + geom=geom, + products=query.products) + grpd_result = datacube.Datacube.group_datasets( + cast(Iterable[datacube.model.Dataset], result), + self.group_by + ) + return grpd_result - ] - elif self.style: + def datasets(self, + all_flag_bands: bool = False, + point: Geometry | None = None, + ) -> dict[ProductBandQuery, xarray.DataArray]: + if self.style: # we have a style - lets go with that. queries = ProductBandQuery.style_queries(self.style) elif all_flag_bands: @@ -203,40 +242,22 @@ def datasets(self, index: datacube.index.Index, geom = point else: geom = self._geobox.extent - if all_time: - times = None - else: - times = self._times - results: list[tuple[ProductBandQuery, PerPBQReturnType]] = [] + results: list[tuple[ProductBandQuery, xarray.DataArray]] = [] for query in queries: if query.ignore_time: qry_times = None else: - qry_times = times - result = mv_search(index, - sel=mode, + qry_times = self._times + result = self._layer.ows_index().ds_search( + layer=self._layer, times=qry_times, geom=geom, products=query.products) - if mode == MVSelectOpts.DATASETS: - grpd_result = datacube.Datacube.group_datasets( - cast(Iterable[datacube.model.Dataset], result), - self.group_by - ) - if all_time: - return grpd_result - results.append((query, grpd_result)) - elif mode == MVSelectOpts.IDS: - result_ids = cast(Iterable[UUID], result) - if all_time: - return result_ids - results.append((query, result_ids)) - elif mode == MVSelectOpts.ALL: - return cast(Iterable[Row], result) - elif mode == MVSelectOpts.COUNT: - return cast(int, result) - else: # MVSelectOpts.EXTENT - return cast(Geometry | None, result) + grpd_result = datacube.Datacube.group_datasets( + cast(Iterable[datacube.model.Dataset], result), + self.group_by + ) + results.append((query, grpd_result)) return OrderedDict(results) def create_nodata_filled_flag_bands(self, data: xarray.Dataset, pbq: ProductBandQuery) -> xarray.Dataset: diff --git a/datacube_ows/mv_index.py b/datacube_ows/mv_index.py deleted file mode 100644 index 7668ca22d..000000000 --- a/datacube_ows/mv_index.py +++ /dev/null @@ -1,180 +0,0 @@ -# This file is part of datacube-ows, part of the Open Data Cube project. -# See https://opendatacube.org for more information. -# -# Copyright (c) 2017-2024 OWS Contributors -# SPDX-License-Identifier: Apache-2.0 - -import datetime -import json -from enum import Enum -from types import UnionType -from typing import Iterable, Type, cast -from uuid import UUID as UUID_ - -import pytz -from datacube.index import Index -from datacube.model import Dataset, Product -from geoalchemy2 import Geometry -from odc.geo.geom import Geometry as ODCGeom -from psycopg2.extras import DateTimeTZRange -from sqlalchemy import (SMALLINT, Column, MetaData, Table, and_, or_, select, - text) -from sqlalchemy.dialects.postgresql import TSTZRANGE, UUID -from sqlalchemy.engine import Row -from sqlalchemy.engine.base import Engine -from sqlalchemy.sql.elements import ClauseElement -from sqlalchemy.sql.functions import count, func - -from datacube_ows.utils import default_to_utc - - -def get_sqlalc_engine(index: Index) -> Engine: - # pylint: disable=protected-access - return index._db._engine # type: ignore[attr-defined] - - -def get_st_view(meta: MetaData) -> Table: - return Table('space_time_view', meta, - Column('id', UUID()), - Column('dataset_type_ref', SMALLINT()), - Column('spatial_extent', Geometry(from_text='ST_GeomFromGeoJSON', name='geometry')), - Column('temporal_extent', TSTZRANGE()), - schema="ows") - - -_meta = MetaData() -st_view = get_st_view(_meta) - - -class MVSelectOpts(Enum): - """ - Enum for mv_search_datasets sel parameter. - - ALL: return all columns, select *, as result set - IDS: return list of database_ids only. - DATASETS: return list of ODC dataset objects - COUNT: return a count of matching datasets - EXTENT: return full extent of query result as a Geometry - """ - ALL = 0 - IDS = 1 - COUNT = 2 - EXTENT = 3 - DATASETS = 4 - - def sel(self, stv: Table) -> list[ClauseElement]: - if self == self.ALL: - return [stv] - if self == self.IDS or self == self.DATASETS: - return [stv.c.id] - if self == self.COUNT: - return [cast(ClauseElement, count(stv.c.id))] - if self == self.EXTENT: - return [text("ST_AsGeoJSON(ST_Union(spatial_extent))")] - raise AssertionError("Invalid selection option") - - -selection_return_types: dict[MVSelectOpts, Type | UnionType] = { - MVSelectOpts.ALL: Iterable[Row], - MVSelectOpts.IDS: Iterable[UUID_], - MVSelectOpts.DATASETS: Iterable[Dataset], - MVSelectOpts.COUNT: int, - MVSelectOpts.EXTENT: ODCGeom | None, -} - - -SelectOut = Iterable[Row] | Iterable[UUID_] | Iterable[Dataset] | int | ODCGeom | None -DateOrDateTime = datetime.datetime | datetime.date -TimeSearchTerm = tuple[datetime.datetime, datetime.datetime] | tuple[datetime.date, datetime.date] | DateOrDateTime - - -def mv_search(index: Index, - sel: MVSelectOpts = MVSelectOpts.IDS, - times: Iterable[TimeSearchTerm] | None = None, - geom: ODCGeom | None = None, - products: Iterable[Product] | None = None) -> SelectOut: - """ - Perform a dataset query via the space_time_view - - :param products: An iterable of combinable products to search - :param index: A datacube index (required) - - :param sel: Selection mode - a MVSelectOpts enum. Defaults to IDS. - :param times: A list of pairs of datetimes (with time zone) - :param geom: A odc.geo.geom.Geometry object - - :return: See MVSelectOpts doc - """ - engine = get_sqlalc_engine(index) - stv = st_view - if products is None: - raise Exception("Must filter by product/layer") - prod_ids = [p.id for p in products] - - s = select(*sel.sel(stv)).where(stv.c.dataset_type_ref.in_(prod_ids)) # type: ignore[call-overload] - if times is not None: - or_clauses = [] - for t in times: - if isinstance(t, datetime.datetime): - st: datetime.datetime = datetime.datetime(t.year, t.month, t.day, t.hour, t.minute, t.second) - st = default_to_utc(t) - if not st.tzinfo: - st = st.replace(tzinfo=pytz.utc) - tmax = st + datetime.timedelta(seconds=1) - or_clauses.append( - and_( - func.lower(stv.c.temporal_extent) >= t, - func.lower(stv.c.temporal_extent) < tmax, - ) - ) - elif isinstance(t, datetime.date): - st = datetime.datetime(t.year, t.month, t.day, tzinfo=pytz.utc) - tmax = st + datetime.timedelta(days=1) - or_clauses.append( - and_( - func.lower(stv.c.temporal_extent) >= st, - func.lower(stv.c.temporal_extent) < tmax, - ) - ) - else: - or_clauses.append( - stv.c.temporal_extent.op("&&")(DateTimeTZRange(*t)) - ) - s = s.where(or_(*or_clauses)) - orig_crs = None - if geom is not None: - orig_crs = geom.crs - if str(geom.crs) != "EPSG:4326": - geom = geom.to_crs("EPSG:4326") - geom_js = json.dumps(geom.json) - s = s.where(stv.c.spatial_extent.intersects(geom_js)) - # print(s) # Print SQL Statement - with engine.connect() as conn: - if sel == MVSelectOpts.ALL: - return conn.execute(s) - elif sel == MVSelectOpts.IDS: - return [r[0] for r in conn.execute(s)] - elif sel in (MVSelectOpts.COUNT, MVSelectOpts.EXTENT): - for r in conn.execute(s): - if sel == MVSelectOpts.COUNT: - return cast(int, r[0]) - else: # MVSelectOpts.EXTENT - geojson = r[0] - if geojson is None: - return None - uniongeom = ODCGeom(json.loads(geojson), crs="EPSG:4326") - if geom: - intersect = uniongeom.intersection(geom) - if intersect.wkt == 'POLYGON EMPTY': - return None - if orig_crs and orig_crs != "EPSG:4326": - intersect = intersect.to_crs(orig_crs) - else: - intersect = uniongeom - return intersect - elif sel == MVSelectOpts.DATASETS: - ids = [r[0] for r in conn.execute(s)] - return index.datasets.bulk_get(ids) - else: - raise Exception("Invalid Selection Option") - raise Exception("Unreachable code reached") diff --git a/datacube_ows/ows_configuration.py b/datacube_ows/ows_configuration.py index 19913597f..de393b350 100644 --- a/datacube_ows/ows_configuration.py +++ b/datacube_ows/ows_configuration.py @@ -1,4 +1,3 @@ -# This file is part of datacube-ows, part of the Open Data Cube project. # See https://opendatacube.org for more information. # # Copyright (c) 2017-2024 OWS Contributors @@ -255,6 +254,10 @@ def __init__(self, cfg: CFG_DICT, object_label: str, parent_layer: Optional["OWS self._cached_local_env: ODCEnvironment | None = None self._cached_dc: Datacube | None = None self.parse_metadata(cfg) + # Do we have a local ODC environment override? + local_env = cfg.get("env") + if local_env is not None: + self._local_env = ODCConfig.get_environment(env=str(local_env)) # Inherit or override attribution if "attribution" in cfg: self.attribution = AttributionCfg.parse( # type: ignore[assignment] diff --git a/datacube_ows/sql/postgis/ows_schema/create/001_create_schema.sql b/datacube_ows/sql/postgis/ows_schema/create/001_create_schema.sql new file mode 100644 index 000000000..1862c31ec --- /dev/null +++ b/datacube_ows/sql/postgis/ows_schema/create/001_create_schema.sql @@ -0,0 +1,3 @@ +-- Creating/replacing ows schema + +create schema if not exists ows; diff --git a/datacube_ows/sql/postgis/ows_schema/create/002_create_product_rng.sql b/datacube_ows/sql/postgis/ows_schema/create/002_create_product_rng.sql new file mode 100644 index 000000000..bc947bacf --- /dev/null +++ b/datacube_ows/sql/postgis/ows_schema/create/002_create_product_rng.sql @@ -0,0 +1,17 @@ +-- Creating/replacing product ranges table + +create table if not exists ows.layer_ranges ( + layer varchar(255) not null primary key, + + lat_min decimal not null, + lat_max decimal not null, + lon_min decimal not null, + lon_max decimal not null, + + dates jsonb not null, + + bboxes jsonb not null, + + meta jsonb not null, + last_updated timestamp not null +); diff --git a/datacube_ows/sql/postgis/ows_schema/grants/read_only/001_grant_usage_requires_role.sql b/datacube_ows/sql/postgis/ows_schema/grants/read_only/001_grant_usage_requires_role.sql new file mode 100644 index 000000000..598fee7a2 --- /dev/null +++ b/datacube_ows/sql/postgis/ows_schema/grants/read_only/001_grant_usage_requires_role.sql @@ -0,0 +1,3 @@ +-- Granting usage on schema + +GRANT USAGE ON SCHEMA ows TO {role} diff --git a/datacube_ows/sql/postgis/ows_schema/grants/read_only/002_grant_range_read_requires_role.sql b/datacube_ows/sql/postgis/ows_schema/grants/read_only/002_grant_range_read_requires_role.sql new file mode 100644 index 000000000..dc1bc3ee0 --- /dev/null +++ b/datacube_ows/sql/postgis/ows_schema/grants/read_only/002_grant_range_read_requires_role.sql @@ -0,0 +1,3 @@ +-- Granting select on layer ranges table to {role} + +GRANT SELECT ON ows.layer_ranges TO {role}; diff --git a/datacube_ows/sql/postgis/ows_schema/grants/read_only/003_grant_odc_user_requires_role.sql b/datacube_ows/sql/postgis/ows_schema/grants/read_only/003_grant_odc_user_requires_role.sql new file mode 100644 index 000000000..1af4c9729 --- /dev/null +++ b/datacube_ows/sql/postgis/ows_schema/grants/read_only/003_grant_odc_user_requires_role.sql @@ -0,0 +1,3 @@ +-- Granting odc_user role to {role} + +GRANT odc_user to {role}; diff --git a/datacube_ows/sql/postgis/ows_schema/grants/read_write/001_grant_usage_requires_role.sql b/datacube_ows/sql/postgis/ows_schema/grants/read_write/001_grant_usage_requires_role.sql new file mode 100644 index 000000000..598fee7a2 --- /dev/null +++ b/datacube_ows/sql/postgis/ows_schema/grants/read_write/001_grant_usage_requires_role.sql @@ -0,0 +1,3 @@ +-- Granting usage on schema + +GRANT USAGE ON SCHEMA ows TO {role} diff --git a/datacube_ows/sql/postgis/ows_schema/grants/read_write/002_grant_writetables_requires_role.sql b/datacube_ows/sql/postgis/ows_schema/grants/read_write/002_grant_writetables_requires_role.sql new file mode 100644 index 000000000..6d7e3f30a --- /dev/null +++ b/datacube_ows/sql/postgis/ows_schema/grants/read_write/002_grant_writetables_requires_role.sql @@ -0,0 +1,3 @@ +-- Granting update/insert/delete on all tables in schema + +GRANT update, select, insert, delete ON ALL TABLES IN SCHEMA ows TO {role} diff --git a/datacube_ows/sql/postgis/ows_schema/grants/read_write/003_grant_odc_user_requires_role.sql b/datacube_ows/sql/postgis/ows_schema/grants/read_write/003_grant_odc_user_requires_role.sql new file mode 100644 index 000000000..1af4c9729 --- /dev/null +++ b/datacube_ows/sql/postgis/ows_schema/grants/read_write/003_grant_odc_user_requires_role.sql @@ -0,0 +1,3 @@ +-- Granting odc_user role to {role} + +GRANT odc_user to {role}; diff --git a/datacube_ows/sql/postgres/ows_schema/grants/read_write/003_grant_agdc_user_requires_role.sql b/datacube_ows/sql/postgres/ows_schema/grants/read_write/003_grant_agdc_user_requires_role.sql new file mode 100644 index 000000000..5783f1b9e --- /dev/null +++ b/datacube_ows/sql/postgres/ows_schema/grants/read_write/003_grant_agdc_user_requires_role.sql @@ -0,0 +1,3 @@ +-- Granting agdc_user role to {role} + +GRANT agdc_user to {role}; diff --git a/datacube_ows/styles/component.py b/datacube_ows/styles/component.py index b9efb0697..564242ffa 100644 --- a/datacube_ows/styles/component.py +++ b/datacube_ows/styles/component.py @@ -165,7 +165,7 @@ def transform_single_date_data(self, data: Dataset) -> Dataset: else: imgband_data = imgband_component if imgband_data is None: - null_np = np.zeros(tuple(data.sizes.values()), 'uint8') + null_np = np.zeros(tuple(data.sizes.values()), 'float64') imgband_data = DataArray(null_np, data.coords, tuple(data.sizes.keys())) if imgband != "alpha": imgband_data = self.compress_band(imgband, imgband_data) diff --git a/datacube_ows/update_ranges_impl.py b/datacube_ows/update_ranges_impl.py index e5059074c..5f5318956 100755 --- a/datacube_ows/update_ranges_impl.py +++ b/datacube_ows/update_ranges_impl.py @@ -31,7 +31,7 @@ help="(Only valid with --schema) Role(s) to grant both read and write/update database permissions to") @click.option("--cleanup", is_flag=True, default=False, help="Cleanup up any datacube-ows 1.8.x tables/views") -@click.option("-e", "--env", default=None, +@click.option("-E", "--env", default=None, help="(Only valid with --schema or --read-role or --write-role or --cleanup) environment to write to.") @click.option("--version", is_flag=True, default=False, help="Print version string and exit") @@ -127,10 +127,14 @@ def main(layers: list[str], app = cfg.odc_app + "-update" errors: bool = False if schema or read_role or write_role or cleanup or views: - if cfg.default_env and env is None: - dc = Datacube(env=cfg.default_env, app=app) - else: - dc = Datacube(env=env, app=app) + try: + if cfg.default_env and env is None: + dc = Datacube(env=cfg.default_env, app=app) + else: + dc = Datacube(env=env, app=app) + except: + click.echo(f"Unable to connect to the {env or cfg.default_env} database.") + sys.exit(1) click.echo(f"Applying database schema updates to the {dc.index.environment.db_database} database:...") try: @@ -167,7 +171,7 @@ def main(layers: list[str], click.echo("") click.echo(" Try running with the --schema options first.") sys.exit(1) - elif isinstance(e.orig, psycopg2.errors.NotNullViloation): + elif isinstance(e.orig, psycopg2.errors.NotNullViolation): click.echo("ERROR: OWS materialised views are most likely missing a newly indexed product") click.echo("") click.echo(" Try running with the --viewes options first.") diff --git a/datacube_ows/wcs1_utils.py b/datacube_ows/wcs1_utils.py index 2e0dc3d1a..93722529a 100644 --- a/datacube_ows/wcs1_utils.py +++ b/datacube_ows/wcs1_utils.py @@ -16,7 +16,6 @@ from datacube_ows.config_utils import ConfigException from datacube_ows.loading import DataStacker -from datacube_ows.mv_index import MVSelectOpts from datacube_ows.ogc_exceptions import WCS1Exception from datacube_ows.ows_configuration import get_config from datacube_ows.resource_limits import ResourceLimited @@ -312,7 +311,7 @@ def get_coverage_data(req, qprof): req.times, bands=req.bands) qprof.start_event("count-datasets") - n_datasets = stacker.datasets(req.layer.dc.index, mode=MVSelectOpts.COUNT) + n_datasets = stacker.n_datasets() qprof.end_event("count-datasets") qprof["n_datasets"] = n_datasets @@ -385,12 +384,12 @@ def get_coverage_data(req, qprof): return n_datasets, data qprof.start_event("fetch-datasets") - datasets = stacker.datasets(index=req.layer.dc.index) + datasets = stacker.datasets() qprof.end_event("fetch-datasets") if qprof.active: qprof["datasets"] = { str(q): [str(i) for i in ids] - for q, ids in stacker.datasets(req.layer.dc.index, mode=MVSelectOpts.IDS).items() + for q, ids in stacker.dsids().items() } qprof.start_event("load-data") output = stacker.data(datasets, skip_corrections=True) diff --git a/datacube_ows/wcs2_utils.py b/datacube_ows/wcs2_utils.py index 482a10f49..7911c6389 100644 --- a/datacube_ows/wcs2_utils.py +++ b/datacube_ows/wcs2_utils.py @@ -13,7 +13,6 @@ from rasterio import MemoryFile from datacube_ows.loading import DataStacker -from datacube_ows.mv_index import MVSelectOpts from datacube_ows.ogc_exceptions import WCS2Exception from datacube_ows.ows_configuration import get_config from datacube_ows.resource_limits import ResourceLimited @@ -257,7 +256,7 @@ def get_coverage_data(request, styles, qprof): bands=bands) qprof.end_event("setup") qprof.start_event("count-datasets") - n_datasets = stacker.datasets(layer.dc.index, mode=MVSelectOpts.COUNT) + n_datasets = stacker.n_datasets() qprof.end_event("count-datasets") qprof["n_datasets"] = n_datasets @@ -281,12 +280,12 @@ def get_coverage_data(request, styles, qprof): http_response=404) qprof.start_event("fetch-datasets") - datasets = stacker.datasets(layer.dc.index) + datasets = stacker.datasets() qprof.end_event("fetch-datasets") if qprof.active: qprof["datasets"] = { str(q): [str(i) for i in ids] - for q, ids in stacker.datasets(layer.dc.index, mode=MVSelectOpts.IDS).items() + for q, ids in stacker.dsids().items() } qprof.start_event("load-data") output = stacker.data(datasets, skip_corrections=True) diff --git a/docker-compose.db.yaml b/docker-compose.db.yaml index 58f17007e..7e722e5de 100644 --- a/docker-compose.db.yaml +++ b/docker-compose.db.yaml @@ -5,15 +5,13 @@ services: # db build: docker/database/ environment: - - POSTGRES_DB=${DB_DATABASE} - - POSTGRES_PASSWORD=${DB_PASSWORD} - - POSTGRES_USER=${DB_USERNAME} + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_USER=${POSTGRES_USER} ports: - - "${DB_PORT}:5432" + - "${POSTGRES_PORT}:${POSTGRES_PORT}" restart: always # Overwrite ows so it can talk to docker db ows: ports: - 8000:8000 - environment: - DB_PORT: 5432 diff --git a/docker-compose.yaml b/docker-compose.yaml index fdfb0e901..087e4d013 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,14 +21,15 @@ services: # Hard coded for now. ODC_ENVIRONMENT: default ODC_DEFAULT_INDEX_DRIVER: postgres + ODC_OWSPOSTGIS_INDEX_DRIVER: postgis # Please switch to single entry url configuration for postgres url ODC_DEFAULT_DB_URL: ${ODC_DEFAULT_DB_URL} - # Overridden by preferred URL entry above. - DB_HOSTNAME: ${DB_HOSTNAME} - DB_PORT: ${DB_PORT} - DB_USERNAME: ${DB_USERNAME} - DB_PASSWORD: ${DB_PASSWORD} - DB_DATABASE: ${DB_DATABASE} + ODC_OWSPOSTGIS_DB_URL: ${ODC_OWSPOSTGIS_DB_URL} + # for wait-for-db check + READY_PROBE_DB: ${READY_PROBE_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_HOSTNAME: ${POSTGRES_HOSTNAME} + SERVER_DB_USERNAME: ${SERVER_DB_USERNAME} # Path from the PYTHONPATH to the config object (default PYTHONPATH is /env) PYTHONPATH: ${PYTHONPATH} DATACUBE_OWS_CFG: ${DATACUBE_OWS_CFG} diff --git a/docker/ows/wait-for-db b/docker/ows/wait-for-db index 732e49707..f06c6a122 100755 --- a/docker/ows/wait-for-db +++ b/docker/ows/wait-for-db @@ -3,8 +3,8 @@ RETRIES=10 # Wait until Database is ready -until pg_isready --dbname=$DB_DATABASE --host=$DB_HOSTNAME --port=$DB_PORT --username=$DB_USERNAME || [ $RETRIES -eq 0 ]; do - echo "Waiting for $DB_HOSTNAME server, $((RETRIES-=1)) remaining attempts..." +until pg_isready --dbname=$READY_PROBE_DB --host=$POSTGRES_HOSTNAME --username=$POSTGRES_USER || [ $RETRIES -eq 0 ]; do + echo "Waiting for $READY_PROBE_DB databases, $((RETRIES-=1)) remaining attempts..." sleep 2 done diff --git a/docs/database.rst b/docs/database.rst index 030ce43cc..d430bb50d 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -14,20 +14,24 @@ System Architecture Diagram --------------------------- .. figure:: diagrams/ows_diagram1.9.png - :target: /_images/ows_diagram.png + :target: /_images/ows_diagram1.9.png OWS Architecture Diagram, including Database structure. Open Data Cube Native Entities ------------------------------ -The core of the Datacube-OWS application database is the ``agdc`` schema see: +The core of the Datacube-OWS application database is found in either +the ``agdc`` schema (for the legacy ``postgres`` index driver), or +the ``odc`` schema (for the new ``postgis`` index driver). +See `Open Data Cube `_. + This schema is created and maintained with the ``datacube`` command. OWS only needs read access to this schema. -Materialised Views over ODC Indexes ------------------------------------ +Materialised Views over ODC Indexes (Postgres driver only) +---------------------------------------------------------- The materialised views provide a dataset level extent index using `PostGIS `_ datatypes. @@ -37,13 +41,15 @@ step to populate the Layer Extent Cache, as described below, and for doing dataset queries for GetMap, GetFeatureInfo and GetCoverage requests. -This layer will eventually become folded up into functionality in core, but -in the meantime (and while using the legacy ``postgres`` index driver), -it must be maintained separately using the ``datacube-ows-update`` (``update_ranges)``) -command. +The materialised views must be manually updated whenever new data +is added to the underlying ODC index, as described below. + +With the new postgis index driver, the functionality provided by the +materialised views is available directly from the ODC index, and so +no materialised views are required. -Range Tables (Layer Extent Cache) ----------------------------------- +Ranges Table (Layer Extent Cache) +--------------------------------- Range tables serve as a cache of full-layer spatio-temporal extents for generating GetCapabilities documents efficiently. @@ -53,7 +59,7 @@ Creating/Maintaining the OWS Schema Creating or updating an OWS schema is performed with following options to ``datacube-ows-update``. -Note that the options in this schema requires database superuser privileges. +Note that the options in this section requires database superuser/admin privileges. =================================== Creating or Updating the OWS Schema @@ -64,6 +70,13 @@ updates to the form required by the installed version of ``datacube-ows``:: datacube-ows-update --schema +The ``--schema`` option creates/updates the OWS schema in the database defined by the ``default`` +ODC environment only. If you are using multiple ODC environments, or are simply not using the ``default`` +environment, you will need to pass the target environment name with the ``-E`` option. E.g. to +create an OWS schema in the ``myenv`` ODC environment:: + + datacube-ows-update -E myenv --schema + ========================================== Cleaning up an old datacube-ows 1.8 schema ========================================== @@ -80,19 +93,21 @@ You can combine the ``--schema`` and ``--cleanup`` options in the one invocation datacube-ows-update --schema --cleanup -The new schema is always created before dropping the new one, regardless of the order you specify the options. +The new schema is always created before dropping the old one, regardless of the order you specify the options. + +The ``--cleanup`` option targets the ``default`` ODC environment database unless a target environment is supplied +with the ``-E`` option. ====================================== Granting permissions to database roles ====================================== The ``datacube-ows`` web application requires permissions to read from the various tables and views in the ``ows`` -schema. These can permissions can be granted to a database role with the ``--read-role `` argument:: +schema. These can permissions (including read-access to the ODC tables can be granted to a database role with +the ``--read-role `` argument:: datacube-ows-update --read-role role1 -The role used by ``datacube-ows`` also needs read-access to the ODC tables. These should managed with the ``datacube users`` -CLI tool - refer to the datacube-core documentation. You do not need to use --read-role and --write-role on the same user - granting write permissions automatically grants read permissions as well. @@ -108,6 +123,10 @@ multiple roles:: datacube-ows-update --schema --cleanup --read-role role1 --read-role role2 --read-role role3 --write-role admin +The ``--read-role`` and ``--write-role`` options are executed against the ODC environment database identified +by the ``-E`` option (Default is ``default``):: + + datacube-ows-update -E myenv --read-role role1 --read-role role2 --read-role role3 --write-role admin Updating/Maintaining OWS data ----------------------------- @@ -125,7 +144,7 @@ manually refreshed, with the ``--views`` flag. ``datacube-ows-update --views`` -A lot of the speed of OWS comes from pushing +A lot of the speed of OWS with the ``postgres`` index driver comes from pushing expensive database calculations down into these materialised views, and refreshing them is slow and computationally expensive. Large, constantly updating databases will unavoidably have @@ -144,18 +163,31 @@ In a production environment you should not be refreshing views much more than 2 or 3 times a day unless your database is small (e.g. less than a few thousand datasets). +If working with multiple ODC environments/databases, you can specify which +environment to refresh the materialised views in with the ``-E`` option. +(The default is to use the ``default`` environment.) + +Materialised views are required for ``postgres`` index driver environments only. +In environments using the ``postgis`` index driver, the `--views` option +does nothing and may be skipped. + ===================== Updating range tables ===================== -The range table is updated from the materialised views by simply calling: +The range tables are updated from the materialised views by simply calling: datacube-ows-update Note that this operation is very fast and computationally light compared to refreshing the materialised views. -In a production environment, this should be run after refreshing the materialised views, as described above (after -waiting a couple of minutes for the final refresh to complete). +Range tables are updated in all ODC environments referenced by the active ODC configuration file. +The ``-E`` flag is therefore not valid for use with this calling mode. + +In a ``postgres`` driver production environment, this should be run after refreshing the materialised views, +as described above (after waiting a couple of minutes for the final refresh to complete). + +In a ``postgis`` driver production environment, this is the only required regular maintenance task. =========================================== Updating range tables for individual layers @@ -169,3 +201,6 @@ Specific layers can be updated using: This will need to be done after adding a new layer to the OWS configuration, or after changing the time resolution or the ODC product(s) of an existing layer. + +The target OWS database for each layer is determined from OWS configuration, so the ``-E`` flag is invalid +with this calling mode. diff --git a/docs/diagrams/ows_diagram1.9.png b/docs/diagrams/ows_diagram1.9.png index 93c0a77d059da1588f9ca97c67f3c67ae2dbe819..6fa7855ba53e240c644bb5f9177b7d8bdb8e9edf 100644 GIT binary patch delta 120928 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

f-Cl=n^Zb=v2<)HKakJ;EySI?xrQDZPq&6AG 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 QMbCGH_{4dOxHUcgKOAIQM*si- diff --git a/docs/environment_variables.rst b/docs/environment_variables.rst index b95a72177..c5b663dd6 100644 --- a/docs/environment_variables.rst +++ b/docs/environment_variables.rst @@ -22,17 +22,38 @@ environment variable. The format of postgres connection URL is:: postgresql://:@:/ +If you are using an ODC environment other than ``default`` or are using multiple ODC environments, +you can specify the url for other environments in the same fashion, e.g. for environment ``myenv`` +use ``$ODC_MYENV_DB_URL``. + +If you want to use a ``postgis`` based ODC index, you should also specify the index driver by +setting e.g. ``$ODC_MYENV_INDEX_DRIVER`` to ``postgis``. + Other valid methods for configuring an OpenDatacube instance (e.g. a ``.datacube.conf`` file) should also work. Note that OWS currently only works with legacy/postgres index driver. Postgis support is hopefully coming soon. +The old `$DB_HOSTNAME`, `$DB_DATABASE` etc. environment variables are now STRONGLY DEPRECATED as they +only work in a single-index environment. + An ODC environment other than ``default`` can be used by setting the ``env`` option in the global OWS configuration. -Note that ``docker-compose`` arrangement used for integration testing on github also redundantly requires -the ``$DB_USERNAME``, ``$DB_PASSWORD``, ``$DB_DATABSE`` and ``$DB_PORT`` environment variables to set up -the generic docker postgres container. If you are connecting to an existing database, these variables -are not required. +For Running Integration Tests +----------------------------- + +The integration tests need to be able to call a running a OWS server connected the test database +and running the version of the OWS codebase being tested. + +SERVER_URL: + The URL of the test server. Defaults to ``http://localhost:5000`` + +SERVER_DB_USERNAME: + This is the database username used by the test server to connect to the test database. Defaults to + the same database username being used by the integration tests themselves. + +Note that ``docker-compose`` arrangement used for integration testing on github + Configuring AWS Access ---------------------- @@ -111,11 +132,13 @@ Docker and Docker-compose ------------------------- The provided ``Dockerfile`` and ``docker-compose.yaml`` read additional -environment variables at build time. Please refer to the `README `_ +environment variables at build time. +Please refer to the `README `_ for further details. -environment variables exclusive for docker-compose +Environment variables exclusive for docker-compose -------------------------------------------------- + OWS_CFG_DIR: path to a folder containing ows config files anywhere on the local machine @@ -124,3 +147,16 @@ OWS_CFG_MOUNT_DIR: PYTHONPATH: PYTHONPATH to ows config file + +POSTGRES_DB: +POSTGRES_USER: +POSTGRES_PASSWORD: + The db superuser name and password for the postgis database container. + If multiple databases are required, use a comma-separated list of database names + +POSTGRES_HOSTNAME: + The name of the database server/container. + +READY_PROBE_DB: + The (single) database to use for the startup database readiness probe. Should be set to one of the + values in ``$POSTGRES_DB`` diff --git a/docs/installation.rst b/docs/installation.rst index 617f62ec9..c5fb9b3af 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -51,10 +51,10 @@ of the config file, and edit it to reflect your requirements. $ DATACUBE_OWS_CFG=ows_local_cfg.ows_cfg * We currently recommend using pip with pre-built binary packages. Create a - new python 3.6 or 3.7 virtualenv and run pip install against the supplied + new python 3.10+ virtualenv and run pip install against the supplied requirements.txt:: - pip install --pre -r requirements.txt + pip install -e .[all] To install datacube-ows, run: @@ -129,7 +129,7 @@ E.g. to set up a new database: $ docker exec -it datacube-ows_ows_1 bash ows_1$ datacube system init - ows_1$ datacube-ows-update --schema --role ubuntu + ows_1$ datacube-ows-update --schema --write-role ubuntu ows_1$ datacube-ows-update diff --git a/docs/usage.rst b/docs/usage.rst index 54ea90888..14655458f 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -92,7 +92,7 @@ Update extents of a new product or to update a product in Datacube to make it ea .. code-block:: console - $ datacube-ows-update --views --blocking + $ datacube-ows-update --views $ datacube-ows-update alos_palsar_mosaic Deploy the Digital Earth Africa OWS config available `here `_ diff --git a/integration_tests/cfg/ows_test_cfg.py b/integration_tests/cfg/ows_test_cfg.py index a1066f7bf..f5029ff5d 100644 --- a/integration_tests/cfg/ows_test_cfg.py +++ b/integration_tests/cfg/ows_test_cfg.py @@ -648,6 +648,11 @@ "horizontal_coord": "x", "vertical_coord": "y", }, + "EPSG:6933": { # Africa - invalid for our test data + "geographic": False, + "horizontal_coord": "x", + "vertical_coord": "y", + }, }, # If True the new EXPERIMENTAL materialised views are used for spatio-temporal extents. # If False (the default), the old "update_ranges" tables (and native ODC search methods) are used. @@ -731,9 +736,9 @@ # is also a coverage, that may be requested in WCS DescribeCoverage or WCS GetCoverage requests. "layers": [ { - "title": "s2", - "abstract": "Images from the sentinel 2 satellite", - "keywords": ["sentinel2"], + "title": "Postgres Data", + "abstract": "Data from the postgres test database", + "keywords": ["postgres"], "attribution": { # Attribution must contain at least one of ("title", "url" and "logo") # A human readable title for the attribution - e.g. the name of the attributed organisation @@ -752,200 +757,456 @@ "format": "image/png", }, }, - "label": "sentinel2", + "label": "postgres", "layers": [ { - "title": "Surface reflectance (Sentinel-2)", - "name": "s2_l2a", - "abstract": """layer s2_l2a""", - "product_name": "s2_l2a", - "bands": bands_sentinel, - "dynamic": True, - "resource_limits": reslim_continental, - "time_resolution": "subday", - "image_processing": { - "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", - "always_fetch_bands": [], - "manual_merge": False, # True - "apply_solar_corrections": False, + "title": "s2", + "abstract": "Images from the sentinel 2 satellite", + "keywords": ["sentinel2"], + "attribution": { + # Attribution must contain at least one of ("title", "url" and "logo") + # A human readable title for the attribution - e.g. the name of the attributed organisation + "title": "Open Data Cube - OWS", + # The associated - e.g. URL for the attributed organisation + "url": "https://www.opendatacube.org/", + # Logo image - e.g. for the attributed organisation + "logo": { + # Image width in pixels (optional) + "width": 268, + # Image height in pixels (optional) + "height": 68, + # URL for the logo image. (required if logo specified) + "url": "https://user-images.githubusercontent.com/4548530/112120795-b215b880-8c12-11eb-8bfa-1033961fb1ba.png", + # Image MIME type for the logo - should match type referenced in the logo url (required if logo specified.) + "format": "image/png", + }, }, - "flags": [ + "label": "sentinel2", + "layers": [ { - "band": "SCL", - "product": "s2_l2a", - "ignore_time": False, - "ignore_info_flags": [], - # This band comes from main product, so cannot set flags manual_merge independently - # "manual_merge": True, + "title": "Surface reflectance (Sentinel-2)", + "name": "s2_l2a", + "abstract": """layer s2_l2a""", + "product_name": "s2_l2a", + "bands": bands_sentinel, + "dynamic": True, + "resource_limits": reslim_continental, + "time_resolution": "subday", + "image_processing": { + "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", + "always_fetch_bands": [], + "manual_merge": False, # True + "apply_solar_corrections": False, + }, + "flags": [ + { + "band": "SCL", + "product": "s2_l2a", + "ignore_time": False, + "ignore_info_flags": [], + # This band comes from main product, so cannot set flags manual_merge independently + # "manual_merge": True, + }, + ], + "native_crs": "EPSG:3857", + "native_resolution": [30.0, -30.0], + "styling": { + "default_style": "simple_rgb", + "styles": styles_s2_list, + }, }, - ], - "native_crs": "EPSG:3857", - "native_resolution": [30.0, -30.0], - "styling": { - "default_style": "simple_rgb", - "styles": styles_s2_list, - }, + { + "inherits": { + "layer": "s2_l2a", + }, + "title": "s2_l2a Clone", + "abstract": "Imagery from the s2_l2a Clone", + "name": "s2_l2a_clone", + "low_res_product_name": "s2_l2a", + "image_processing": { + "extent_mask_func": [], + "manual_merge": True, + "apply_solar_corrections": True, + }, + "resource_limits": { + "wcs": { + "max_image_size": 2000 * 2000 * 3 * 2, + } + }, + "patch_url_function": f"{cfgbase}utils.trivial_identity", + }, + ] }, { - "inherits": { - "layer": "s2_l2a", - }, - "title": "s2_l2a Clone", - "abstract": "Imagery from the s2_l2a Clone", - "name": "s2_l2a_clone", - "low_res_product_name": "s2_l2a", - "image_processing": { - "extent_mask_func": [], - "manual_merge": True, - "apply_solar_corrections": True, - }, - "resource_limits": { - "wcs": { - "max_image_size": 2000 * 2000 * 3 * 2, + "title": "DEA Config Samples", + "abstract": "", + "layers": [ + { + "title": "DEA Surface Reflectance (Sentinel-2)", + "name": "s2_ard_granule_nbar_t", + "abstract": """Sentinel-2 Multispectral Instrument - Nadir BRDF Adjusted Reflectance + Terrain Illumination Correction (Sentinel-2 MSI) + This product has been corrected to account for variations caused by atmospheric properties, sun position and sensor view angle at time of image capture. + These corrections have been applied to all satellite imagery in the Sentinel-2 archive. This is undertaken to allow comparison of imagery acquired at different times, in different seasons and in different geographic locations. + These products also indicate where the imagery has been affected by cloud or cloud shadow, contains missing data or has been affected in other ways. The Surface Reflectance products are useful as a fundamental starting point for any further analysis, and underpin all other optical derived Digital Earth Australia products. + This is a definitive archive of daily Sentinel-2 data. This is processed using correct ancillary data to provide a more accurate product than the Near Real Time. + The Surface Reflectance product has been corrected to account for variations caused by atmospheric properties, sun position and sensor view angle at time of image capture. These corrections have been applied to all satellite imagery in the Sentinel-2 archive. + The Normalised Difference Chlorophyll Index (NDCI) is based on the method of Mishra & Mishra 2012, and adapted to bands on the Sentinel-2A & B sensors. + The index indicates levels of chlorophyll-a (chl-a) concentrations in complex turbid productive waters such as those encountered in many inland water bodies. The index has not been validated in Australian waters, and there are a range of environmental conditions that may have an effect on the accuracy of the derived index values in this test implementation, including: + - Influence on the remote sensing signal from nearby land and/or atmospheric effects + - Optically shallow water + - Cloud cover + Mishra, S., Mishra, D.R., 2012. Normalized difference chlorophyll index: A novel model for remote estimation of chlorophyll-a concentration in turbid productive waters. Remote Sensing of Environment, Remote Sensing of Urban Environments 117, 394–406. https://doi.org/10.1016/j.rse.2011.10.016 + For more information see http://pid.geoscience.gov.au/dataset/ga/129684 + https://cmi.ga.gov.au/data-products/dea/190/dea-surface-reflectance-nbart-sentinel-2-msi + For service status information, see https://status.dea.ga.gov.au + """, + "multi_product": True, + "product_names": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], + "low_res_product_names": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], + "bands": bands_sentinel2_ard_nbart, + "resource_limits": reslim_for_sentinel2, + "native_crs": "EPSG:3577", + "native_resolution": [10.0, -10.0], + "image_processing": { + "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", + "always_fetch_bands": [], + "manual_merge": False, + }, + "flags": [ + { + "band": "fmask_alias", + "products": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], + "ignore_time": False, + "ignore_info_flags": [] + }, + { + "band": "land", + "products": ["geodata_coast_100k", "geodata_coast_100k"], + "ignore_time": True, + "ignore_info_flags": [] + }, + ], + "time_axis": { + "time_interval": 1 + }, + "styling": {"default_style": "ndci", "styles": styles_s2_ga_list}, + }, + { + "inherits": { + "layer": "s2_ard_granule_nbar_t", + }, + "title": "DEA Surface Reflectance Mosaic (Sentinel-2)", + "name": "s2_ard_latest_mosaic", + "multi_product": True, + "abstract": """Sentinel-2 Multispectral Instrument - Nadir BRDF Adjusted Reflectance + Terrain Illumination Correction (Sentinel-2 MSI) + + Latest imagery mosaic with no time dimension. + """, + "mosaic_date_func": { + "function": "datacube_ows.time_utils.rolling_window_ndays", + "pass_layer_cfg": True, + "kwargs": { + "ndays": 6, + } + } + }, + { + "title": "DEA Fractional Cover (Landsat)", + "name": "ga_ls_fc_3", + "abstract": """Geoscience Australia Landsat Fractional Cover Collection 3 + Fractional Cover (FC), developed by the Joint Remote Sensing Research Program, is a measurement that splits the landscape into three parts, or fractions: + green (leaves, grass, and growing crops) + brown (branches, dry grass or hay, and dead leaf litter) + bare ground (soil or rock) + DEA uses Fractional Cover to characterise every 30 m square of Australia for any point in time from 1987 to today. + https://cmi.ga.gov.au/data-products/dea/629/dea-fractional-cover-landsat-c3 + For service status information, see https://status.dea.ga.gov.au""", + "product_name": "ga_ls_fc_3", + "bands": bands_fc_3, + "resource_limits": reslim_for_sentinel2, + "dynamic": True, + "native_crs": "EPSG:3577", + "native_resolution": [25, -25], + "image_processing": { + "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", + "always_fetch_bands": [], + "manual_merge": False, + }, + "flags": [ + # flags is now a list of flag band definitions - NOT a dictionary with identifiers + { + "band": "land", + "product": "geodata_coast_100k", + "ignore_time": True, + "ignore_info_flags": [], + }, + { + "band": "water", + "product": "ga_ls_wo_3", + "ignore_time": False, + "ignore_info_flags": [], + "fuse_func": "datacube_ows.wms_utils.wofls_fuser", + }, + ], + "styling": { + "default_style": "fc_rgb_unmasked", + "styles": [style_fc_c3_rgb_unmasked], + }, } - }, - "patch_url_function": f"{cfgbase}utils.trivial_identity", + ] }, - ] - }, - { - "title": "DEA Config Samples", - "abstract": "", - "layers": [ { - "title": "DEA Surface Reflectance (Sentinel-2)", - "name": "s2_ard_granule_nbar_t", - "abstract": """Sentinel-2 Multispectral Instrument - Nadir BRDF Adjusted Reflectance + Terrain Illumination Correction (Sentinel-2 MSI) - This product has been corrected to account for variations caused by atmospheric properties, sun position and sensor view angle at time of image capture. - These corrections have been applied to all satellite imagery in the Sentinel-2 archive. This is undertaken to allow comparison of imagery acquired at different times, in different seasons and in different geographic locations. - These products also indicate where the imagery has been affected by cloud or cloud shadow, contains missing data or has been affected in other ways. The Surface Reflectance products are useful as a fundamental starting point for any further analysis, and underpin all other optical derived Digital Earth Australia products. - This is a definitive archive of daily Sentinel-2 data. This is processed using correct ancillary data to provide a more accurate product than the Near Real Time. - The Surface Reflectance product has been corrected to account for variations caused by atmospheric properties, sun position and sensor view angle at time of image capture. These corrections have been applied to all satellite imagery in the Sentinel-2 archive. - The Normalised Difference Chlorophyll Index (NDCI) is based on the method of Mishra & Mishra 2012, and adapted to bands on the Sentinel-2A & B sensors. - The index indicates levels of chlorophyll-a (chl-a) concentrations in complex turbid productive waters such as those encountered in many inland water bodies. The index has not been validated in Australian waters, and there are a range of environmental conditions that may have an effect on the accuracy of the derived index values in this test implementation, including: - - Influence on the remote sensing signal from nearby land and/or atmospheric effects - - Optically shallow water - - Cloud cover - Mishra, S., Mishra, D.R., 2012. Normalized difference chlorophyll index: A novel model for remote estimation of chlorophyll-a concentration in turbid productive waters. Remote Sensing of Environment, Remote Sensing of Urban Environments 117, 394–406. https://doi.org/10.1016/j.rse.2011.10.016 - For more information see http://pid.geoscience.gov.au/dataset/ga/129684 - https://cmi.ga.gov.au/data-products/dea/190/dea-surface-reflectance-nbart-sentinel-2-msi - For service status information, see https://status.dea.ga.gov.au - """, - "multi_product": True, - "product_names": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], - "low_res_product_names": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], - "bands": bands_sentinel2_ard_nbart, + "title": "Landsat-8 Geomedian", + "name": "ls8_geomedian", + "abstract": """DEA Landsat-8 Geomedian""", + "product_name": "ga_ls8c_nbart_gm_cyear_3", + "bands": bands_c3_ls, "resource_limits": reslim_for_sentinel2, + "dynamic": False, "native_crs": "EPSG:3577", - "native_resolution": [10.0, -10.0], + "time_resolution": "summary", + "native_resolution": [25, -25], "image_processing": { "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", "always_fetch_bands": [], "manual_merge": False, }, - "flags": [ + "styling": { + "default_style": "simple_rgb", + "styles": styles_ls_list, + }, + } + ] ####### End of "postgres" layers + }, + { + "title": "Postgis Data", + "abstract": "Data from the postgis test database", + "keywords": ["postgis"], + "attribution": { + # Attribution must contain at least one of ("title", "url" and "logo") + # A human readable title for the attribution - e.g. the name of the attributed organisation + "title": "Open Data Cube - OWS", + # The associated - e.g. URL for the attributed organisation + "url": "https://www.opendatacube.org/", + # Logo image - e.g. for the attributed organisation + "logo": { + # Image width in pixels (optional) + "width": 268, + # Image height in pixels (optional) + "height": 68, + # URL for the logo image. (required if logo specified) + "url": "https://user-images.githubusercontent.com/4548530/112120795-b215b880-8c12-11eb-8bfa-1033961fb1ba.png", + # Image MIME type for the logo - should match type referenced in the logo url (required if logo specified.) + "format": "image/png", + }, + }, + "label": "postgis", + "layers": [ + { + "title": "s2 (postgis)", + "abstract": "Images from the sentinel 2 satellite (postgis db)", + "keywords": ["sentinel2"], + "label": "sentinel2_pgis", + "layers": [ { - "band": "fmask_alias", - "products": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], - "ignore_time": False, - "ignore_info_flags": [] + "title": "Surface reflectance (Sentinel-2) (postgis db)", + "name": "s2_l2a_postgis", + "abstract": """layer s2_l2a (postgis db)""", + "product_name": "s2_l2a", + "bands": bands_sentinel, + "env": "owspostgis", + "dynamic": True, + "resource_limits": reslim_continental, + "time_resolution": "subday", + "image_processing": { + "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", + "always_fetch_bands": [], + "manual_merge": False, # True + "apply_solar_corrections": False, + }, + "flags": [ + { + "band": "SCL", + "product": "s2_l2a", + "ignore_time": False, + "ignore_info_flags": [], + # This band comes from main product, so cannot set flags manual_merge independently + # "manual_merge": True, + }, + ], + "native_crs": "EPSG:3857", + "native_resolution": [30.0, -30.0], + "styling": { + "default_style": "simple_rgb", + "styles": styles_s2_list, + }, }, { - "band": "land", - "products": ["geodata_coast_100k", "geodata_coast_100k"], - "ignore_time": True, - "ignore_info_flags": [] + "inherits": { + "layer": "s2_l2a_postgis", + }, + "title": "s2_l2a Clone (postgis db)", + "abstract": "Imagery from the s2_l2a Clone (postgis db)", + "name": "s2_l2a_clone_postgis", + "env": "owspostgis", + "low_res_product_name": "s2_l2a", + "image_processing": { + "extent_mask_func": [], + "manual_merge": True, + "apply_solar_corrections": True, + }, + "resource_limits": { + "wcs": { + "max_image_size": 2000 * 2000 * 3 * 2, + } + }, + "patch_url_function": f"{cfgbase}utils.trivial_identity", }, - ], - "time_axis": { - "time_interval": 1 - }, - "styling": {"default_style": "ndci", "styles": styles_s2_ga_list}, + ] }, { - "inherits": { - "layer": "s2_ard_granule_nbar_t", - }, - "title": "DEA Surface Reflectance Mosaic (Sentinel-2)", - "name": "s2_ard_latest_mosaic", - "multi_product": True, - "abstract": """Sentinel-2 Multispectral Instrument - Nadir BRDF Adjusted Reflectance + Terrain Illumination Correction (Sentinel-2 MSI) - -Latest imagery mosaic with no time dimension. - """, - "mosaic_date_func": { - "function": "datacube_ows.time_utils.rolling_window_ndays", - "pass_layer_cfg": True, - "kwargs": { - "ndays": 6, + "title": "DEA Config Samples", + "abstract": "", + "layers": [ + { + "title": "DEA Surface Reflectance (Sentinel-2) (postgis db)", + "name": "s2_ard_granule_nbar_t_postgis", + "abstract": """Sentinel-2 Multispectral Instrument - Nadir BRDF Adjusted Reflectance + Terrain Illumination Correction (Sentinel-2 MSI) + This product has been corrected to account for variations caused by atmospheric properties, sun position and sensor view angle at time of image capture. + These corrections have been applied to all satellite imagery in the Sentinel-2 archive. This is undertaken to allow comparison of imagery acquired at different times, in different seasons and in different geographic locations. + These products also indicate where the imagery has been affected by cloud or cloud shadow, contains missing data or has been affected in other ways. The Surface Reflectance products are useful as a fundamental starting point for any further analysis, and underpin all other optical derived Digital Earth Australia products. + This is a definitive archive of daily Sentinel-2 data. This is processed using correct ancillary data to provide a more accurate product than the Near Real Time. + The Surface Reflectance product has been corrected to account for variations caused by atmospheric properties, sun position and sensor view angle at time of image capture. These corrections have been applied to all satellite imagery in the Sentinel-2 archive. + The Normalised Difference Chlorophyll Index (NDCI) is based on the method of Mishra & Mishra 2012, and adapted to bands on the Sentinel-2A & B sensors. + The index indicates levels of chlorophyll-a (chl-a) concentrations in complex turbid productive waters such as those encountered in many inland water bodies. The index has not been validated in Australian waters, and there are a range of environmental conditions that may have an effect on the accuracy of the derived index values in this test implementation, including: + - Influence on the remote sensing signal from nearby land and/or atmospheric effects + - Optically shallow water + - Cloud cover + Mishra, S., Mishra, D.R., 2012. Normalized difference chlorophyll index: A novel model for remote estimation of chlorophyll-a concentration in turbid productive waters. Remote Sensing of Environment, Remote Sensing of Urban Environments 117, 394–406. https://doi.org/10.1016/j.rse.2011.10.016 + For more information see http://pid.geoscience.gov.au/dataset/ga/129684 + https://cmi.ga.gov.au/data-products/dea/190/dea-surface-reflectance-nbart-sentinel-2-msi + For service status information, see https://status.dea.ga.gov.au (postgis db) + """, + "multi_product": True, + "product_names": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], + "low_res_product_names": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], + "bands": bands_sentinel2_ard_nbart, + "env": "owspostgis", + "resource_limits": reslim_for_sentinel2, + "native_crs": "EPSG:3577", + "native_resolution": [10.0, -10.0], + "image_processing": { + "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", + "always_fetch_bands": [], + "manual_merge": False, + }, + "flags": [ + { + "band": "fmask_alias", + "products": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], + "ignore_time": False, + "ignore_info_flags": [] + }, + { + "band": "land", + "products": ["geodata_coast_100k", "geodata_coast_100k"], + "ignore_time": True, + "ignore_info_flags": [] + }, + ], + "time_axis": { + "time_interval": 1 + }, + "styling": {"default_style": "ndci", "styles": styles_s2_ga_list}, + }, + { + "inherits": { + "layer": "s2_ard_granule_nbar_t_postgis", + }, + "title": "DEA Surface Reflectance Mosaic (Sentinel-2) (postgis)", + "name": "s2_ard_latest_mosaic_postgis", + "multi_product": True, + "abstract": """Sentinel-2 Multispectral Instrument - Nadir BRDF Adjusted Reflectance + Terrain Illumination Correction (Sentinel-2 MSI) + + Latest imagery mosaic with no time dimension. (postgis db) + """, + "mosaic_date_func": { + "function": "datacube_ows.time_utils.rolling_window_ndays", + "pass_layer_cfg": True, + "kwargs": { + "ndays": 6, + } + } + }, + { + "title": "DEA Fractional Cover (Landsat) (postgis db)", + "name": "ga_ls_fc_3_postgis", + "abstract": """Geoscience Australia Landsat Fractional Cover Collection 3 + Fractional Cover (FC), developed by the Joint Remote Sensing Research Program, is a measurement that splits the landscape into three parts, or fractions: + green (leaves, grass, and growing crops) + brown (branches, dry grass or hay, and dead leaf litter) + bare ground (soil or rock) + DEA uses Fractional Cover to characterise every 30 m square of Australia for any point in time from 1987 to today. + https://cmi.ga.gov.au/data-products/dea/629/dea-fractional-cover-landsat-c3 + For service status information, see https://status.dea.ga.gov.au (postgis db)""", + "product_name": "ga_ls_fc_3", + "bands": bands_fc_3, + "resource_limits": reslim_for_sentinel2, + "env": "owspostgis", + "dynamic": True, + "native_crs": "EPSG:3577", + "native_resolution": [25, -25], + "image_processing": { + "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", + "always_fetch_bands": [], + "manual_merge": False, + }, + "flags": [ + # flags is now a list of flag band definitions - NOT a dictionary with identifiers + { + "band": "land", + "product": "geodata_coast_100k", + "ignore_time": True, + "ignore_info_flags": [], + }, + { + "band": "water", + "product": "ga_ls_wo_3", + "ignore_time": False, + "ignore_info_flags": [], + "fuse_func": "datacube_ows.wms_utils.wofls_fuser", + }, + ], + "styling": { + "default_style": "fc_rgb_unmasked", + "styles": [style_fc_c3_rgb_unmasked], + }, } - } + ] }, { - "title": "DEA Fractional Cover (Landsat)", - "name": "ga_ls_fc_3", - "abstract": """Geoscience Australia Landsat Fractional Cover Collection 3 - Fractional Cover (FC), developed by the Joint Remote Sensing Research Program, is a measurement that splits the landscape into three parts, or fractions: - green (leaves, grass, and growing crops) - brown (branches, dry grass or hay, and dead leaf litter) - bare ground (soil or rock) - DEA uses Fractional Cover to characterise every 30 m square of Australia for any point in time from 1987 to today. - https://cmi.ga.gov.au/data-products/dea/629/dea-fractional-cover-landsat-c3 - For service status information, see https://status.dea.ga.gov.au""", - "product_name": "ga_ls_fc_3", - "bands": bands_fc_3, + "title": "Landsat-8 Geomedian (postgis db)", + "name": "ls8_geomedian_postgis", + "abstract": """DEA Landsat-8 Geomedian (postgis db)""", + "product_name": "ga_ls8c_nbart_gm_cyear_3", + "bands": bands_c3_ls, + "env": "owspostgis", "resource_limits": reslim_for_sentinel2, - "dynamic": True, + "dynamic": False, "native_crs": "EPSG:3577", + "time_resolution": "summary", "native_resolution": [25, -25], "image_processing": { "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", "always_fetch_bands": [], "manual_merge": False, }, - "flags": [ - # flags is now a list of flag band definitions - NOT a dictionary with identifiers - { - "band": "land", - "product": "geodata_coast_100k", - "ignore_time": True, - "ignore_info_flags": [], - }, - { - "band": "water", - "product": "ga_ls_wo_3", - "ignore_time": False, - "ignore_info_flags": [], - "fuse_func": "datacube_ows.wms_utils.wofls_fuser", - }, - ], "styling": { - "default_style": "fc_rgb_unmasked", - "styles": [style_fc_c3_rgb_unmasked], + "default_style": "simple_rgb", + "styles": styles_ls_list, }, } - ] + ] ####### End of "postgis" layers }, - { - "title": "Landsat-8 Geomedian", - "name": "ls8_geomedian", - "abstract": """DEA Landsat-8 Geomedian""", - "product_name": "ga_ls8c_nbart_gm_cyear_3", - "bands": bands_c3_ls, - "resource_limits": reslim_for_sentinel2, - "dynamic": False, - "native_crs": "EPSG:3577", - "time_resolution": "summary", - "native_resolution": [25, -25], - "image_processing": { - "extent_mask_func": "datacube_ows.ogc_utils.mask_by_val", - "always_fetch_bands": [], - "manual_merge": False, - }, - "styling": { - "default_style": "simple_rgb", - "styles": styles_ls_list, - }, - } - ], ##### End of "layers" list. + ] ##### End of "layers" list. } #### End of test configuration object diff --git a/integration_tests/cfg/test_translations/de/LC_MESSAGES/ows_cfg.po b/integration_tests/cfg/test_translations/de/LC_MESSAGES/ows_cfg.po new file mode 100644 index 000000000..2689c7196 --- /dev/null +++ b/integration_tests/cfg/test_translations/de/LC_MESSAGES/ows_cfg.po @@ -0,0 +1,31 @@ +# German translations for PROJECT. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-07-03 17:29+1000\n" +"PO-Revision-Date: 2024-07-03 17:29+1000\n" +"Last-Translator: FULL NAME \n" +"Language: de\n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.15.0\n" + +msgid "global.title" +msgstr "Over-ridden: aardvark" + +msgid "folder.sentinel2.abstract" +msgstr "Over-ridden: bunny-rabbit" + +msgid "layer.s2_l2a.title" +msgstr "Over-ridden: chook" + +msgid "style.s2_l2a.simple_rgb.title" +msgstr "Over-ridden: donkey" diff --git a/integration_tests/cfg/test_translations/en/LC_MESSAGES/ows_cfg.po b/integration_tests/cfg/test_translations/en/LC_MESSAGES/ows_cfg.po new file mode 100644 index 000000000..1091369fe --- /dev/null +++ b/integration_tests/cfg/test_translations/en/LC_MESSAGES/ows_cfg.po @@ -0,0 +1,31 @@ +# English translations for PROJECT. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-07-03 17:29+1000\n" +"PO-Revision-Date: 2024-07-03 17:29+1000\n" +"Last-Translator: FULL NAME \n" +"Language: en\n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.15.0\n" + +msgid "global.title" +msgstr "Over-ridden: aardvark" + +msgid "folder.sentinel2.abstract" +msgstr "Over-ridden: bunny-rabbit" + +msgid "layer.s2_l2a.title" +msgstr "Over-ridden: chook" + +msgid "style.s2_l2a.simple_rgb.title" +msgstr "Over-ridden: donkey" diff --git a/integration_tests/cfg/translations/de/LC_MESSAGES/ows_cfg.po b/integration_tests/cfg/translations/de/LC_MESSAGES/ows_cfg.po index 4677bba88..8d40721c4 100644 --- a/integration_tests/cfg/translations/de/LC_MESSAGES/ows_cfg.po +++ b/integration_tests/cfg/translations/de/LC_MESSAGES/ows_cfg.po @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: Open web-services for the Open Data Cube " "2022-03-24T23:29:57.407805\n" "Report-Msgid-Bugs-To: test@example.com\n" -"POT-Creation-Date: 2022-08-26 13:23+1000\n" +"POT-Creation-Date: 2024-07-03 17:29+1000\n" "PO-Revision-Date: 2022-03-24 23:33+0000\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.15.0\n" msgid "global.title" msgstr "This is the German translation of the title" diff --git a/integration_tests/cfg/translations/en/LC_MESSAGES/ows_cfg.mo b/integration_tests/cfg/translations/en/LC_MESSAGES/ows_cfg.mo index 5c23f027191b2da035d1e76d1d34e3947249da48..56adeb3ed55b0c8963bd1e6c496544e8b4685776 100644 GIT binary patch delta 34 pcmey&`k8fuJ0q`&u7SC(fw6+2xs{RS<`6~&MrKnzgURJgH2|ho2*LmW delta 34 pcmey&`k8fuJ0q`=u7QQFk(q*_v6YeW<`6~&MrH#&\n" "Language: en\n" diff --git a/integration_tests/conftest.py b/integration_tests/conftest.py index 6fdf3982d..ac18b457d 100644 --- a/integration_tests/conftest.py +++ b/integration_tests/conftest.py @@ -96,11 +96,19 @@ def product_name(): @pytest.fixture -def role_name(): +def write_role_name(): odc_env = ODCConfig.get_environment() return odc_env.db_username +@pytest.fixture +def read_role_name(write_role_name): + if read_role_name := os.environ.get("SERVER_DB_USERNAME"): + return read_role_name + else: + return write_role_name + + @pytest.fixture def multiproduct_name(): return "s2_ard_granule_nbar_t" diff --git a/integration_tests/metadata/COAST_100K_15_-40.yaml b/integration_tests/metadata/COAST_100K_15_-40.yaml new file mode 100644 index 000000000..d13bb3075 --- /dev/null +++ b/integration_tests/metadata/COAST_100K_15_-40.yaml @@ -0,0 +1,41 @@ +$schema: https://schemas.opendatacube.org/dataset +crs: epsg:3577 +extent: + lat: + begin: -35.72031832673588 + end: -34.70891190784338 + lon: + begin: 148.63868967606328 + end: 149.73417611149185 +grids: + default: + shape: + - 4000 + - 4000 + transform: + - 25.0 + - 0 + - 1500000.0 + - 0 + - -25.0 + - -3900000.0 + - 0 + - 0 + - 1 +id: 2921b863-9c09-4d5b-a561-5b8d5ddf24bc +label: COAST_100L_15_-40 +lineage: + source_datasets: {} +measurements: + land: + path: COAST_100K_15_-40.tif +product: + name: geodata_coast_100k +properties: + created: '2018-12-03T04:28:16.827902' + datetime: '2004-01-01T00:00:00' + odc:file_format: GeoTIFF + odc:region_code: 15_-40 + eo:instrument: unknown + eo:platform: unknown + proj:epsg: 3577 diff --git a/integration_tests/metadata/COAST_100K_8_-21.yaml b/integration_tests/metadata/COAST_100K_8_-21.yaml new file mode 100644 index 000000000..88ff130d8 --- /dev/null +++ b/integration_tests/metadata/COAST_100K_8_-21.yaml @@ -0,0 +1,41 @@ +$schema: https://schemas.opendatacube.org/dataset +crs: epsg:3577 +extent: + lat: + begin: -19.38776011021664 + end: -18.43108812058793 + lon: + begin: 139.58869840733868 + end: 140.59834925204385 +grids: + default: + shape: + - 4000 + - 4000 + transform: + - 25.0 + - 0 + - 800000.0 + - 0 + - -25.0 + - -2000000.0 + - 0 + - 0 + - 1 +id: 701478bc-2625-4743-99bf-10865d3bb2da +label: COAST_100K_8_-21 +lineage: + source_datasets: {} +measurements: + land: + path: COAST_100K_8_-21.tif +product: + name: geodata_coast_100k +properties: + created: '2018-12-03T04:31:43.146830' + datetime: '2004-01-01T00:00:00' + odc:file_format: GeoTIFF + odc:region_code: 8_-21 + eo:instrument: unknown + eo:platform: unknown + proj:epsg: 3577 diff --git a/integration_tests/metadata/metadata_importer.py b/integration_tests/metadata/metadata_importer.py index ec88df60f..7f8c3a128 100644 --- a/integration_tests/metadata/metadata_importer.py +++ b/integration_tests/metadata/metadata_importer.py @@ -7,8 +7,10 @@ from datacube.index.hl import Doc2Dataset dc = Datacube() +dc_pgis = Datacube(env="owspostgis") -doc2ds = Doc2Dataset(dc.index, products=["s2_l2a"], skip_lineage=True, verify_lineage=False) +doc2ds = Doc2Dataset(dc.index, products=["s2_l2a", "geodata_coast_100k"], skip_lineage=True, verify_lineage=False) +doc2ds_pgis = Doc2Dataset(dc_pgis.index, products=["s2_l2a", "geodata_coast_100k"], skip_lineage=True, verify_lineage=False) for line in fileinput.input(): filename, uri = line.split() @@ -20,7 +22,14 @@ del doc["extent"] ds, err = doc2ds(doc, uri) if ds: - dc.index.datasets.add(ds) + dc.index.datasets.add(ds, with_lineage=False) else: - print("Dataset add failed:", err) + print("Dataset add (postgres) failed:", err) + exit(1) + + ds, err = doc2ds_pgis(doc, uri) + if ds: + dc_pgis.index.datasets.add(ds, with_lineage=False) + else: + print("Dataset add (postgis) failed:", err) exit(1) diff --git a/integration_tests/metadata/product_geodata_coast_100k.yaml b/integration_tests/metadata/product_geodata_coast_100k.yaml new file mode 100644 index 000000000..69e7585cf --- /dev/null +++ b/integration_tests/metadata/product_geodata_coast_100k.yaml @@ -0,0 +1,37 @@ +name: geodata_coast_100k +description: Coastline data for Australia +metadata_type: eo3 +license: CC-BY-4.0 +metadata: + product: + name: geodata_coast_100k +measurements: +- name: land + dtype: uint8 + flags_definition: + land_type: + bits: + - 0 + - 1 + description: Sea, Mainland or Island + values: + 0: sea + 1: island + 2: mainland + sea: + bits: + - 0 + - 1 + description: Sea + values: + 0: true + nodata: 0 + units: '1' +load: + crs: 'EPSG:3577' + resolution: + y: -25 + x: 25 + align: + y: 0 + x: 0 diff --git a/integration_tests/test_mv_index.py b/integration_tests/test_mv_index.py index 234f719a1..ea158d004 100644 --- a/integration_tests/test_mv_index.py +++ b/integration_tests/test_mv_index.py @@ -7,7 +7,7 @@ import pytest from odc.geo.geom import box -from datacube_ows.mv_index import MVSelectOpts, mv_search +from datacube_ows.index.postgres.mv_index import MVSelectOpts, mv_search from datacube_ows.ows_configuration import get_config from datacube_ows.time_utils import local_solar_date_range diff --git a/integration_tests/test_update_ranges.py b/integration_tests/test_update_ranges.py index 6834fb56e..b333ffae0 100644 --- a/integration_tests/test_update_ranges.py +++ b/integration_tests/test_update_ranges.py @@ -16,18 +16,38 @@ def test_update_ranges_schema_without_roles(runner): assert "Insufficient Privileges" not in result.output assert "Cannot find SQL resource" not in result.output assert result.exit_code == 0 + result = runner.invoke(main, ["-E", "owspostgis", "--schema"]) + assert "appear to be missing" not in result.output + assert "Insufficient Privileges" not in result.output + assert "Cannot find SQL resource" not in result.output + assert result.exit_code == 0 -def test_update_ranges_schema_with_roles(runner, role_name): - result = runner.invoke(main, ["--schema", "--read-role", role_name, "--write-role", role_name]) +def test_update_ranges_schema_with_roles(runner, read_role_name, write_role_name): + result = runner.invoke(main, ["--schema", "--read-role", read_role_name, "--write-role", write_role_name]) assert "appear to be missing" not in result.output assert "Insufficient Privileges" not in result.output assert "Cannot find SQL resource" not in result.output assert result.exit_code == 0 + result = runner.invoke(main, ["-E", "owspostgis", + "--schema", "--read-role", read_role_name, "--write-role", write_role_name]) + assert "appear to be missing" not in result.output + assert "Insufficient Privileges" not in result.output + assert "Cannot find SQL resource" not in result.output + assert result.exit_code == 0 + result = runner.invoke(main, ["-E", "nonononodontcallanenviornmentthis", + "--schema", "--read-role", read_role_name, "--write-role", write_role_name]) + assert "Unable to connect to the nonono" in result.output + assert result.exit_code == 1 -def test_update_ranges_roles_only(runner, role_name): - result = runner.invoke(main, ["--read-role", role_name, "--write-role", role_name]) +def test_update_ranges_roles_only(runner, read_role_name, write_role_name): + result = runner.invoke(main, ["--read-role", read_role_name, "--write-role", write_role_name]) + assert "appear to be missing" not in result.output + assert "Insufficient Privileges" not in result.output + assert "Cannot find SQL resource" not in result.output + assert result.exit_code == 0 + result = runner.invoke(main, ["-E", "owspostgis", "--read-role", read_role_name, "--write-role", write_role_name]) assert "appear to be missing" not in result.output assert "Insufficient Privileges" not in result.output assert "Cannot find SQL resource" not in result.output diff --git a/integration_tests/test_version.py b/integration_tests/test_version.py index d6e98a75b..2d437d999 100644 --- a/integration_tests/test_version.py +++ b/integration_tests/test_version.py @@ -11,7 +11,7 @@ from datacube_ows.update_ranges_impl import main -def test_updates_ranges_schema(runner, role_name): +def test_updates_ranges_version(runner): result = runner.invoke(main, ["--version"]) assert __version__ in result.output assert result.exit_code == 0 diff --git a/integration_tests/test_wcs_server.py b/integration_tests/test_wcs_server.py index ad7a03a67..4a845b6fd 100644 --- a/integration_tests/test_wcs_server.py +++ b/integration_tests/test_wcs_server.py @@ -1194,20 +1194,24 @@ def test_wcs20_getcoverage_multidate_netcdf(ows_server): # Ensure that we have at least some layers available contents = list(wcs.contents) - assert len(contents) == 6 - layer = cfg.layer_index[contents[0]] - extent = ODCExtent(layer) - subsets = extent.wcs2_subsets( - ODCExtent.OFFSET_SUBSET_FOR_TIMES, ODCExtent.FIRST_TWO, crs="EPSG:4326" - ) - resp = wcs.getCoverage( - identifier=[contents[0]], - format="application/x-netcdf", - subsets=subsets, - subsettingcrs="EPSG:4326", - scalesize="x(400),y(300)", - ) - assert resp + assert len(contents) == 12 + for i in (0, 11): + layer = cfg.layer_index[contents[i]] + extent = ODCExtent(layer) + subsets = extent.wcs2_subsets( + ODCExtent.OFFSET_SUBSET_FOR_TIMES, ODCExtent.FIRST_TWO, crs="EPSG:4326" + ) + if len(subsets[2]) < 2: + continue + resp = wcs.getCoverage( + identifier=[layer.name], + format="application/x-netcdf", + subsets=subsets, + subsettingcrs="EPSG:4326", + scalesize="x(400),y(300)", + timeout=90 + ) + assert resp def test_wcs21_server(ows_server): # N.B. At time of writing owslib does not support WCS 2.1, so we have to make requests manually. diff --git a/integration_tests/utils.py b/integration_tests/utils.py index 107b1a6c3..c91bdede7 100644 --- a/integration_tests/utils.py +++ b/integration_tests/utils.py @@ -9,8 +9,6 @@ from odc.geo.geom import BoundingBox, Geometry, point from shapely.ops import triangulate, unary_union -from datacube_ows.mv_index import MVSelectOpts, mv_search - class WCS20Extent: def __init__(self, desc_cov): @@ -227,8 +225,8 @@ def subset(self, time_extent, full_extent): hslice_bbox = BoundingBox( left=bbox.left, right=bbox.right, - top=offset_y + 0.02 * height, - bottom=offset_y - 0.02 * height, + top=offset_y + 0.01 * height, + bottom=offset_y - 0.01 * height, ) hslice_geom = geom_from_bbox(hslice_bbox) hslice_geom = hslice_geom.intersection(time_extent) @@ -236,7 +234,7 @@ def subset(self, time_extent, full_extent): hslice_bbox = BoundingBox( left=bbox.left, right=bbox.right, - top=bbox.bottom + 0.02 * height, + top=bbox.bottom + 0.01 * height, bottom=bbox.bottom, ) hslice_geom = geom_from_bbox(hslice_bbox) @@ -255,8 +253,8 @@ def subset(self, time_extent, full_extent): elif self == self.OFFSET_SUBSET_FOR_TIMES: offset_x = centre_x + 0.25 * height vslice_bbox = BoundingBox( - left=offset_x - 0.02 * width, - right=offset_x + 0.02 * width, + left=offset_x - 0.01 * width, + right=offset_x + 0.01 * width, top=slice_bbox.top, bottom=slice_bbox.bottom, ) @@ -371,14 +369,11 @@ def subsets( ext_times = time.slice(self.layer.ranges.times) search_times = [self.layer.search_times(t) for t in ext_times] if space.needs_full_extent() and not self.full_extent: - self.full_extent = mv_search( - self.layer.dc.index, products=self.layer.products, sel=MVSelectOpts.EXTENT - ) + self.full_extent = self.layer.ows_index().extent(layer=self.layer, products=self.layer.products) if space.needs_time_extent(): - time_extent = mv_search( - self.layer.dc.index, + time_extent = self.layer.ows_index().extent( + layer=self.layer, products=self.layer.products, - sel=MVSelectOpts.EXTENT, times=search_times, ) else: diff --git a/ows_cfg_report.json b/ows_cfg_report.json index 8298b8820..082481178 100644 --- a/ows_cfg_report.json +++ b/ows_cfg_report.json @@ -1 +1,165 @@ -{"total_layers_count": 6, "layers": [{"layer": "s2_l2a","product": ["s2_l2a"], "styles_count": 8, "styles_list": ["simple_rgb", "style_ls_simple_rgb_clone", "infra_red", "rgb_ndvi", "blue", "ndvi", "ndvi_expr", "ndvi_delta"]}, {"layer": "s2_l2a_clone", "product": ["s2_l2a"], "styles_count": 8, "styles_list": ["simple_rgb", "style_ls_simple_rgb_clone", "infra_red", "rgb_ndvi", "blue", "ndvi", "ndvi_expr", "ndvi_delta"]}, {"layer": "s2_ard_granule_nbar_t", "product": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], "styles_count": 2, "styles_list": ["ndci", "mndwi"]}, {"layer": "s2_ard_latest_mosaic", "product": ["ga_s2am_ard_3", "ga_s2bm_ard_3"], "styles_count": 2, "styles_list": ["ndci", "mndwi"]}, {"layer": "ga_ls_fc_3", "product": ["ga_ls_fc_3"], "styles_count": 1, "styles_list": ["fc_rgb_unmasked"]},{"layer": "ls8_geomedian", "product": ["ga_ls8c_nbart_gm_cyear_3"], "styles_count": 3, "styles_list": ["simple_rgb","infra_red","ndvi"]}]} +{ + "total_layers_count": 12, + "layers": [ + { + "layer": "s2_l2a", + "product": [ + "s2_l2a" + ], + "styles_count": 8, + "styles_list": [ + "simple_rgb", + "style_ls_simple_rgb_clone", + "infra_red", + "blue", + "ndvi", + "ndvi_expr", + "rgb_ndvi", + "ndvi_delta" + ] + }, + { + "layer": "s2_l2a_clone", + "product": [ + "s2_l2a" + ], + "styles_count": 8, + "styles_list": [ + "simple_rgb", + "style_ls_simple_rgb_clone", + "infra_red", + "blue", + "ndvi", + "ndvi_expr", + "rgb_ndvi", + "ndvi_delta" + ] + }, + { + "layer": "s2_ard_granule_nbar_t", + "product": [ + "ga_s2am_ard_3", + "ga_s2bm_ard_3" + ], + "styles_count": 2, + "styles_list": [ + "ndci", + "mndwi" + ] + }, + { + "layer": "s2_ard_latest_mosaic", + "product": [ + "ga_s2am_ard_3", + "ga_s2bm_ard_3" + ], + "styles_count": 2, + "styles_list": [ + "ndci", + "mndwi" + ] + }, + { + "layer": "ga_ls_fc_3", + "product": [ + "ga_ls_fc_3" + ], + "styles_count": 1, + "styles_list": [ + "fc_rgb_unmasked" + ] + }, + { + "layer": "ls8_geomedian", + "product": [ + "ga_ls8c_nbart_gm_cyear_3" + ], + "styles_count": 3, + "styles_list": [ + "simple_rgb", + "infra_red", + "ndvi" + ] + }, + { + "layer": "s2_l2a_postgis", + "product": [ + "s2_l2a" + ], + "styles_count": 8, + "styles_list": [ + "simple_rgb", + "style_ls_simple_rgb_clone", + "infra_red", + "blue", + "ndvi", + "ndvi_expr", + "rgb_ndvi", + "ndvi_delta" + ] + }, + { + "layer": "s2_l2a_clone_postgis", + "product": [ + "s2_l2a" + ], + "styles_count": 8, + "styles_list": [ + "simple_rgb", + "style_ls_simple_rgb_clone", + "infra_red", + "blue", + "ndvi", + "ndvi_expr", + "rgb_ndvi", + "ndvi_delta" + ] + }, + { + "layer": "s2_ard_granule_nbar_t_postgis", + "product": [ + "ga_s2am_ard_3", + "ga_s2bm_ard_3" + ], + "styles_count": 2, + "styles_list": [ + "ndci", + "mndwi" + ] + }, + { + "layer": "s2_ard_latest_mosaic_postgis", + "product": [ + "ga_s2am_ard_3", + "ga_s2bm_ard_3" + ], + "styles_count": 2, + "styles_list": [ + "ndci", + "mndwi" + ] + }, + { + "layer": "ga_ls_fc_3_postgis", + "product": [ + "ga_ls_fc_3" + ], + "styles_count": 1, + "styles_list": [ + "fc_rgb_unmasked" + ] + }, + { + "layer": "ls8_geomedian_postgis", + "product": [ + "ga_ls8c_nbart_gm_cyear_3" + ], + "styles_count": 3, + "styles_list": [ + "simple_rgb", + "infra_red", + "ndvi" + ] + } + ] +} diff --git a/setup.py b/setup.py index 73954af56..510e40673 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import find_packages, setup install_requirements = [ - 'datacube[performance,s3]>=1.9.0-rc4', + 'datacube[performance,s3]>=1.9.0-rc9', 'flask', 'requests', 'affine', @@ -111,6 +111,7 @@ ], "datacube_ows.plugins.index": [ 'postgres = datacube_ows.index.postgres.api:ows_index_driver_init', + 'postgis = datacube_ows.index.postgis.api:ows_index_driver_init', ] }, python_requires=">=3.10.0", diff --git a/tests/test_driver_cache.py b/tests/test_driver_cache.py index 055f6ffab..09cf036d7 100644 --- a/tests/test_driver_cache.py +++ b/tests/test_driver_cache.py @@ -7,6 +7,9 @@ def test_index_driver_cache(): from datacube_ows.index.driver import ows_index_drivers + a = 2 + a = a + 1 assert "postgres" in ows_index_drivers() + assert "postgis" in ows_index_drivers() from datacube_ows.index.driver import ows_index_driver_by_name assert ows_index_driver_by_name("postgres") is not None diff --git a/tests/test_mv_selopts.py b/tests/test_mv_selopts.py index 68b80fb56..e9b910a53 100644 --- a/tests/test_mv_selopts.py +++ b/tests/test_mv_selopts.py @@ -4,7 +4,7 @@ # Copyright (c) 2017-2024 OWS Contributors # SPDX-License-Identifier: Apache-2.0 -from datacube_ows.mv_index import MVSelectOpts +from datacube_ows.index.postgres.mv_index import MVSelectOpts def test_all(): diff --git a/tests/test_update_ranges.py b/tests/test_update_ranges.py index 3e0a93514..0150d5ed3 100644 --- a/tests/test_update_ranges.py +++ b/tests/test_update_ranges.py @@ -10,7 +10,7 @@ import pytest from click.testing import CliRunner from datacube_ows.update_ranges_impl import main -from datacube_ows.index.postgres.sql import run_sql +from datacube_ows.index.sql import run_sql @pytest.fixture @@ -67,8 +67,8 @@ def test_update_ranges_misuse_cases(runner, role_name, layer_name): def test_run_sql(minimal_dc): - assert not run_sql(minimal_dc, "no_such_directory") + assert not run_sql(minimal_dc, "postgres", "no_such_directory") - assert not run_sql(minimal_dc, "templates") + assert not run_sql(minimal_dc, "postgres", "templates") - assert not run_sql(minimal_dc, "ows_schema/grants/read_only") + assert not run_sql(minimal_dc, "postgres", "ows_schema/grants/read_only") diff --git a/wordlist.txt b/wordlist.txt index 9f842b0ca..9b2a0bc80 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -77,6 +77,7 @@ cfg ci cli cloudfront +codebase codecov cogs COGs @@ -259,6 +260,7 @@ multiproc multiproduct mv mydb +myenv mypassword mysecretpassword myuser