From d04b5e8dc4743890954504c265f0f840f9e97aa4 Mon Sep 17 00:00:00 2001 From: C1-BA-B1-F3 Date: Sat, 3 Oct 2026 11:31:23 +0800 Subject: [PATCH 1/5] fix(process manager): apply limit and offset in get_jobs PostgreSQL and MongoDB managers ignored the limit and offset parameters, returning all jobs regardless of the query string. Apply offset before limit in both backends, matching the existing TinyDB behaviour. Fixes #2426 --- pygeoapi/process/manager/mongodb_.py | 10 ++++++++-- pygeoapi/process/manager/postgresql.py | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pygeoapi/process/manager/mongodb_.py b/pygeoapi/process/manager/mongodb_.py index 06e6d909ad..5479bea78a 100644 --- a/pygeoapi/process/manager/mongodb_.py +++ b/pygeoapi/process/manager/mongodb_.py @@ -79,9 +79,15 @@ def get_jobs(self, status=None, limit=None, offset=None): database = self.db.job_manager_pygeoapi collection = database.jobs if status is not None: - jobs = list(collection.find({}, {"status": status})) + query = {"status": status} else: - jobs = list(collection.find({})) + query = {} + cursor = collection.find(query) + if offset: + cursor = cursor.skip(offset) + if limit: + cursor = cursor.limit(limit) + jobs = list(cursor) LOGGER.info("JOBMANAGER - MongoDB jobs queried") return { 'jobs': jobs, diff --git a/pygeoapi/process/manager/postgresql.py b/pygeoapi/process/manager/postgresql.py index cfd63eafb8..180d6ba282 100644 --- a/pygeoapi/process/manager/postgresql.py +++ b/pygeoapi/process/manager/postgresql.py @@ -133,6 +133,11 @@ def get_jobs( if status is not None: results = results.filter(self.c.status == status.value) + if offset: + results = results.offset(offset) + if limit: + results = results.limit(limit) + jobs = [r._asdict() for r in results.all()] return {'jobs': jobs, 'numberMatched': len(jobs)} From 36e9016ef59dd695c698cb41c7a3bbf0fb9af468 Mon Sep 17 00:00:00 2001 From: C1-BA-B1-F3 Date: Sat, 3 Oct 2026 12:19:58 +0800 Subject: [PATCH 2/5] fix(tiles): pass server templates to render_j2_template for TMS endpoints The tilematrixsets and tilematrixset HTML handlers passed api.tpl_config as the template configuration, but that dict carries the full server config (including 'server') and lacks the 'path' key that render_j2_template expects. The lookup therefore always fell back to the default templates, ignoring any custom theme. Pass api.config['server']['templates'] instead, matching every other render_j2_template call site. Fixes #2422 --- pygeoapi/api/tiles.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pygeoapi/api/tiles.py b/pygeoapi/api/tiles.py index afdde22b1f..cb08459f5b 100644 --- a/pygeoapi/api/tiles.py +++ b/pygeoapi/api/tiles.py @@ -389,7 +389,8 @@ def tilematrixsets(api: API, }] if request.format == F_HTML: # render - content = render_j2_template(api.tpl_config, api.tpl_config, + content = render_j2_template(api.tpl_config, + api.config['server']['templates'], 'tilematrixsets/index.html', tms, request.locale) return headers, HTTPStatus.OK, content @@ -436,7 +437,8 @@ def tilematrixset(api: API, } if request.format == F_HTML: # render - content = render_j2_template(api.tpl_config, api.tpl_config, + content = render_j2_template(api.tpl_config, + api.config['server']['templates'], 'tilematrixsets/tilematrixset.html', tms, request.locale) return headers, HTTPStatus.OK, content From a60ce90ab66130617ce919d0cb44be17c3b28cb6 Mon Sep 17 00:00:00 2001 From: C1-BA-B1-F3 Date: Sat, 3 Oct 2026 14:17:08 +0800 Subject: [PATCH 3/5] fix(provider): honour driver_name when a connection string omits the DBAPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A provider configured with a bare connection string (e.g. "postgresql://user@host/db") resolved to SQLAlchemy's default DBAPI for that backend — psycopg (v3) for PostgreSQL. Deployments that only install psycopg2 then fail with 'ModuleNotFoundError: No module named psycopg' even though the provider pins 'postgresql+psycopg2'. When the caller's URL has no '+driver' suffix and the provider's driver_name does, rewrite the URL's drivername to the pinned one. Fixes #2424 --- pygeoapi/provider/sql.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pygeoapi/provider/sql.py b/pygeoapi/provider/sql.py index 121f48ac26..d9058d95e9 100644 --- a/pygeoapi/provider/sql.py +++ b/pygeoapi/provider/sql.py @@ -44,7 +44,7 @@ from decimal import Decimal import functools import logging -from typing import Optional, Any +from typing import Optional, Any, Union from geoalchemy2 import Geometry # noqa - this isn't used explicitly but is needed to process Geometry columns from geoalchemy2.functions import ST_MakeEnvelope, ST_Intersects @@ -60,7 +60,7 @@ desc, delete ) -from sqlalchemy.engine import URL, Engine +from sqlalchemy.engine import URL, Engine, make_url from sqlalchemy.exc import ( ConstraintColumnNotFoundError, InvalidRequestError, @@ -649,7 +649,7 @@ def get_engine( database: str, user: str, password: str, - conn_str: Optional[str] = None, + conn_str: Optional[Union[str, URL]] = None, **connect_args ) -> Engine: """ @@ -679,6 +679,18 @@ def get_engine( port=int(port), database=database ) + else: + # A caller-provided URL with a bare driver name (e.g. "postgresql://") + # resolves to SQLAlchemy's default DBAPI for that backend (psycopg v3 + # for PostgreSQL), which may not be installed. Honour the provider's + # driver_name when the URL does not already pin one. + parsed = make_url(conn_str) + if '+' not in parsed.drivername and '+' in driver_name: + backend = driver_name.split('+', 1)[0] + if parsed.drivername == backend: + conn_str = parsed.set( + drivername=driver_name + ) # Separate connection-pool tuning from DBAPI connect args. Pool keys are # applied to create_engine() directly; everything left in connect_args is From 032010b465a3f17130e1a290fa67b51dc65f574d Mon Sep 17 00:00:00 2001 From: C1-BA-B1-F3 Date: Sat, 3 Oct 2026 15:10:12 +0800 Subject: [PATCH 4/5] fix(mapscript): fall back to storage_crs when options.projection is absent The MapScript provider only read options.projection, ignoring the documented storage_crs configuration. Deployments that set storage_crs (like every other provider) silently got EPSG:4326, producing empty images for data in any other projection. Use storage_crs when options.projection is not set; the legacy options.projection override still wins when present so existing workarounds keep working. Fixes #2400 --- pygeoapi/provider/mapscript_.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pygeoapi/provider/mapscript_.py b/pygeoapi/provider/mapscript_.py index e46d8b11d2..e5fc64d625 100644 --- a/pygeoapi/provider/mapscript_.py +++ b/pygeoapi/provider/mapscript_.py @@ -93,10 +93,16 @@ def __init__(self, provider_def): self._layer.type = getattr(mapscript, self.options['type']) + # The documented configuration surface is storage_crs (a URI + # such as "http://www.opengis.net/def/crs/EPSG/0/2169"). + # options.projection is the legacy integer override kept for + # backward compatibility: when it is present it still wins, + # so existing deployments that rely on it keep working. try: self.crs = int(self.options['projection']) - except KeyError: - self.crs = 4326 + except (KeyError, ValueError): + crs_epsg = self.storage_crs.to_epsg() + self.crs = crs_epsg if crs_epsg is not None else 4326 self._layer.setProjection(self._epsg2projstring(self.crs)) From 818d5c228cc9da98f99bec9dcc6a793fec3525a1 Mon Sep 17 00:00:00 2001 From: C1-BA-B1-F3 Date: Sat, 3 Oct 2026 15:41:08 +0800 Subject: [PATCH 5/5] feat(crs): accept EPSG:xxxx shorthand in the crs query parameter The crs parameter only accepted the full URI form (http://www.opengis.net/def/crs/EPSG/0/28992); the common shorthand EPSG:28992 was rejected with 'CRS not supported for this collection'. Normalize an EPSG:xxxx query parameter to the canonical URL before matching against the supported-CRS list, so both forms work. Fixes #2375 --- pygeoapi/crs.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index ca0a28a4d4..dd5a488850 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -532,6 +532,25 @@ def _inplace_replace_geometry_filter_name( sub_node, geometry_column_name) +def normalize_crs_uri(crs: str) -> str: + """Normalize a CRS identifier to the canonical URL form. + + Accepts the shorthand "EPSG:xxxx" and returns the corresponding + http://www.opengis.net/def/crs/EPSG/0/xxxx URI, so that a query + parameter can be matched against the supported-CRS list which uses + URL form. Non-EPSG inputs are returned unchanged. + + :param crs: CRS identifier (URL, URN, or "AUTHORITY:CODE"). + :returns: Canonical URL-form URI when the input is an EPSG shorthand, + else the input unchanged. + """ + if ':' in crs and '/' not in crs: + authority, _, code = crs.partition(':') + if authority.upper() == 'EPSG' and code.isdigit(): + return f'http://www.opengis.net/def/crs/EPSG/0/{code}' + return crs + + def create_crs_transform_spec( provider_def: dict, query_crs_uri: Optional[str] = None ) -> Union[None, CrsTransformSpec]: @@ -565,6 +584,10 @@ def create_crs_transform_spec( else: query_crs_uri = DEFAULT_CRS LOGGER.debug(f'no crs parameter, using default: {query_crs_uri}') + else: + # Allow the common shorthand "EPSG:xxxx" in addition to the full + # URI form; both denote the same CRS. + query_crs_uri = normalize_crs_uri(query_crs_uri) supported_crs_list = get_supported_crs_list(provider_def) # Check that the crs specified by the query parameter is supported.