From 7d81a141781bf0b24256baf1618c7509d634b8aa Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Tue, 2 Jun 2026 23:52:13 -0400 Subject: [PATCH 1/2] Added CLAUDE.md file --- CLAUDE.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4ab89cd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repository is + +A Python client SDK for the **actinia** REST API (the GRASS REST API for scalable, distributed geospatial processing). The package name is `actinia_openapi_python_client`. + +**Almost all of the code in this repository is auto-generated** by [OpenAPI Generator](https://openapi-generator.tech) (generator version 7.2.0-SNAPSHOT, `PythonClientCodegen`) from an Actinia Swagger/OpenAPI spec. This is the single most important fact about the repo: do not hand-edit generated files to add features or fix API-shape bugs — regenerate from the spec instead. Hand-editing is only appropriate for files the generator is told to leave alone (see `.openapi-generator-ignore`) or for the packaging/CI metadata described below. + +## Regenerating the client + +The source of truth is the OpenAPI spec file in the repo root (`actinia_swagger_x.x.x.json`; an older `actinia_swagger.json` is also present). Generation is configured by `config.yaml` (package name, version, author, install requires). The output is the `actinia_openapi_python_client/`, `test/`, and `docs/` trees plus `README.md`. + +Note: `generate.sh` in the repo root invokes the `kotlin-spring` generator, not the Python client generator — it does **not** reproduce this package and should not be used to regenerate it. The actual Python client is produced with the `python` generator driven by `config.yaml`. When regenerating, run `openapi-generator-cli` with `-g python -c config.yaml -i actinia_swagger_x.x.x.json` and bump `packageVersion` in `config.yaml` to match. + +When changing the package version, keep these in sync: `config.yaml` (`packageVersion`), `pyproject.toml` (`version`), and `setup.py` (`VERSION`). + +## Commands + +Install for development and testing: +```sh +pip install -r requirements.txt +pip install -r test-requirements.txt +``` + +Run the full test suite: +```sh +pytest +``` + +Run a single test file / test: +```sh +pytest test/test_api_log_api.py +pytest test/test_api_log_api.py::TestAPILogApi::test_api_log_user_id_get +``` +Note: `pytest-randomly` is installed, so test ordering is randomized by default. Use `pytest -p no:randomly` for a fixed order. + +Run with coverage (as CI does): +```sh +pytest --cov=actinia_openapi_python_client +``` + +Lint (matches the GitHub Actions check): +```sh +# Fails the build on real errors: +flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics +# Style warnings only (never fails): +flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics +``` +`setup.cfg` sets `max-line-length=99` for editor/flake8 defaults, but CI uses `--max-line-length=127`. + +Build the distribution (as the publish workflow does): +```sh +python -m build +``` + +## Architecture + +The generated SDK follows the standard OpenAPI `python` client layout. Understanding these few pieces lets you navigate the ~28 API classes and ~98 models without reading them all: + +- **`actinia_openapi_python_client/__init__.py`** — the package facade. Re-exports every `*Api` class and every model, so users do `actinia_openapi_python_client.ProcessingApi`, etc. +- **`api/`** — one module per API tag (e.g. `processing_api.py`, `raster_management_api.py`, `location_management_api.py`). Each holds a single `*Api` class whose methods correspond 1:1 to REST endpoints. Method names are derived from the path + verb (e.g. `locations_location_name_mapsets_get`). Every endpoint method has three variants: the plain call, `*_with_http_info`, and `*_without_preload_content`. +- **`models/`** — one Pydantic v2 model per module (the SDK requires `pydantic >= 2`). These are the request/response body schemas. +- **`api_client.py`** (`ApiClient`) — the engine. Handles serialization/deserialization between models and JSON, parameter substitution, auth, and dispatching to the REST layer. The `*Api` classes are thin wrappers that build call params and hand them to `ApiClient`. +- **`configuration.py`** (`Configuration`) — host, credentials, TLS, and other settings. The only auth scheme defined is HTTP basic auth (`basicAuth`); construct with `username=`/`password=`. Default host is `http://localhost`. +- **`rest.py`** (`RESTClientObject`) — the urllib3-based HTTP transport. +- **`exceptions.py`** — exception hierarchy rooted at `OpenApiException` / `ApiException`, with subclasses by status family (`BadRequestException`, `UnauthorizedException`, `NotFoundException`, `ForbiddenException`, `ServiceException`). + +Domain concepts that recur throughout the API surface (from Actinia/GRASS GIS): **locations** contain **mapsets**, which contain **raster_layers**, **vector_layers**, and **STRDS** (space-time raster datasets). Long-running operations are **resources** with async (`*_async`) and sync variants and status-polling endpoints. + +The `test/` tree mirrors the generated code: one `test_*.py` per API class and per model. These are generator-produced stubs (`pass` bodies) — they verify importability/instantiation rather than behavior. + +## CI + +GitHub Actions (`.github/workflows/python.yml`) runs flake8 + pytest on Python 3.9–3.12 for every push and PR. `.github/workflows/python-publish.yml` builds and publishes to PyPI on a published GitHub release (uses `PYPI_API_TOKEN`). A parallel GitLab CI config (`.gitlab-ci.yml`) runs the same pytest matrix. Supported Python is **3.9+**. From 75ba8ce66bcd995b7a5e3afc8d09eac99fa9e86d Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Wed, 3 Jun 2026 00:46:11 -0400 Subject: [PATCH 2/2] Automate client regen on actinia-core releases; OIDC PyPI publishing - Add release-on-actinia.yml: daily/dispatch workflow that dumps the swagger spec from actinia-core (with plugins) via Docker, regenerates the client with a pinned generator, bumps an independent semver, and opens a PR for review. - Switch python-publish.yml to PyPI Trusted Publishers (OIDC) using the pypi environment; drop PYPI_API_TOKEN and the user/password inputs. - Populate .openapi-generator-ignore to protect hand-maintained files (CLAUDE.md, setup.py, pyproject.toml, CI workflows, generate.sh). - Modernize python.yml actions (checkout@v4, setup-python@v5) and add concurrency; correct config.yaml packageInstallRequires. - Add UPSTREAM.md to record the targeted actinia-core version. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/python-publish.yml | 56 +++--- .github/workflows/python.yml | 8 +- .github/workflows/release-on-actinia.yml | 221 +++++++++++++++++++++++ .openapi-generator-ignore | 18 ++ UPSTREAM.md | 12 ++ config.yaml | 2 +- 6 files changed, 288 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/release-on-actinia.yml create mode 100644 UPSTREAM.md diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index fea5d8b..957bf9a 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,10 +1,11 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. +# Publishes the package to PyPI when a GitHub Release is published. +# +# Authentication uses PyPI Trusted Publishers (OIDC) — there is no stored +# API token. The exchange is performed automatically by the publish action +# using the workflow's short-lived OIDC identity. For this to work, a Trusted +# Publisher must be configured on PyPI for this repository, naming this +# workflow file (python-publish.yml) and the `pypi` environment. +# See: https://docs.pypi.org/trusted-publishers/ name: Upload Python Package @@ -15,25 +16,28 @@ on: permissions: contents: read -jobs: - deploy: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false +jobs: + publish: runs-on: ubuntu-latest - + environment: + name: pypi + url: https://pypi.org/p/actinia-openapi-python-client + permissions: + id-token: write # REQUIRED for OIDC trusted publishing steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Build package + run: | + python -m pip install --upgrade pip build + python -m build + - name: Publish package to PyPI + # Pinned to a release tag; OIDC means no user/password is supplied. + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index aeb8dba..729c94d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -7,6 +7,10 @@ name: actinia_openapi_python_client Python package on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: @@ -16,9 +20,9 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release-on-actinia.yml b/.github/workflows/release-on-actinia.yml new file mode 100644 index 0000000..fc95b34 --- /dev/null +++ b/.github/workflows/release-on-actinia.yml @@ -0,0 +1,221 @@ +# Auto-regenerate the client when actinia-core publishes a new release. +# +# What it does (see CLAUDE.md and the plan in repo history): +# 1. Find the latest actinia-org/actinia-core release tag. +# 2. Skip if that version was already processed. +# 3. Run actinia-core (with the official plugins) in Docker and dump its +# consolidated swagger.json — this is the only place the full spec exists. +# 4. Regenerate the Python client with a pinned OpenAPI Generator. +# 5. Bump this package's version on its own (independent) semver line. +# 6. Open a PR for human review. Merging + cutting a GitHub Release then +# triggers python-publish.yml to publish to PyPI. +# +# The exact swagger route, container image tag, and credentials are exposed as +# `env:` below so they are easy to adjust without rewriting the logic. + +name: Release on new actinia-core version + +on: + schedule: + - cron: '0 6 * * *' # daily at 06:00 UTC + workflow_dispatch: + inputs: + core_version: + description: 'actinia-core version to build (defaults to latest release)' + required: false + type: string + +permissions: + contents: write # push the branch + pull-requests: write # open the PR + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + # OpenAPI Generator image — pinned for reproducible output. The repo's + # .openapi-generator/VERSION is 7.2.0-SNAPSHOT; the closest stable release is v7.2.0. + OPENAPI_GENERATOR_IMAGE: openapitools/openapi-generator-cli:v7.2.0 + # actinia-core HTTP endpoint and swagger route inside the running container. + ACTINIA_BASE_URL: http://localhost:8088 + SWAGGER_PATH: /api/v3/swagger.json + # Default credentials shipped by the actinia-docker dev stack. + ACTINIA_USER: actinia-gdi + ACTINIA_PASSWORD: actinia-gdi + +jobs: + regenerate: + runs-on: ubuntu-latest + steps: + - name: Checkout client repo + uses: actions/checkout@v4 + + - name: Resolve target actinia-core version + id: ver + env: + GH_TOKEN: ${{ github.token }} + INPUT_VERSION: ${{ github.event.inputs.core_version }} + run: | + set -euo pipefail + if [ -n "${INPUT_VERSION:-}" ]; then + CORE_VERSION="${INPUT_VERSION}" + else + CORE_VERSION="$(gh release view \ + --repo actinia-org/actinia-core \ + --json tagName --jq .tagName)" + fi + # Normalise: strip a leading "v" if upstream ever adds one. + CORE_VERSION="${CORE_VERSION#v}" + echo "Latest actinia-core version: ${CORE_VERSION}" + echo "core_version=${CORE_VERSION}" >> "$GITHUB_OUTPUT" + echo "branch=auto/actinia-core-${CORE_VERSION}" >> "$GITHUB_OUTPUT" + echo "spec=actinia_swagger_${CORE_VERSION}.json" >> "$GITHUB_OUTPUT" + + - name: Skip if version already processed + id: guard + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + SPEC="${{ steps.ver.outputs.spec }}" + BRANCH="${{ steps.ver.outputs.branch }}" + PROCEED=true + if [ -f "${SPEC}" ]; then + echo "Spec ${SPEC} already committed on this branch — nothing to do." + PROCEED=false + elif git ls-remote --exit-code --heads origin "${BRANCH}" >/dev/null 2>&1; then + echo "Branch ${BRANCH} already exists on origin — nothing to do." + PROCEED=false + fi + echo "proceed=${PROCEED}" >> "$GITHUB_OUTPUT" + + - name: Start actinia-core (with plugins) and dump swagger.json + if: steps.guard.outputs.proceed == 'true' + run: | + set -euo pipefail + SPEC="${{ steps.ver.outputs.spec }}" + + # The actinia-docker dev stack runs actinia-core together with the + # official plugins and its KVDB/redis dependency. + git clone --depth 1 https://github.com/actinia-org/actinia-docker.git + cd actinia-docker + docker compose -f docker-compose.yml up -d + + # Wait for actinia-core to answer. + echo "Waiting for actinia-core to become healthy..." + for i in $(seq 1 60); do + if curl -sf -u "${ACTINIA_USER}:${ACTINIA_PASSWORD}" \ + "${ACTINIA_BASE_URL}/api/v3/version" >/dev/null 2>&1; then + echo "actinia-core is up." + break + fi + sleep 5 + if [ "$i" -eq 60 ]; then + echo "::error::actinia-core did not become healthy in time" + docker compose -f docker-compose.yml logs --tail=200 + exit 1 + fi + done + + # Dump the consolidated spec (includes plugin endpoints). + curl -sf -u "${ACTINIA_USER}:${ACTINIA_PASSWORD}" \ + "${ACTINIA_BASE_URL}${SWAGGER_PATH}" -o "../${SPEC}" + + cd .. + # Validate: real JSON with paths, and confirm plugin endpoints landed. + python -c "import json,sys; d=json.load(open('${SPEC}')); \ + assert d.get('paths'), 'no paths in spec'; \ + print('paths:', len(d['paths']))" + echo "Sample of discovered paths (look for plugin routes):" + python -c "import json; d=json.load(open('${SPEC}')); \ + [print(' ', p) for p in sorted(d['paths'])[:40]]" + + - name: Regenerate the Python client + if: steps.guard.outputs.proceed == 'true' + run: | + set -euo pipefail + SPEC="${{ steps.ver.outputs.spec }}" + docker run --rm -v "${PWD}:/local" "${OPENAPI_GENERATOR_IMAGE}" generate \ + -g python \ + -c /local/config.yaml \ + -i "/local/${SPEC}" \ + -o /local + # The generator may write as root; normalise ownership for later steps. + sudo chown -R "$(id -u):$(id -g)" . + + - name: Bump client version (independent semver patch) + if: steps.guard.outputs.proceed == 'true' + id: bump + env: + CORE_VERSION: ${{ steps.ver.outputs.core_version }} + run: | + set -euo pipefail + NEW_VERSION="$(python - <<'PY' + import re, pathlib + cfg = pathlib.Path("config.yaml").read_text() + cur = re.search(r'packageVersion:\s*([0-9]+\.[0-9]+\.[0-9]+)', cfg).group(1) + major, minor, patch = (int(x) for x in cur.split(".")) + new = f"{major}.{minor}.{patch + 1}" + + # config.yaml + pathlib.Path("config.yaml").write_text( + re.sub(r'(packageVersion:\s*)[0-9]+\.[0-9]+\.[0-9]+', rf'\g<1>{new}', cfg)) + + # pyproject.toml + pp = pathlib.Path("pyproject.toml") + pp.write_text(re.sub(r'(?m)^(version\s*=\s*")[0-9]+\.[0-9]+\.[0-9]+(")', + rf'\g<1>{new}\g<2>', pp.read_text(), count=1)) + + # setup.py + sp = pathlib.Path("setup.py") + sp.write_text(re.sub(r'(VERSION\s*=\s*")[0-9]+\.[0-9]+\.[0-9]+(")', + rf'\g<1>{new}\g<2>', sp.read_text(), count=1)) + + # UPSTREAM.md — record the actinia-core version this build targets. + import os + core = os.environ["CORE_VERSION"] + up = pathlib.Path("UPSTREAM.md") + up.write_text(re.sub(r'(\| actinia-core \| ).*?( \|)', + rf'\g<1>{core}\g<2>', up.read_text(), count=1)) + + print(new) + PY + )" + echo "New client version: ${NEW_VERSION}" + echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + + - name: Create branch, commit, and open PR + if: steps.guard.outputs.proceed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + BRANCH="${{ steps.ver.outputs.branch }}" + CORE_VERSION="${{ steps.ver.outputs.core_version }}" + NEW_VERSION="${{ steps.bump.outputs.new_version }}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "${BRANCH}" + + # Drop the throwaway actinia-docker clone before committing. + rm -rf actinia-docker + git add -A + git commit -m "Regenerate client for actinia-core ${CORE_VERSION} (v${NEW_VERSION})" + git push --set-upstream origin "${BRANCH}" + + gh pr create \ + --title "Regenerate client for actinia-core ${CORE_VERSION} (v${NEW_VERSION})" \ + --body "Automated regeneration triggered by actinia-core **${CORE_VERSION}**. + + - Client version bumped to **${NEW_VERSION}** (independent semver). + - Spec dumped from a running actinia-core (with plugins): \`actinia_swagger_${CORE_VERSION}.json\`. + - Generated with \`${OPENAPI_GENERATOR_IMAGE}\`. + + Review the diff, merge, then cut a GitHub Release to publish to PyPI (via \`python-publish.yml\`)." \ + --label "automated" \ + --label "dependencies" || \ + gh pr create \ + --title "Regenerate client for actinia-core ${CORE_VERSION} (v${NEW_VERSION})" \ + --body "Automated regeneration for actinia-core ${CORE_VERSION}; client bumped to ${NEW_VERSION}." diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore index 7484ee5..7eb9139 100644 --- a/.openapi-generator-ignore +++ b/.openapi-generator-ignore @@ -21,3 +21,21 @@ #docs/*.md # Then explicitly reverse the ignore rule for a single file: #!docs/README.md + +# --------------------------------------------------------------------------- +# Hand-maintained files (do NOT let the generator overwrite these). +# The automated regeneration workflow relies on these being protected. +# --------------------------------------------------------------------------- + +# Project guidance and packaging metadata customized by hand. +CLAUDE.md +setup.py +pyproject.toml + +# CI workflows are hand-maintained, not generator-managed. +.github/workflows/python.yml +.github/workflows/python-publish.yml +.github/workflows/release-on-actinia.yml + +# Stale helper script (kotlin-spring); not used to build this client. +generate.sh diff --git a/UPSTREAM.md b/UPSTREAM.md new file mode 100644 index 0000000..7fa146c --- /dev/null +++ b/UPSTREAM.md @@ -0,0 +1,12 @@ +# Upstream tracking + +This client is generated from the actinia REST API. The table below records +which upstream version the current generated code targets. It is updated +automatically by the `release-on-actinia` workflow on each regeneration. + +| Component | Version | +|-----------|---------| +| actinia-core | (not yet regenerated by automation) | + +The consolidated OpenAPI spec used for generation is committed in the repo root +as `actinia_swagger_.json`. diff --git a/config.yaml b/config.yaml index 71f1154..5b4636c 100644 --- a/config.yaml +++ b/config.yaml @@ -7,4 +7,4 @@ additionalProperties: packageDescription: Actinia Core Python Client packageUrl: "" packageKeywords: ["actinia", "actinia-core", "python", "client", "grassgis", "gis", "grass", "openplains"] - packageInstallRequires: ["requests", "urllib3", "six", "certifi", "python-dateutil"] + packageInstallRequires: ["urllib3", "python-dateutil", "pydantic >= 2", "typing-extensions >= 4.7.1"]