Read-only mirror of https://github.com/swissgeo/service-oa-records — SWISSGEO. Issues & pull requests at the source.
  • Python 88.6%
  • Makefile 8.6%
  • Dockerfile 1.9%
  • Shell 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-15 12:08:19 +02:00
.github Added/Modified github workflows pr-auto-semver.yml #skip-tagging-and-release 2026-08-06 10:43:40 +02:00
.zed GPS-498: debug 2026-04-22 20:45:39 +02:00
config-files GPS-954: streamline the setup, more like service-control 2026-08-27 11:47:02 +02:00
pygeoapi-swissgeo-extensions GPS-1057: substring search 2026-10-15 08:46:28 +02:00
tests GPS-1057: substring search 2026-10-15 08:46:28 +02:00
.dockerignore GPS-498: cleanup and more 2026-04-22 14:47:45 +02:00
.env.default GPS-954: streamline env file 2026-08-27 15:02:59 +02:00
.gitignore chore: Ignore git merge conflict files 2026-10-12 09:15:43 +02:00
docker-entrypoint.sh GPS-1075: service-oa-records performance improvements 2026-09-29 09:54:29 +02:00
Dockerfile GPS-1075: service-oa-records performance improvements 2026-09-29 09:54:29 +02:00
LICENSE.md Initial commit 2026-03-31 14:10:16 +02:00
Makefile GPS-954: streamline env file 2026-08-27 15:02:59 +02:00
pygeoapi-config.yml GPS-1075: service-oa-records performance improvements 2026-09-29 09:54:29 +02:00
pyproject.toml chore(dep): Excluded unused rasterio and sqlalchemy dependencies 2026-10-12 09:45:21 +02:00
README.md GPS-977: Added configurable Cache-Control header to GET/HEAD responses 2026-10-09 10:33:58 +02:00
renovate.json GPS-498: update catalog 2026-04-22 13:38:40 +02:00
uv.lock chore(dep): Excluded unused rasterio and sqlalchemy dependencies 2026-10-12 09:45:21 +02:00

service-oa-records

OGC API Records service for SwissGeo, built on pygeoapi with an OpenSearch backend and multilingual record support.

Branch Status Coverage
develop Build Status codecov-develop
main Build Status codecov-main

Note

This is still in POC phase

Overview

This service exposes Swiss geospatial catalogue data as an OGC API Records endpoint. pygeoapi handles the OGC API layer; records are stored in OpenSearch and queried via SwissGeoProvider, a custom provider that adds language-aware field selection and link patching on top of pygeoapi's built-in OpenSearchCatalogueProvider.

Client
  │  ?lang=de&f=json
  ▼
uvicorn (app.py)          ← patches call_api_threadsafe to inject lang/fmt
  │                          into the executor thread-local before each call
  ▼
pygeoapi Starlette app
  │
  ▼
SwissGeoProvider          ← extends OpenSearchCatalogueProvider
  │  query() / get()
  ├─ reads lang from thread-local (set by app.py)
  ├─ mask_prop()    – filters/sorts on <lang>.properties.<field>
  ├─ calls super().query() / super().get()
  ├─ picks the <lang> record out of each document
  └─ _patch_links() – appends ?lang=…&f=… to same-host links
  │
  ▼
OpenSearch

swissgeo_provider.py

Why app.py is needed

pygeoapi's Starlette integration runs provider calls in a thread pool. By the time the provider executes, the Starlette request context is no longer accessible. app.py monkey-patches call_api_threadsafe to call set_request_params(lang, fmt) just before dispatching each call, storing the values in a threading.local that SwissGeoProvider reads. Flask users are handled via a fallback flask.request.args read inside _get_lang_and_fmt().

Localized index layout

The indexes are written by manage.py oar_opensearch_export in service-control. Each document holds one complete record per language next to the shared id and type:

{"id": "ch.bafu.moose", "type": "Feature", "de": {record}, "fr": {record}, "it": {record}, "rm": {record}, "en": {record}}

Only the fields under <lang>.properties that are listed in the mapping are indexed. The request language decides which record is returned and which subtree filters (?concept=, ?protocol=, …) and sorting run against. Free-text search (?q=) matches the records of all languages, so ?q=wander&lang=fr finds records by their German title too. The available fields are read from the de subtree of the mapping; they are the same in every language.

Sorting is supported on text fields that have a sort keyword subfield in the mapping (title; name and acronym for organizations). Sorting on other text fields returns 400.

_patch_links() appends ?lang=<lang>&f=<fmt> to any link whose href is relative or starts with PYGEOAPI_SERVER_URL. External links are left untouched.

Configuration

Provider registration in pygeoapi-config.yml:

providers:
  - type: record
    name: swissgeo_provider.SwissGeoProvider
    data: ${OPENSEARCH_URL:-http://localhost:9200}/swissgeo-catalog
    id_field: externalId
    time_field: recordCreated
    title_field: title
    languages:
      - en
      - de
      - fr
      - it

Key environment variables:

Variable Default Purpose
PYGEOAPI_SERVER_URL / Base URL used to identify same-host links for patching
OPENSEARCH_URL http://localhost:9200 OpenSearch base URL
PYGEOAPI_CONFIG /pygeoapi/pygeoapi-config.yml pygeoapi config file path
THREADPOOL_MAX_WORKERS 16 Threads running the API calls; also the OpenSearch connection pool size
OPENSEARCH_TIMEOUT 30 OpenSearch request timeout in seconds
OPENSEARCH_MAX_RETRIES 3 OpenSearch retries, also on timeouts
CACHE_CONTROL_HEADER public, max-age=300 Cache-Control header set on GET/HEAD responses, except 5xx and responses that already have one; empty disables it
UVICORN_LIMIT_CONCURRENCY 100 (image) Open connections/requests before uvicorn answers 503
MALLOC_ARENA_MAX 2 (image) Limits glibc malloc arenas to reduce memory fragmentation

pygeoapi creates a new provider instance for every request. SwissGeoProvider therefore creates the OpenSearch client and reads the index mapping only once per index, and shares both across instances. With aws4auth: true a single refreshable SigV4 signer is used.

Running locally

Important

This service does not start any backing services of its own. It expects the service-control stack to be running, which provides OpenSearch on localhost:9200 (with the catalogue indexes loaded) and the OTLP collector on localhost:4317. Start that stack first. Also run manage.py oar_opensearch_export at least once to create the OpenSearch indexes and write the data.

Setup

make setup

Creates the virtualenv with uv sync, creates .env from .env.default if it is missing, and drops you into a shell with those variables exported. .env is gitignored, so local overrides (e.g. a different OPENSEARCH_URL) survive. A .env.local, if present, takes precedence over .env.

Run on the host

make serve

Run the built image

make dockerrun

Builds the image and runs it with --net=host, so it reaches the same localhost:9200 / localhost:4317 as make serve. This is for verifying the deployment artifact; day-to-day development uses make serve.

Debugging

make serve-debug

Runs the app under debugpy listening on port 5678 and waiting for a client, so you can attach before the first request is handled.

Then attach your debugger (e.g. "Attach to Docker (swissgeo_provider)" in Zed) to localhost:5678.

To debug the container instead, build the image with --build-arg INSTALL_DEBUGPY=true (debugpy is not part of the production image) and run it with PYDEBUG=true.

Project structure

pygeoapi-swissgeo-extensions/
  app.py                  # Starlette entrypoint; patches call_api_threadsafe
  swissgeo_provider.py    # SwissGeoProvider: language selection + link patching
pygeoapi-config.yml       # pygeoapi server + collection configuration
static-s3/                # 1:1 catalog data from S3