diff --git a/.devcontainer/dev_container.dockerfile b/.devcontainer/dev_container.dockerfile index 741b275..f602887 100644 --- a/.devcontainer/dev_container.dockerfile +++ b/.devcontainer/dev_container.dockerfile @@ -1,5 +1,5 @@ # Base image for the development container -ARG BASE_URL=python:3.11-slim +ARG BASE_URL=python:3.12-slim FROM ${BASE_URL} USER root diff --git a/.github/workflows/github-python-workflow.yml b/.github/workflows/github-python-workflow.yml index 69a4f81..45ab8bc 100644 --- a/.github/workflows/github-python-workflow.yml +++ b/.github/workflows/github-python-workflow.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: "3.11" + python-version: "3.12" - name: Install dependencies run: | @@ -64,7 +64,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: "3.11" + python-version: "3.12" - name: Install dependencies run: | diff --git a/ogc/edr/config/default.json b/ogc/edr/config/default.json index c827b04..cf2a734 100644 --- a/ogc/edr/config/default.json +++ b/ogc/edr/config/default.json @@ -16,7 +16,7 @@ "admin": false, "map": { "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", - "attribution": "'© OpenStreetMap contributors'" + "attribution": "© OpenStreetMap contributors" } }, "logging": { diff --git a/ogc/edr/edr_config.py b/ogc/edr/edr_config.py index 5bdd87d..488f83b 100644 --- a/ogc/edr/edr_config.py +++ b/ogc/edr/edr_config.py @@ -102,8 +102,8 @@ def _resources_definition(base_url: str, layers: List[pogc.Layer]) -> Dict[str, "data": group_name, "base_url": base_url, "crs": [ - "https://www.opengis.net/def/crs/OGC/1.3/CRS84", - "https://www.opengis.net/def/crs/EPSG/0/4326", + settings.crs_84_uri_format, + settings.epsg_4326_uri_format, ], "format": { "name": "geotiff", @@ -163,14 +163,14 @@ def _generate_extents(layers: List[pogc.Layer]) -> Dict[str, Any]: return { "spatial": { "bbox": [llc_lon, llc_lat, urc_lon, urc_lat], # minx, miny, maxx, maxy - "crs": "https://www.opengis.net/def/crs/OGC/1.3/CRS84", + "crs": settings.crs_84_uri_format, }, **( { "temporal": { "begin": datetime.fromisoformat(time_range[0]), # start datetime in RFC3339 "end": datetime.fromisoformat(time_range[-1]), # end datetime in RFC3339 - "trs": "https://www.opengis.net/def/uom/ISO-8601/0/Gregorian", + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian", } } if time_range is not None @@ -200,5 +200,5 @@ def _wgs84_bounding_box(layer: pogc.Layer) -> Tuple[float, float, float, float]: layer.grid_coordinates.URC.lat, ) except Exception: - crs_extents = settings.EDR_CRS["crs:84"] + crs_extents = settings.EDR_CRS[settings.crs_84_uri_format] return (crs_extents["minx"], crs_extents["miny"], crs_extents["maxx"], crs_extents["maxy"]) diff --git a/ogc/edr/edr_provider.py b/ogc/edr/edr_provider.py index dad1b4e..dd1530d 100644 --- a/ogc/edr/edr_provider.py +++ b/ogc/edr/edr_provider.py @@ -427,8 +427,8 @@ def interpret_crs(crs: str | None) -> str: ProviderInvalidQueryError Raised if the provided CRS string is unknown. """ - if crs is None or crs.lower() == "crs:84": - return settings.crs_84_pyproj_format # Pyproj acceptable format + if crs is None: + return settings.crs_84_uri_format # Pyproj acceptable format if crs.lower() not in [key.lower() for key in settings.EDR_CRS.keys()]: msg = f"Invalid CRS provided, expected one of {', '.join(settings.EDR_CRS.keys())}" diff --git a/ogc/edr/edr_routes.py b/ogc/edr/edr_routes.py index 30b4625..35160cd 100644 --- a/ogc/edr/edr_routes.py +++ b/ogc/edr/edr_routes.py @@ -21,7 +21,7 @@ class EdrRoutes(tl.HasTraits): """Class responsible for routing EDR requests to the appropriate pygeoapi API method.""" - base_url = tl.Unicode(default_value="http://127.0.0.1:5000/") + base_url = tl.Unicode(default_value="http://127.0.0.1:5000/edr") layers = tl.List(trait=tl.Instance(pogc.Layer)) def __init__(self, **kwargs): @@ -40,6 +40,17 @@ def layers_change(self, change: Dict[str, Any]): """ self.api = self.create_api() + @tl.observe("base_url") + def base_url_change(self, change: Dict[str, Any]): + """Monitor the base url and update the API when a change occurs. + + Parameters + ---------- + change : Dict[str, Any] + Dictionary holding type of modification and name of the attribute that triggered it. + """ + self.api = self.create_api() + def create_api(self) -> pygeoapi.api.API: """Create the pygeoapi API using a custom configuration. @@ -50,8 +61,8 @@ def create_api(self) -> pygeoapi.api.API: """ # Allow specifying GeoTiff or CoverageJSON in the format argument. # This is a bypass which is needed to get by a conditional check in pygeoapi. - pygeoapi.plugin.PLUGINS["formatter"]["GeoTiff"] = "" - pygeoapi.plugin.PLUGINS["formatter"]["CoverageJSON"] = "" + pygeoapi.plugin.PLUGINS["formatter"]["geotiff"] = "" + pygeoapi.plugin.PLUGINS["formatter"]["coveragejson"] = "" EdrProvider.set_layers(self.base_url, self.layers) config = EdrConfig.get_configuration(self.base_url, self.layers) open_api = get_oas(config, fail_on_invalid_collection=False) @@ -61,6 +72,24 @@ def clean_configuration_cache(self): """Clean a pygeoapi internal translation cache so that multiple configurations can be used simultaneously.""" pygeoapi.l10n._cfg_cache = {} + def update_configuration_base_url(self, request: pygeoapi.api.APIRequest): + """Update the EDR configuration base URL based on the provided request. + The EDR configuration base URL does not necessarily match the full path of the request base URL. + + Parameters + ---------- + request : pygeoapi.api.APIRequest + The API request containing a base URL. + The base URL should include scheme, host, and path for the request. + """ + request_base_url = request.params.get("base_url", "") + # Limit the base URL from the request to the EDR subdirectory + base_url_partitioned = request_base_url.partition("/edr") + if len(base_url_partitioned[0]) > 0: + configuration_base_url = base_url_partitioned[0] + base_url_partitioned[1] + if configuration_base_url != self.base_url: + self.base_url = configuration_base_url + def static_files(self, request: pygeoapi.api.APIRequest, file_path: str) -> Tuple[dict, int, str | bytes]: """Handle static file requests using the custom static file folder or the pygeoapi default folder. @@ -75,6 +104,7 @@ def static_files(self, request: pygeoapi.api.APIRequest, file_path: str) -> Tupl Headers, HTTP Status, and Content returned as a tuple to make the server response. """ self.clean_configuration_cache() + self.update_configuration_base_url(request) static_path = os.path.join(os.path.dirname(pygeoapi.__file__), "static") if "templates" in self.api.config["server"]: static_path = self.api.config["server"]["templates"].get("static", static_path) @@ -102,6 +132,7 @@ def landing_page(self, request: pygeoapi.api.APIRequest) -> Tuple[dict, int, str Headers, HTTP Status, and Content returned as a tuple to make the server response. """ self.clean_configuration_cache() + self.update_configuration_base_url(request) return pygeoapi.api.landing_page(self.api, request) def openapi(self, request: pygeoapi.api.APIRequest) -> Tuple[dict, int, str | bytes]: @@ -118,6 +149,7 @@ def openapi(self, request: pygeoapi.api.APIRequest) -> Tuple[dict, int, str | by Headers, HTTP Status, and Content returned as a tuple to make the server response. """ self.clean_configuration_cache() + self.update_configuration_base_url(request) return pygeoapi.api.openapi_(self.api, request) def conformance(self, request: pygeoapi.api.APIRequest) -> Tuple[dict, int, str | bytes]: @@ -134,6 +166,7 @@ def conformance(self, request: pygeoapi.api.APIRequest) -> Tuple[dict, int, str Headers, HTTP Status, and Content returned as a tuple to make the server response. """ self.clean_configuration_cache() + self.update_configuration_base_url(request) return pygeoapi.api.conformance(self.api, request) def describe_collections( @@ -156,6 +189,7 @@ def describe_collections( Headers, HTTP Status, and Content returned as a tuple to make the server response. """ self.clean_configuration_cache() + self.update_configuration_base_url(request) return pygeoapi.api.describe_collections(self.api, request, collection_id) def describe_instances( @@ -181,6 +215,7 @@ def describe_instances( Headers, HTTP Status, and Content returned as a tuple to make the server response. """ self.clean_configuration_cache() + self.update_configuration_base_url(request) return pygeoedr.get_collection_edr_instances(self.api, request, collection_id, instance_id=instance_id) def collection_query( @@ -209,6 +244,7 @@ def collection_query( Headers, HTTP Status, and Content returned as a tuple to make the server response. """ self.clean_configuration_cache() + self.update_configuration_base_url(request) headers, http_status, content = pygeoedr.get_collection_edr_query( self.api, request, collection_id, instance_id, query_type=query_type, location_id=None ) diff --git a/ogc/edr/static/css/default.css b/ogc/edr/static/css/default.css new file mode 100644 index 0000000..36f9261 --- /dev/null +++ b/ogc/edr/static/css/default.css @@ -0,0 +1,86 @@ +.flat { + border: 0px; +} + +header { + display: inline-block; +} + +main { + background-color: white; +} + +.crumbs { + background-color: rgb(230, 230, 230); + padding: 6px; +} + +.crumbs a { + padding: 0px 6px; + color: black; + text-decoration: none; + /* text-transform: capitalize;*/ +} + +#items-map, +#collection-map { + width: 100%; + height: 400px; +} + +#coverages-map { + width: 100%; + height: 80vh; +} + +.c3-tooltip-container { + z-index: 300; +} + +/* cancel mini-css header>button uppercase */ +header button, +header [type="button"], +header .button, +header [role="button"] { + text-transform: none; +} + +html { + background-color: #fff; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; + background-color: #fff; +} + +footer.sticky-bottom { + margin-top: auto; +} + +main { + padding-bottom: 65px; + /* prevent from falling under the footer */ +} + +table:not(.horizontal) { + max-height: none; +} + +mark.successful { + background-color: green; +} + +mark.accepted { + background-color: default; +} + +mark.failed { + background-color: red; +} + +mark.running { + background-color: orange; +} \ No newline at end of file diff --git a/ogc/edr/test/test_edr_provider.py b/ogc/edr/test/test_edr_provider.py index 6797baa..3c635af 100644 --- a/ogc/edr/test/test_edr_provider.py +++ b/ogc/edr/test/test_edr_provider.py @@ -29,7 +29,7 @@ def get_provider_definition(base_url: str) -> Dict[str, Any]: "name": "ogc.edr.edr_provider.EdrProvider", "data": "Layers", "base_url": base_url, - "crs": ["https://www.opengis.net/def/crs/OGC/1.3/CRS84", "https://www.opengis.net/def/crs/EPSG/0/4326"], + "crs": ["http://www.opengis.net/def/crs/OGC/1.3/CRS84", "http://www.opengis.net/def/crs/EPSG/0/4326"], "format": {"name": "GeoJSON", "mimetype": "application/json"}, } @@ -573,12 +573,13 @@ def test_edr_provider_altitude_invalid_string(): def test_edr_provider_crs_interpreter_default_value(): """Test the CRS interpretation returns a default value when the argument is None.""" - assert EdrProvider.interpret_crs(None) == "urn:ogc:def:crs:OGC:1.3:CRS84" + assert EdrProvider.interpret_crs(None) == "http://www.opengis.net/def/crs/OGC/1.3/CRS84" def test_edr_provider_crs_interpreter_valid_value(): """Test the CRS interpretation returns a valid value when the argument is acceptable.""" - assert EdrProvider.interpret_crs("epsg:4326") == "epsg:4326" + crs = "http://www.opengis.net/def/crs/EPSG/0/4326" + assert EdrProvider.interpret_crs(crs) == crs def test_edr_provider_crs_interpreter_invalid_value(): diff --git a/ogc/edr/test/test_edr_routes.py b/ogc/edr/test/test_edr_routes.py index a1cd183..caa0cbd 100644 --- a/ogc/edr/test/test_edr_routes.py +++ b/ogc/edr/test/test_edr_routes.py @@ -24,7 +24,7 @@ def mock_request(request_args: Dict[str, Any] = {}) -> APIRequest: APIRequest Mock API request for route testing. """ - environ = create_environ(base_url="http://127.0.0.1:5000/ogc/") + environ = create_environ(base_url="http://127.0.0.1:5000/ogc/edr") request = Request(environ) request.args = ImmutableMultiDict(request_args.items()) return APIRequest(request, ["en"]) @@ -336,3 +336,16 @@ def test_edr_routes_collection_query_missing_parameter( ) assert status == HTTPStatus.BAD_REQUEST + + +def test_edr_routes_request_url_updates_configuration_url(): + """Test the EDR routes request base URL updates the configuration URL.""" + request_url = "http://test:5000/ogc/edr/static/img/logo.png" + expected_config_url = "http://test:5000/ogc/edr" + request = mock_request({"base_url": request_url}) + edr_routes = EdrRoutes(layers=[]) + + _, status, _ = edr_routes.static_files(request, "img/logo.png") + + assert status == HTTPStatus.OK + assert edr_routes.api.config["server"]["url"] == expected_config_url diff --git a/ogc/servers.py b/ogc/servers.py index b7b859e..fe8fda8 100755 --- a/ogc/servers.py +++ b/ogc/servers.py @@ -54,6 +54,13 @@ def home(endpoint):