diff --git a/.checkov.yaml b/.checkov.yaml new file mode 100644 index 00000000..beb95eb9 --- /dev/null +++ b/.checkov.yaml @@ -0,0 +1,9 @@ +--- +# Checkov skips for intentional local-dev / OpenAPI design choices. +# Auth is enforced at nginx (basic auth), not in the OpenAPI document. +skip-check: + - CKV_DOCKER_2 # HEALTHCHECK optional for short-lived local compose services + - CKV_DOCKER_3 # non-root USER deferred; compose network is private + - CKV_OPENAPI_4 # security schemes declared at operation / gateway layer + - CKV_OPENAPI_5 + - CKV2_GHA_1 # workflow permissions set per-job, not write-all globally diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..52c2a68d --- /dev/null +++ b/.codespellrc @@ -0,0 +1,3 @@ +[codespell] +ignore-words-list = connexion,Connexion,etuff,eTUFF,hadolint,tagbase,Tagbase,pgadmin,argos,Argos,teardown,Teardown +skip = .tox,.git,*.pem,*.zip,pylock.toml diff --git a/.github/super-linter.env b/.github/super-linter.env new file mode 100644 index 00000000..b07a558a --- /dev/null +++ b/.github/super-linter.env @@ -0,0 +1,29 @@ +# Shared Super Linter configuration (CI + local Docker). +# Docs: https://github.com/super-linter/super-linter/blob/main/docs/run-linter-locally.md +# +# Keys are alphabetical (dotenv-linter UnorderedKey). + +DEFAULT_BRANCH=main +# Exclude local tox/venv envs, intentional TLS fixtures, staging drops, and +# pg_dump-style schema SQL (OWNER TO / ROLE / \connect). Hand-written +# materialized views remain under SQLFluff. +# (Do not set IGNORE_GITIGNORED_FILES=true — it passes --gitignore to jscpd, +# which this Super Linter image rejects.) +FILTER_REGEX_EXCLUDE=\.tox/|\.venv/|venv/|services/nginx/ssl/|test/fixtures/nginx/|staging_data(\.bk)?(/|$)|services/postgis/tagbase_schema(_tables)?\.sql$ +LINTER_RULES_PATH=. +VALIDATE_ALL_CODEBASE=true +# Drop Biome; Prettier + ESLint JSON cover the little non-Python surface area. +VALIDATE_BIOME_FORMAT=false +VALIDATE_BIOME_LINT=false +# No commitlint config in-repo. +VALIDATE_GIT_COMMITLINT=false +# Soft prose lint; markdownlint + codespell remain. +VALIDATE_NATURAL_LANGUAGE=false +# Pre-commit runs on developer machines (Hadolint); keep it out of Super Linter. +VALIDATE_PRE_COMMIT=false +# Python style/bugfinding: keep Black + mypy + Ruff; drop overlapping gates. +VALIDATE_PYTHON_FLAKE8=false +VALIDATE_PYTHON_ISORT=false +VALIDATE_PYTHON_PYLINT=false +# Black remains the Python formatter. +VALIDATE_PYTHON_RUFF_FORMAT=false diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index f4e109c6..00000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Black Lint - -on: [push, pull_request] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: psf/black@stable -# with: -# #options: "--check --verbose" -# #src: "." -# #version: "22.1.0" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 597a751f..e1224f74 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,11 @@ on: branches: - main pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened] + +permissions: + contents: read + jobs: lint_openapi: name: Lint OpenAPI Specification @@ -15,71 +19,53 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Required to mount the Github Workspace to a volume - - name: Lint OpenAPI Specification - uses: addnab/docker-run-action@v3 with: - image: ibmdevxsdk/openapi-validator:latest - options: -v ${{ github.workspace }}:/data -w /data - run: | + persist-credentials: false + - name: Lint OpenAPI Specification + run: | + docker run --rm \ + -v "${{ github.workspace }}:/data" \ + -w /data \ + ibmdevxsdk/openapi-validator:latest \ lint-openapi --ruleset /data/.spectral.yaml --warnings-limit 0 openapi.yaml - exit $? - lint_dockerfiles: - name: Lint Dockerfiles - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - dockerfile: - - tagbase_server/Dockerfile - - services/docker-cron/Dockerfile - - services/fswatch/Dockerfile - - services/nginx/Dockerfile - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Lint ${{ matrix.dockerfile }} - uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 - with: - dockerfile: ${{ matrix.dockerfile }} - config: .hadolint.yaml sonarcloud: name: SonarCloud Analysis runs-on: ubuntu-latest steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # Disabling shallow clone is recommended for improving relevancy of reporting - fetch-depth: 0 - - name: Setup Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.14" - - name: Install tox and any other packages - run: python -m pip install tox - - name: Run tox - working-directory: tagbase_server - run: tox -e py - - name: Fix code coverage paths - working-directory: tagbase_server/tagbase_server - run: | - sed -i "s/\/home\/runner\/work\/tagbase-server\/tagbase-server\/tagbase_server\/tagbase_server/\/github\/workspace\/tagbase_server\/tagbase_server\//g" coverage.xml - - name: SonarCloud Analysis - if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') }} - uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - with: - projectBaseDir: tagbase_server/tagbase_server - args: > - -Dsonar.exclusions=test/** - -Dsonar.organization=tagbase - -Dsonar.projectKey=tagbase_tagbase-server - -Dsonar.python.coverage.reportPaths=coverage.xml - -Dsonar.python.version=3.10,3.11,3.12,3.13,3.14 - -Dsonar.sources=. - -Dsonar.tests=test - -Dsonar.test.inclusions=test/** - -Dsonar.verbose=true + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting + fetch-depth: 0 + persist-credentials: false + - name: Setup Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + - name: Install tox and any other packages + run: python -m pip install tox + - name: Run tox + working-directory: tagbase_server + run: tox -e py + - name: SonarCloud Analysis + if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') }} + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + projectBaseDir: tagbase_server/tagbase_server + args: > + -Dsonar.exclusions=test/** + -Dsonar.organization=tagbase + -Dsonar.projectKey=tagbase_tagbase-server + -Dsonar.python.coverage.reportPaths=coverage.xml + -Dsonar.python.version=3.10,3.11,3.12,3.13,3.14 + -Dsonar.sourceEncoding=UTF-8 + -Dsonar.sources=. + -Dsonar.tests=test + -Dsonar.test.inclusions=test/** + -Dsonar.test.exclusions=test/fixtures/** + -Dsonar.verbose=true tox_tests: name: Tox Testing runs-on: ubuntu-latest @@ -88,6 +74,8 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: @@ -105,10 +93,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Start PostGIS run: | docker compose -f docker-compose.test.yml up -d postgis - for i in $(seq 1 60); do + for _ in $(seq 1 60); do if docker compose -f docker-compose.test.yml exec -T postgis \ pg_isready -d tagbase -U tagbase ; then break @@ -137,11 +127,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Start stack run: | mkdir -p staging_data docker compose -f docker-compose.test.yml --profile stack up -d --build - for i in $(seq 1 90); do + for _ in $(seq 1 90); do if curl -kf -s -o /dev/null -u testuser:testpass \ https://localhost/tagbase/api/v0.14.0/tags ; then break @@ -172,5 +164,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Run observability smoke run: ./scripts/observability-smoke.sh diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml new file mode 100644 index 00000000..6bbc8e36 --- /dev/null +++ b/.github/workflows/super-linter.yml @@ -0,0 +1,28 @@ +name: Super Linter + +on: [push, pull_request] + +permissions: {} + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + packages: read + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Load Super Linter configuration + run: grep -v '^#' .github/super-linter.env >> "$GITHUB_ENV" + + - name: Super-linter + uses: super-linter/super-linter@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.htmlhintrc b/.htmlhintrc new file mode 100644 index 00000000..cf9d7c23 --- /dev/null +++ b/.htmlhintrc @@ -0,0 +1,8 @@ +{ + "doctype-first": true, + "title-require": false, + "attr-lowercase": true, + "tag-pair": true, + "space-tab-mixed-disabled": "space", + "id-unique": true +} diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 00000000..8864f0d5 --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,12 @@ +{ + "threshold": 25, + "reporters": ["consoleFull"], + "ignore": [ + "**/__snapshots__/**", + "**/node_modules/**", + "**/.tox/**", + "**/openapi.yaml", + "**/docker-compose.test.yml" + ], + "absolute": true +} diff --git a/.markdown-lint.yml b/.markdown-lint.yml new file mode 100644 index 00000000..0f5714f5 --- /dev/null +++ b/.markdown-lint.yml @@ -0,0 +1,13 @@ +--- +# Markdownlint rules tuned for README + agent docs. +MD013: + line_length: 1000 + code_blocks: false + tables: false +MD009: false +MD022: false +MD031: false +MD032: false +MD033: false +MD041: false +MD047: false diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 00000000..95034c1b --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,11 @@ +[mypy] +python_version = 3.10 +ignore_missing_imports = True +ignore_errors = True +warn_unused_ignores = False +pretty = True +exclude = (?x)( + ^services/ + | ^tagbase_server/\.tox/ + | /test/ + ) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16c06f8a..8aa09547 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,6 @@ repos: - - repo: https://github.com/psf/black - rev: 26.5.1 - hooks: - - id: black - language_version: python3 - repo: https://github.com/AleksaC/hadolint-py rev: v2.14.0 hooks: - id: hadolint args: ["--config", ".hadolint.yaml"] - - repo: local - hooks: - - id: pytest-diff-cover - name: Coverage on changed lines (≥80%) - entry: scripts/check-diff-coverage.sh - language: system - pass_filenames: false - files: ^tagbase_server/tagbase_server/.*\.py$ - exclude: ^tagbase_server/tagbase_server/test/ diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..31eed4fd --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,15 @@ +line-length = 120 +exclude = [".tox", ".git", "__pycache__", "tagbase_server/.tox"] + +[lint] +# I = isort-compatible import sorting (replaces PYTHON_ISORT). +extend-select = ["I"] +ignore = ["E501", "E721"] + +[lint.per-file-ignores] +"tagbase_server/tagbase_server/models/*" = ["F401", "F811"] +"tagbase_server/tagbase_server/utils/__init__.py" = ["F401", "F403"] +"tagbase_server/tagbase_server/util.py" = ["F401", "E721"] + +[lint.isort] +known-first-party = ["tagbase_server"] diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 00000000..02ea2edd --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,2 @@ +# ShellCheck config for Super Linter +disable=SC2034 diff --git a/.sqlfluff b/.sqlfluff new file mode 100644 index 00000000..a88ffd96 --- /dev/null +++ b/.sqlfluff @@ -0,0 +1,20 @@ +[sqlfluff] +dialect = postgres +templater = raw +max_line_length = 120 +# RF04: view output columns intentionally use names like `variable` / `depth` +# LT02: CREATE MATERIALIZED VIEW … AS SELECT indent rules fight WITH/JOIN layout +# L031/L034/L044: alias/join-key style noise on analytical views +exclude_rules = RF04,LT02,L031,L034,L044 + +[sqlfluff:indentation] +tab_space_size = 2 + +[sqlfluff:rules:capitalisation.keywords] +capitalisation_policy = upper + +[sqlfluff:rules:capitalisation.identifiers] +extended_capitalisation_policy = lower + +[sqlfluff:rules:capitalisation.functions] +extended_capitalisation_policy = lower diff --git a/.yaml-lint.yml b/.yaml-lint.yml new file mode 100644 index 00000000..592a1087 --- /dev/null +++ b/.yaml-lint.yml @@ -0,0 +1,17 @@ +--- +extends: default +rules: + document-start: disable + line-length: + max: 300 + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 1 + indentation: disable + comments: disable + comments-indentation: disable + truthy: disable + new-line-at-end-of-file: enable diff --git a/AGENTS.md b/AGENTS.md index 7b47f61d..50ba07aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Default mattpocock triage vocabulary (`needs-triage`, `needs-info`, `ready-for-a ### Domain docs -Single-context layout (`CONTEXT.md` + `docs/adr/` at repo root). See `docs/agents/domain.md`. +Single-context layout (`CONTEXT.md` + `docs/adr/` at repository root). See `docs/agents/domain.md`. ### Ingest diff --git a/README.md b/README.md index 174ed8a1..c1ae637e 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Code Smells - - Code style: black + + Super-Linter CI/CD @@ -56,15 +56,18 @@ tagbase-server facilitates ingestion operations via REST courtesy of the [OpenAP ## Documentation -See the [project wiki](https://github.com/tagbase/tagbase-server/wiki) for guidance on -* [Installation](https://github.com/tagbase/tagbase-server/wiki/Installation) -* [Operations](https://github.com/tagbase/tagbase-server/wiki/Operations) -* [Release Management](https://github.com/tagbase/tagbase-server/wiki/Release-Management-Guide) -* [System Architecture](https://github.com/tagbase/tagbase-server/wiki/Systems-Architecture) -* [Working with OpenAPI](https://github.com/tagbase/tagbase-server/wiki/Working-with-the-OpenAPI-Specification-a.k.a-openapi.yaml) +See the [project wiki](https://github.com/tagbase/tagbase-server/wiki) for guidance on + +- [Installation](https://github.com/tagbase/tagbase-server/wiki/Installation) +- [Operations](https://github.com/tagbase/tagbase-server/wiki/Operations) +- [Release Management](https://github.com/tagbase/tagbase-server/wiki/Release-Management-Guide) +- [System Architecture](https://github.com/tagbase/tagbase-server/wiki/Systems-Architecture) +- [Working with OpenAPI](https://github.com/tagbase/tagbase-server/wiki/Working-with-the-OpenAPI-Specification-a.k.a-openapi.yaml) ## Development, Support and Community + Please create a [issue](https://github.com/tagbase/tagbase-server/issues). ## License + Licensed permissively under the Apache License v2.0, a copy of which ships with this software. diff --git a/docker-compose.yml b/docker-compose.yml index b54cac04..5606628b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -175,7 +175,7 @@ services: slack_docker: environment: - - webhook=${webhook:-} + - webhook=${WEBHOOK:-} image: ghcr.io/int128/slack-docker labels: #"docker_compose_diagram.cluster": "Internal Network" @@ -328,4 +328,4 @@ volumes: prometheus-data: loki-data: tempo-data: - grafana-data: \ No newline at end of file + grafana-data: diff --git a/docs/agents/domain.md b/docs/agents/domain.md index b548c538..eaa0c0d5 100644 --- a/docs/agents/domain.md +++ b/docs/agents/domain.md @@ -1,20 +1,20 @@ # Domain Docs -How the engineering skills should consume this repo's domain documentation when exploring the codebase. +How the engineering skills should consume this repository's domain documentation when exploring the codebase. ## Before exploring, read these -- **`CONTEXT.md`** at the repo root, or -- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. +- **`CONTEXT.md`** at the repository root, or +- **`CONTEXT-MAP.md`** at the repository root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. - **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved. ## File structure -Single-context repo (most repos): +Single-context repository (most repos): -``` +```text / ├── CONTEXT.md ├── docs/adr/ @@ -23,9 +23,9 @@ Single-context repo (most repos): └── src/ ``` -Multi-context repo (presence of `CONTEXT-MAP.md` at the root): +Multi-context repository (presence of `CONTEXT-MAP.md` at the root): -``` +```text / ├── CONTEXT-MAP.md ├── docs/adr/ ← system-wide decisions diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md index ef15a1c3..53faf5b6 100644 --- a/docs/agents/issue-tracker.md +++ b/docs/agents/issue-tracker.md @@ -1,21 +1,21 @@ # Issue tracker: GitHub -Issues and PRDs for this repo live as GitHub issues on `tagbase/tagbase-server`. Use the `gh` CLI for all operations. +Issues and PRDs for this repository live as GitHub issues on `tagbase/tagbase-server`. Use the `gh` CLI for all operations. ## Conventions -- **Create an issue**: `gh issue create --repo tagbase/tagbase-server --title "..." --body "..."`. Use a heredoc for multi-line bodies. -- **Read an issue**: `gh issue view --repo tagbase/tagbase-server --comments`, filtering comments by `jq` and also fetching labels. -- **List issues**: `gh issue list --repo tagbase/tagbase-server --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. -- **Comment on an issue**: `gh issue comment --repo tagbase/tagbase-server --body "..."` -- **Apply / remove labels**: `gh issue edit --repo tagbase/tagbase-server --add-label "..."` / `--remove-label "..."` -- **Close**: `gh issue close --repo tagbase/tagbase-server --comment "..."` +- **Create an issue**: `gh issue create --repository tagbase/tagbase-server --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --repository tagbase/tagbase-server --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --repository tagbase/tagbase-server --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --repository tagbase/tagbase-server --body "..."` +- **Apply / remove labels**: `gh issue edit --repository tagbase/tagbase-server --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --repository tagbase/tagbase-server --comment "..."` -Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone of `tagbase/tagbase-server`. +Infer the repository from `git remote -v` — `gh` does this automatically when run inside a clone of `tagbase/tagbase-server`. ## Pull requests as a triage surface -**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_ +**PRs as a request surface: no.** _(Set to `yes` if this repository treats external PRs as feature requests; `/triage` reads this flag.)_ When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents: @@ -39,7 +39,7 @@ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tic - **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`. - **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #` at the top of the child body. Labels: `wayfinder:` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev. -- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos///issues//dependencies/blocked_by -F issue_id=`, where `` is the blocker's numeric **database id** (`gh api repos///issues/ --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #, #` line at the top of the child body. A ticket is unblocked when every blocker is closed. +- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos///issues//dependencies/blocked_by -F issue_id=`, where `` is the blocker's numeric **database id** (`gh api repos///issues/ --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #, #` line at the top of the child body. A ticket is unblocked when every blocker is closed. - **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins. - **Claim**: `gh issue edit --add-assignee @me` — the session's first write. - **Resolve**: `gh issue comment --body ""`, then `gh issue close `, then append a context pointer (gist + link) to the map's Decisions-so-far. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md index b716855d..1d6e85fc 100644 --- a/docs/agents/triage-labels.md +++ b/docs/agents/triage-labels.md @@ -1,6 +1,6 @@ # Triage Labels -The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repository's issue tracker. | Label in mattpocock/skills | Label in our tracker | Meaning | | -------------------------- | -------------------- | ---------------------------------------- | diff --git a/openapi.yaml b/openapi.yaml index 405b6374..45277469 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -15,59 +15,59 @@ info: title: tagbase-server API version: v0.14.0 servers: -- description: Local development tagbase-server - url: https://localhost/tagbase/api/v0.14.0 -- description: ICCAT Test tagbase-server - url: https://162.13.162.49/tagbase/api/v0.14.0 + - description: Local development tagbase-server + url: https://localhost/tagbase/api/v0.14.0 + - description: ICCAT Test tagbase-server + url: https://162.13.162.49/tagbase/api/v0.14.0 tags: -- description: Ingestion operations. - name: ingest -- description: Tag Operations. - name: tags + - description: Ingestion operations. + name: ingest + - description: Tag Operations. + name: tags paths: /ingest: get: description: Get network accessible file and execute ingestion operationId: ingest_get parameters: - - description: Location of a network accessible (file, ftp, http, https) - file e.g. 'file:///usr/src/app/data/eTUFF-sailfish-117259.txt'. - explode: true - in: query - name: file - required: true - schema: - maxLength: 100 - minLength: 10 - pattern: ^(?!\s*$).+$ - type: string - style: form - - $ref: '#/components/parameters/notes' - - $ref: '#/components/parameters/type' - - $ref: '#/components/parameters/version' + - description: Location of a network accessible (file, ftp, http, https) + file e.g. 'file:///usr/src/app/data/eTUFF-sailfish-117259.txt'. + explode: true + in: query + name: file + required: true + schema: + maxLength: 100 + minLength: 10 + pattern: ^(?!\s*$).+$ + type: string + style: form + - $ref: "#/components/parameters/notes" + - $ref: "#/components/parameters/type" + - $ref: "#/components/parameters/version" responses: "200": content: application/json: schema: - $ref: '#/components/schemas/Ingest200' + $ref: "#/components/schemas/Ingest200" description: A success message confirming ingestion. "400": - $ref: '#/components/responses/BadRequest' + $ref: "#/components/responses/BadRequest" "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Get network accessible file and execute ingestion tags: - - ingest + - ingest x-openapi-router-controller: tagbase_server.controllers.ingest_controller post: description: Post a local file and perform a ingest operation operationId: ingest_post parameters: - - $ref: '#/components/parameters/filename' - - $ref: '#/components/parameters/notes' - - $ref: '#/components/parameters/type' - - $ref: '#/components/parameters/version' + - $ref: "#/components/parameters/filename" + - $ref: "#/components/parameters/notes" + - $ref: "#/components/parameters/type" + - $ref: "#/components/parameters/version" requestBody: content: application/octet-stream: @@ -91,15 +91,15 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Ingest200' + $ref: "#/components/schemas/Ingest200" description: A success message confirming ingestion. "400": - $ref: '#/components/responses/BadRequest' + $ref: "#/components/responses/BadRequest" "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Post a local file and perform a ingest operation tags: - - ingest + - ingest x-codegen-request-body-name: body x-openapi-router-controller: tagbase_server.controllers.ingest_controller /tags: @@ -110,10 +110,10 @@ paths: "204": description: Successful delete operation. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Delete all tags tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller get: description: Get information about all tags @@ -123,17 +123,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Tag200Collection' + $ref: "#/components/schemas/Tag200Collection" description: A success message confirming ingestion. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Get information about all tags tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller /tags/{tag_id}: parameters: - - $ref: '#/components/parameters/tagId' + - $ref: "#/components/parameters/tagId" delete: description: Delete an individual tag operationId: delete_tag @@ -141,10 +141,10 @@ paths: "204": description: Successful delete operation. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Delete an individual tag tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller get: description: Get information about an individual tag @@ -154,18 +154,18 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Tag200' + $ref: "#/components/schemas/Tag200" description: A success message confirming ingestion. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Get information about an individual tag tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller /tags/{tag_id}/subs/{sub_id}: parameters: - - $ref: '#/components/parameters/subId' - - $ref: '#/components/parameters/tagId' + - $ref: "#/components/parameters/subId" + - $ref: "#/components/parameters/tagId" delete: description: Delete a tag submission operationId: delete_tag_sub @@ -173,29 +173,29 @@ paths: "204": description: Successful delete operation. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Delete a tag submission tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller put: description: Update a tag submission operationId: replace_tag_sub parameters: - - $ref: '#/components/parameters/notes' - - $ref: '#/components/parameters/version' + - $ref: "#/components/parameters/notes" + - $ref: "#/components/parameters/version" responses: "200": content: application/json: schema: - $ref: '#/components/schemas/TagPut200' + $ref: "#/components/schemas/TagPut200" description: A success message confirming ingestion. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Update the 'notes' and/or 'version' associated with a tag submission tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller components: responses: @@ -204,13 +204,13 @@ components: content: application/problem+json: schema: - $ref: '#/components/schemas/Problem' + $ref: "#/components/schemas/Problem" InternalError: description: Unexpected server error. Contact admin detailed in openapi.yaml. content: application/problem+json: schema: - $ref: '#/components/schemas/Problem' + $ref: "#/components/schemas/Problem" parameters: filename: description: Free-form text field to explicitly define the name of the file to be persisted @@ -357,8 +357,9 @@ components: description: HTTP 200 success response example: code: "200" - elapsed: '0:00:06.506691' - message: Data file eTUFF-sailfish-117259.txt successfully ingested into Tagbase + elapsed: "0:00:06.506691" + message: + Data file eTUFF-sailfish-117259.txt successfully ingested into Tagbase DB. properties: code: @@ -376,7 +377,8 @@ components: pattern: ^(?!\s*$).+$ message: description: A string detailing specifics of an HTTP operation - example: Data file eTUFF-sailfish-117259.txt successfully ingested into + example: + Data file eTUFF-sailfish-117259.txt successfully ingested into Tagbase DB. type: string minLength: 1 @@ -404,45 +406,45 @@ components: - tag example: tag_id: 3 - filename: 'eTUFF-sailfish-117259_2.txt' + filename: "eTUFF-sailfish-117259_2.txt" tag: - - dataset_id: 1 - date_time: '2022-04-01T04:58:21.319061+00:00' - filename: 'eTUFF-sailfish-117259_2.txt' - hash_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b977' - metadata: - person_owner: John Do - owner_contect: john@do.net - manufacturer: telemetry inc. - model: new_gen - attachment_method: anchor - notes: 'Ingested by admin on 2022-06-01 for Sailfish tagging campaign.' - submission_id: 5 - tag_id: 3 - version: '1' - - dataset_id: 1 - date_time: '2022-06-01T05:39:46.896088+00:00' - filename: 'eTUFF-sailfish-117259_2.txt' - hash_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' - metadata: - person_owner: Jane Do - owner_contect: jane@do.net - manufacturer: telemetry inc. - model: newer_gen - attachment_method: anchor - notes: 'Ingested by admin on 2022-06-01 for version 2 of the Sailfish tagging campaign.' - submission_id: 6 - tag_id: 3 - version: '2' + - dataset_id: 1 + date_time: "2022-04-01T04:58:21.319061+00:00" + filename: "eTUFF-sailfish-117259_2.txt" + hash_sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b977" + metadata: + person_owner: John Do + owner_contect: john@do.net + manufacturer: telemetry inc. + model: new_gen + attachment_method: anchor + notes: "Ingested by admin on 2022-06-01 for Sailfish tagging campaign." + submission_id: 5 + tag_id: 3 + version: "1" + - dataset_id: 1 + date_time: "2022-06-01T05:39:46.896088+00:00" + filename: "eTUFF-sailfish-117259_2.txt" + hash_sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + metadata: + person_owner: Jane Do + owner_contect: jane@do.net + manufacturer: telemetry inc. + model: newer_gen + attachment_method: anchor + notes: "Ingested by admin on 2022-06-01 for version 2 of the Sailfish tagging campaign." + submission_id: 6 + tag_id: 3 + version: "2" properties: tag_id: - $ref: '#/components/schemas/TagId' + $ref: "#/components/schemas/TagId" filename: - $ref: '#/components/schemas/Filename' + $ref: "#/components/schemas/Filename" tag: description: List containing one or more submissions for a given tag items: - $ref: '#/components/schemas/TagSubmission' + $ref: "#/components/schemas/TagSubmission" minItems: 1 maxItems: 100 type: array @@ -477,9 +479,9 @@ components: filename: eTUFF-sailfish-117259_2.txt properties: tag_id: - $ref: '#/components/schemas/TagId' + $ref: "#/components/schemas/TagId" filename: - $ref: '#/components/schemas/Filename' + $ref: "#/components/schemas/Filename" type: object TagSubmission: type: object @@ -493,22 +495,22 @@ components: maximum: 2147483647 date_time: description: Local datetime stamp at the time of eTUFF tag data file ingestion - example: '2022-04-01T04:58:21.319061+00:00' + example: "2022-04-01T04:58:21.319061+00:00" format: date-time type: string minLength: 20 maxLength: 64 filename: - $ref: '#/components/schemas/Filename' + $ref: "#/components/schemas/Filename" hash_sha256: description: SHA256 hash representing the contents of the submission eTUFF file - example: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + example: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" type: string minLength: 64 maxLength: 64 pattern: ^[0-9a-fA-F]{64}$ metadata: - $ref: '#/components/schemas/Metadata' + $ref: "#/components/schemas/Metadata" notes: description: Free-form text field where details of submitted eTUFF file for ingest can be provided e.g. submitter name, etuff data contents (tag metadata and measurements + primary position data, or just secondary solutionpositional meta/data) example: "Ingested by admin on XXXX-XX-XX to back-process campaign XYZ." @@ -524,7 +526,7 @@ components: minimum: 1 maximum: 2147483647 tag_id: - $ref: '#/components/schemas/TagId' + $ref: "#/components/schemas/TagId" version: description: Version identifier for the eTUFF tag data file ingested example: "1" @@ -539,10 +541,10 @@ components: example: count: 2 tags: - - tag_id: 1 - filename: eTUFF-sailfish-117259_2.txt - - tag_id: 2 - filename: eTUFF-sailfish-117259.txt + - tag_id: 1 + filename: eTUFF-sailfish-117259_2.txt + - tag_id: 2 + filename: eTUFF-sailfish-117259.txt properties: count: description: Total count of unique tags @@ -555,7 +557,7 @@ components: description: List of unique numeric Tag IDs and associated filename type: array items: - $ref: '#/components/schemas/Tag200Summary' + $ref: "#/components/schemas/Tag200Summary" minItems: 1 maxItems: 100000 title: Tag200Collection diff --git a/openapitools.json b/openapitools.json deleted file mode 100644 index 7f483d15..00000000 --- a/openapitools.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", - "spaces": 2, - "generator-cli": { - "version": "5.4.0" - } -} diff --git a/scripts/check-diff-coverage.sh b/scripts/check-diff-coverage.sh deleted file mode 100755 index a01902d4..00000000 --- a/scripts/check-diff-coverage.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# Run the test suite and fail if coverage on lines changed vs the base -# branch is below 80% (mirrors the SonarCloud "Coverage on New Code" gate). -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SERVER_DIR="${ROOT}/tagbase_server" -cd "${SERVER_DIR}" - -COMPARE_BRANCH="${DIFF_COVER_COMPARE_BRANCH:-}" -if [[ -z "${COMPARE_BRANCH}" ]]; then - if git -C "${ROOT}" rev-parse --verify --quiet origin/main >/dev/null; then - COMPARE_BRANCH="origin/main" - elif git -C "${ROOT}" rev-parse --verify --quiet main >/dev/null; then - COMPARE_BRANCH="main" - else - echo "error: cannot find origin/main or main to compare against" >&2 - exit 1 - fi -fi - -# Prefer an existing tox env; otherwise create one for the current Python. -TOX_ENV="" -for candidate in py314 py313 py312 py311 py310 py; do - if [[ -x "${SERVER_DIR}/.tox/${candidate}/bin/python" ]]; then - TOX_ENV="${candidate}" - break - fi -done - -if [[ -z "${TOX_ENV}" ]]; then - if ! command -v tox >/dev/null 2>&1; then - python3 -m pip install --user --quiet 'tox>=4' - fi - tox -e py --notest - TOX_ENV="$(basename "$(ls -d "${SERVER_DIR}"/.tox/py* | head -1)")" -fi - -ENV_BIN="${SERVER_DIR}/.tox/${TOX_ENV}/bin" -"${ENV_BIN}/python" -m pip install --quiet 'diff-cover==10.3.0' - -COVERAGE_XML="${SERVER_DIR}/tagbase_server/coverage.xml" -rm -f "${COVERAGE_XML}" - -"${ENV_BIN}/pytest" \ - --cov-config=.coveragerc \ - --cov=tagbase_server \ - --cov-branch \ - --cov-report="xml:${COVERAGE_XML}" \ - --cov-report=term-missing:skip-covered \ - -q - -# Coverage XML filenames are package-relative (e.g. models/tag200.py). -# Rewrite them to repo-root paths so diff-cover can match git diff output. -python3 - "${COVERAGE_XML}" <<'PY' -import pathlib -import sys -import xml.etree.ElementTree as ET - -coverage_xml = pathlib.Path(sys.argv[1]) -prefix = "tagbase_server/tagbase_server/" -tree = ET.parse(coverage_xml) -root = tree.getroot() -for cls in root.iter("class"): - filename = cls.attrib.get("filename", "") - if not filename or filename.startswith(prefix): - continue - if filename.startswith("tagbase_server/"): - cls.set("filename", f"tagbase_server/{filename}") - else: - cls.set("filename", f"{prefix}{filename}") -tree.write(coverage_xml) -PY - -cd "${ROOT}" -echo "Comparing coverage for changes against ${COMPARE_BRANCH}..." -"${ENV_BIN}/diff-cover" "${COVERAGE_XML}" \ - --compare-branch="${COMPARE_BRANCH}" \ - --fail-under=80 \ - --include='tagbase_server/tagbase_server/**' \ - --exclude='tagbase_server/tagbase_server/test/**' diff --git a/scripts/observability-smoke.sh b/scripts/observability-smoke.sh index a4bb4d52..6278ac21 100755 --- a/scripts/observability-smoke.sh +++ b/scripts/observability-smoke.sh @@ -15,15 +15,15 @@ FIXTURE="${FIXTURE:-tagbase_server/tagbase_server/test/fixtures/etuff/minimal-et TIMEOUT_SECS="${TIMEOUT_SECS:-240}" cleanup() { - "${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true + "${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true } trap cleanup EXIT # HTTP GET via tagbase_server (same Docker network; backends have no host ports). docker_http_get() { - local url="$1" - "${COMPOSE[@]}" exec -T tagbase_server \ - python -c "import urllib.request; print(urllib.request.urlopen('${url}', timeout=30).read().decode())" + local url="$1" + "${COMPOSE[@]}" exec -T tagbase_server \ + python -c "import urllib.request; print(urllib.request.urlopen('${url}', timeout=30).read().decode())" } echo "==> Starting observability test stack" @@ -42,23 +42,23 @@ GRAFANA_BASE="http://grafana:3000/grafana" echo "==> Waiting for Prometheus / Tempo / Grafana / tagbase_server / nginx" deadline=$((SECONDS + TIMEOUT_SECS)) ready=0 -while (( SECONDS < deadline )); do - if docker_http_get "${PROM_BASE}/-/ready" >/dev/null 2>&1 \ - && docker_http_get "http://tempo:3200/ready" >/dev/null 2>&1 \ - && docker_http_get "${GRAFANA_BASE}/api/health" >/dev/null 2>&1 \ - && docker_http_get "http://127.0.0.1:5433/tagbase/api/v0.14.0/tags" >/dev/null 2>&1 \ - && curl -kf -s -o /dev/null -u "${GATEWAY_USER}:${GATEWAY_PASS}" \ - "${GATEWAY_URL}/tagbase/api/v0.14.0/tags"; then - ready=1 - break - fi - sleep 5 +while ((SECONDS < deadline)); do + if docker_http_get "${PROM_BASE}/-/ready" >/dev/null 2>&1 && + docker_http_get "http://tempo:3200/ready" >/dev/null 2>&1 && + docker_http_get "${GRAFANA_BASE}/api/health" >/dev/null 2>&1 && + docker_http_get "http://127.0.0.1:5433/tagbase/api/v0.14.0/tags" >/dev/null 2>&1 && + curl -kf -s -o /dev/null -u "${GATEWAY_USER}:${GATEWAY_PASS}" \ + "${GATEWAY_URL}/tagbase/api/v0.14.0/tags"; then + ready=1 + break + fi + sleep 5 done if [[ "$ready" -ne 1 ]]; then - echo "ERROR: stack not ready within ${TIMEOUT_SECS}s" >&2 - "${COMPOSE[@]}" ps >&2 || true - "${COMPOSE[@]}" logs --tail=80 tagbase_server alloy prometheus tempo grafana nginx >&2 || true - exit 1 + echo "ERROR: stack not ready within ${TIMEOUT_SECS}s" >&2 + "${COMPOSE[@]}" ps >&2 || true + "${COMPOSE[@]}" logs --tail=80 tagbase_server alloy prometheus tempo grafana nginx >&2 || true + exit 1 fi # Allow OTel metric export interval (10s) + scrape path @@ -66,8 +66,9 @@ sleep 5 echo "==> Ingesting fixture ${FIXTURE}" "${COMPOSE[@]}" cp "${FIXTURE}" tagbase_server:/tmp/minimal-etuff.txt -ingest_status="$("${COMPOSE[@]}" exec -T tagbase_server \ - python - <<'PY' +ingest_status="$( + "${COMPOSE[@]}" exec -T tagbase_server \ + python - <<'PY' import urllib.error import urllib.parse import urllib.request @@ -87,39 +88,39 @@ echo "Ingest HTTP status: ${ingest_status}" echo "==> Waiting for tagbase_ingest_requests_total in Prometheus" metric_ok=0 deadline=$((SECONDS + TIMEOUT_SECS)) -while (( SECONDS < deadline )); do - body="$(docker_http_get "${PROM_BASE}/api/v1/query?query=sum(tagbase_ingest_requests_total)" || true)" - if echo "$body" | grep -Eq '"value":\[[0-9.]+,"[0-9.]+"\]'; then - metric_ok=1 - break - fi - sleep 5 +while ((SECONDS < deadline)); do + body="$(docker_http_get "${PROM_BASE}/api/v1/query?query=sum(tagbase_ingest_requests_total)" || true)" + if echo "$body" | grep -Eq '"value":\[[0-9.]+,"[0-9.]+"\]'; then + metric_ok=1 + break + fi + sleep 5 done if [[ "$metric_ok" -ne 1 ]]; then - echo "ERROR: metric tagbase_ingest_requests_total not found" >&2 - docker_http_get "${PROM_BASE}/api/v1/query?query=tagbase_ingest_requests_total" >&2 || true - "${COMPOSE[@]}" logs --tail=80 alloy tagbase_server >&2 || true - exit 1 + echo "ERROR: metric tagbase_ingest_requests_total not found" >&2 + docker_http_get "${PROM_BASE}/api/v1/query?query=tagbase_ingest_requests_total" >&2 || true + "${COMPOSE[@]}" logs --tail=80 alloy tagbase_server >&2 || true + exit 1 fi echo "OK: Prometheus has tagbase_ingest_requests_total" echo "==> Searching Tempo for ingest spans" trace_ok=0 deadline=$((SECONDS + TIMEOUT_SECS)) -while (( SECONDS < deadline )); do - # Tempo search API (TraceQL) - body="$(docker_http_get 'http://tempo:3200/api/search?q=%7B%20resource.service.name%3D%22tagbase_server%22%20%26%26%20name%3D~%22ingest.%2A%22%20%7D&limit=20' || true)" - if echo "$body" | grep -q '"traceID"'; then - trace_ok=1 - break - fi - sleep 5 +while ((SECONDS < deadline)); do + # Tempo search API (TraceQL) + body="$(docker_http_get 'http://tempo:3200/api/search?q=%7B%20resource.service.name%3D%22tagbase_server%22%20%26%26%20name%3D~%22ingest.%2A%22%20%7D&limit=20' || true)" + if echo "$body" | grep -q '"traceID"'; then + trace_ok=1 + break + fi + sleep 5 done if [[ "$trace_ok" -ne 1 ]]; then - echo "ERROR: no ingest.* spans found in Tempo" >&2 - docker_http_get 'http://tempo:3200/api/search?q=%7B%20resource.service.name%3D%22tagbase_server%22%20%7D&limit=20' >&2 || true - "${COMPOSE[@]}" logs --tail=80 alloy tempo tagbase_server >&2 || true - exit 1 + echo "ERROR: no ingest.* spans found in Tempo" >&2 + docker_http_get 'http://tempo:3200/api/search?q=%7B%20resource.service.name%3D%22tagbase_server%22%20%7D&limit=20' >&2 || true + "${COMPOSE[@]}" logs --tail=80 alloy tempo tagbase_server >&2 || true + exit 1 fi echo "OK: Tempo has ingest spans" @@ -127,48 +128,48 @@ echo "==> Checking Grafana datasource proxy (Prometheus + Tempo)" grafana_prom_ok=0 grafana_tempo_ok=0 deadline=$((SECONDS + TIMEOUT_SECS)) -while (( SECONDS < deadline )); do - prom_body="$(docker_http_get \ - "${GRAFANA_BASE}/api/datasources/proxy/uid/prometheus/api/v1/query?query=sum(tagbase_ingest_requests_total)" \ - || true)" - if echo "$prom_body" | grep -Eq '"value":\[[0-9.]+,"[0-9.]+"\]'; then - grafana_prom_ok=1 - fi - tempo_body="$(docker_http_get \ - "${GRAFANA_BASE}/api/datasources/proxy/uid/tempo/api/search?q=%7B%20resource.service.name%3D%22tagbase_server%22%20%26%26%20name%3D~%22ingest.%2A%22%20%7D&limit=20" \ - || true)" - if echo "$tempo_body" | grep -q '"traceID"'; then - grafana_tempo_ok=1 - fi - if [[ "$grafana_prom_ok" -eq 1 && "$grafana_tempo_ok" -eq 1 ]]; then - break - fi - sleep 5 +while ((SECONDS < deadline)); do + prom_body="$(docker_http_get \ + "${GRAFANA_BASE}/api/datasources/proxy/uid/prometheus/api/v1/query?query=sum(tagbase_ingest_requests_total)" || + true)" + if echo "$prom_body" | grep -Eq '"value":\[[0-9.]+,"[0-9.]+"\]'; then + grafana_prom_ok=1 + fi + tempo_body="$(docker_http_get \ + "${GRAFANA_BASE}/api/datasources/proxy/uid/tempo/api/search?q=%7B%20resource.service.name%3D%22tagbase_server%22%20%26%26%20name%3D~%22ingest.%2A%22%20%7D&limit=20" || + true)" + if echo "$tempo_body" | grep -q '"traceID"'; then + grafana_tempo_ok=1 + fi + if [[ "$grafana_prom_ok" -eq 1 && "$grafana_tempo_ok" -eq 1 ]]; then + break + fi + sleep 5 done if [[ "$grafana_prom_ok" -ne 1 ]]; then - echo "ERROR: Grafana Prometheus datasource proxy failed" >&2 - docker_http_get \ - "${GRAFANA_BASE}/api/datasources/proxy/uid/prometheus/api/v1/query?query=sum(tagbase_ingest_requests_total)" \ - >&2 || true - "${COMPOSE[@]}" logs --tail=80 grafana prometheus >&2 || true - exit 1 + echo "ERROR: Grafana Prometheus datasource proxy failed" >&2 + docker_http_get \ + "${GRAFANA_BASE}/api/datasources/proxy/uid/prometheus/api/v1/query?query=sum(tagbase_ingest_requests_total)" \ + >&2 || true + "${COMPOSE[@]}" logs --tail=80 grafana prometheus >&2 || true + exit 1 fi if [[ "$grafana_tempo_ok" -ne 1 ]]; then - echo "ERROR: Grafana Tempo datasource proxy failed" >&2 - docker_http_get \ - "${GRAFANA_BASE}/api/datasources/proxy/uid/tempo/api/search?q=%7B%20resource.service.name%3D%22tagbase_server%22%20%7D&limit=20" \ - >&2 || true - "${COMPOSE[@]}" logs --tail=80 grafana tempo >&2 || true - exit 1 + echo "ERROR: Grafana Tempo datasource proxy failed" >&2 + docker_http_get \ + "${GRAFANA_BASE}/api/datasources/proxy/uid/tempo/api/search?q=%7B%20resource.service.name%3D%22tagbase_server%22%20%7D&limit=20" \ + >&2 || true + "${COMPOSE[@]}" logs --tail=80 grafana tempo >&2 || true + exit 1 fi echo "OK: Grafana can query Prometheus and Tempo datasources" echo "==> Checking nginx gateway (/grafana/)" if ! curl -kf -s -o /dev/null -u "${GATEWAY_USER}:${GATEWAY_PASS}" \ - "${GATEWAY_URL}/grafana/"; then - echo "ERROR: gateway https://localhost/grafana/ failed" >&2 - "${COMPOSE[@]}" logs --tail=80 nginx grafana >&2 || true - exit 1 + "${GATEWAY_URL}/grafana/"; then + echo "ERROR: gateway https://localhost/grafana/ failed" >&2 + "${COMPOSE[@]}" logs --tail=80 nginx grafana >&2 || true + exit 1 fi echo "OK: nginx gateway serves /grafana/" diff --git a/services/docker-cron/__init__.py b/services/docker-cron/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/services/docker-cron/entrypoint.sh b/services/docker-cron/entrypoint.sh index 487d4d46..3b72c8f1 100755 --- a/services/docker-cron/entrypoint.sh +++ b/services/docker-cron/entrypoint.sh @@ -5,7 +5,7 @@ POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-tagbase}" POSTGRES_PORT="${POSTGRES_PORT:-5432}" # Runtime credentials for cron jobs (compose environment, not build ARGs). -cat > /usr/src/app/.env </usr/src/app/.env <"$DEDUPE_STATE" 2>/dev/null; then - DEDUPE_STATE="/tmp/fswatch-ingest-dedupe-$$" - : >"$DEDUPE_STATE" + DEDUPE_STATE="/tmp/fswatch-ingest-dedupe-$$" + : >"$DEDUPE_STATE" fi echo "Watching $PATH_TO_CHECK (dedupe=$DEDUPE_STATE)" # Detect GNU vs BSD stat once (not per-file — missing files must not flip dialects). if stat -c%s / >/dev/null 2>&1; then - file_size() { - [ -f "$1" ] || return 1 - stat -c%s "$1" - } + file_size() { + [ -f "$1" ] || return 1 + stat -c%s "$1" + } else - file_size() { - [ -f "$1" ] || return 1 - stat -f%z "$1" - } + file_size() { + [ -f "$1" ] || return 1 + stat -f%z "$1" + } fi urlencode() { - # Prefer python3 (available in fswatch image); fall back to raw name. - if command -v python3 >/dev/null 2>&1; then - python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1" - else - printf '%s' "$1" - fi + # Prefer python3 (available in fswatch image); fall back to raw name. + if command -v python3 >/dev/null 2>&1; then + python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1" + else + printf '%s' "$1" + fi } should_ingest() { - name=$1 - case "$name" in - .*|*.crdownload|*.part|*.tmp|*.download) - return 1 - ;; - Unconfirmed\ *) - return 1 - ;; - esac - case "$name" in - *.txt|*.zip|*.tar.gz|*.tgz) - return 0 - ;; - *) - return 1 - ;; - esac + name=$1 + case "$name" in + .* | *.crdownload | *.part | *.tmp | *.download) + return 1 + ;; + Unconfirmed\ *) + return 1 + ;; + esac + case "$name" in + *.txt | *.zip | *.tar.gz | *.tgz) + return 0 + ;; + *) + return 1 + ;; + esac } wait_stable() { - path=$1 - i=0 - while [ "$i" -lt 60 ]; do - [ -f "$path" ] || return 1 - a=$(file_size "$path") || return 1 - sleep 0.5 - [ -f "$path" ] || return 1 - b=$(file_size "$path") || return 1 - if [ "$a" -eq "$b" ]; then - return 0 - fi - i=$((i + 1)) - done - return 1 + path=$1 + i=0 + while [ "$i" -lt 60 ]; do + [ -f "$path" ] || return 1 + a=$(file_size "$path") || return 1 + sleep 0.5 + [ -f "$path" ] || return 1 + b=$(file_size "$path") || return 1 + if [ "$a" -eq "$b" ]; then + return 0 + fi + i=$((i + 1)) + done + return 1 } already_ingested() { - path=$1 - size=$2 - [ -f "$DEDUPE_STATE" ] || return 1 - { - read -r last_path - read -r last_size - } <"$DEDUPE_STATE" || return 1 - [ "$path" = "$last_path" ] && [ "$size" = "$last_size" ] + path=$1 + size=$2 + [ -f "$DEDUPE_STATE" ] || return 1 + { + read -r last_path + read -r last_size + } <"$DEDUPE_STATE" || return 1 + [ "$path" = "$last_path" ] && [ "$size" = "$last_size" ] } ingest_file() { - line=$1 - filename=${line##*/} - if ! should_ingest "$filename"; then - echo "Skipping non-ingestible: $filename" - return 0 - fi - echo "Contents of $PATH_TO_CHECK changed; Checking: $filename" - if ! wait_stable "$line"; then - echo "Gave up waiting for stable file: $filename" - return 0 - fi - size=$(file_size "$line") || return 0 - if already_ingested "$line" "$size"; then - echo "Already ingested at this size, skipping: $filename" - return 0 - fi - echo "Processing: $filename" - enc=$(urlencode "$filename") - ctype=application/octet-stream - case "$filename" in - *.txt) ctype=text/plain ;; - esac - if ! curl -sS -f -X POST \ - -H "accept: application/json" \ - -H "Content-Type: ${ctype}" \ - -T "$line" \ - "${TAGBASE_INGEST_BASE}/ingest?filename=${enc}&type=etuff"; then - echo "Ingest failed for $filename (see tagbase_server logs)" - else - printf '%s\n%s\n' "$line" "$size" >"$DEDUPE_STATE" - echo - echo "Ingest OK: $filename" - fi + line=$1 + filename=${line##*/} + if ! should_ingest "$filename"; then + echo "Skipping non-ingestible: $filename" + return 0 + fi + echo "Contents of $PATH_TO_CHECK changed; Checking: $filename" + if ! wait_stable "$line"; then + echo "Gave up waiting for stable file: $filename" + return 0 + fi + size=$(file_size "$line") || return 0 + if already_ingested "$line" "$size"; then + echo "Already ingested at this size, skipping: $filename" + return 0 + fi + echo "Processing: $filename" + enc=$(urlencode "$filename") + ctype=application/octet-stream + case "$filename" in + *.txt) ctype=text/plain ;; + esac + if ! curl -sS -f -X POST \ + -H "accept: application/json" \ + -H "Content-Type: ${ctype}" \ + -T "$line" \ + "${TAGBASE_INGEST_BASE}/ingest?filename=${enc}&type=etuff"; then + echo "Ingest failed for $filename (see tagbase_server logs)" + else + printf '%s\n%s\n' "$line" "$size" >"$DEDUPE_STATE" + echo + echo "Ingest OK: $filename" + fi } scan_existing() { - # Process files already present (startup / leftover from a prior blind window). - # shellcheck disable=SC2039 - for path in "$PATH_TO_CHECK"*; do - [ -f "$path" ] || continue - ingest_file "$path" - done + # Process files already present (startup / leftover from a prior blind window). + # shellcheck disable=SC2039 + for path in "$PATH_TO_CHECK"*; do + [ -f "$path" ] || continue + ingest_file "$path" + done } scan_existing @@ -135,16 +135,16 @@ scan_existing # Outer loop only restarts fswatch if the binary exits unexpectedly. FSWATCH_CMD="fswatch" if command -v stdbuf >/dev/null 2>&1; then - FSWATCH_CMD="stdbuf -oL fswatch" + FSWATCH_CMD="stdbuf -oL fswatch" fi while true; do - # shellcheck disable=SC2086 - $FSWATCH_CMD "$PATH_TO_CHECK" \ - --event Created --event MovedTo --event IsFile --event Updated \ - | while IFS= read -r line; do - ingest_file "$line" - done - echo "fswatch exited; restarting watch on $PATH_TO_CHECK" - sleep 1 + # shellcheck disable=SC2086 + $FSWATCH_CMD "$PATH_TO_CHECK" \ + --event Created --event MovedTo --event IsFile --event Updated | + while IFS= read -r line; do + ingest_file "$line" + done + echo "fswatch exited; restarting watch on $PATH_TO_CHECK" + sleep 1 done diff --git a/services/nginx/proxy/index.html b/services/nginx/proxy/index.html index 8965b8a0..cc005544 100644 --- a/services/nginx/proxy/index.html +++ b/services/nginx/proxy/index.html @@ -1,26 +1,86 @@ -

Welcome to tagbase server

+ + + + + tagbase-server + + +

+ Welcome to + tagbase server +

- + -

Navigation

- +

Navigation

+ -

Observability

-

Requires docker compose --profile observability. Protected by the same nginx basic auth.

- +

Observability

+

+ Requires docker compose --profile observability. Protected by + the same nginx basic auth. +

+ -

Documentation and Support

- +

Documentation and Support

+ + + diff --git a/services/observability/.env.observability.example b/services/observability/.env.observability.example index 3555e334..7948501d 100644 --- a/services/observability/.env.observability.example +++ b/services/observability/.env.observability.example @@ -1,29 +1,27 @@ -# Stack credentials (required by docker-compose.yml; these match local defaults) -POSTGRES_PASSWORD=tagbase -POSTGRES_PORT=5432 -PGADMIN_DEFAULT_EMAIL=admin@example.com -PGADMIN_DEFAULT_PASSWORD=tagbase -SLACK_BOT_TOKEN= -webhook= +# Example env for the observability Compose profile. +# Keys are alphabetical (dotenv-linter UnorderedKey). +# +# Stack credentials match local docker-compose defaults. +# Set ALLOY_CONFIG_FILE=config.cloud.alloy and fill PROM_/LOKI_/TEMPO_ +# when exporting to Grafana Cloud instead of the local LGTM stack. +# OTEL_* enables app OTLP export (required with the observability profile). -# Which Alloy config to mount (local LGTM is default) ALLOY_CONFIG_FILE=config.local.alloy - -# Enable app OTLP export (required when using the observability profile) -OTEL_SDK_DISABLED=false +LOKI_ENDPOINT= +LOKI_PASSWORD= +LOKI_USERNAME= OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy:4317 +OTEL_SDK_DISABLED=false OTEL_SERVICE_NAME=tagbase_server - -# ── Grafana Cloud (required only when ALLOY_CONFIG_FILE=config.cloud.alloy) ── -# From Grafana Cloud → Connections → OpenTelemetry / Prometheus / Loki / Tempo +PGADMIN_DEFAULT_EMAIL=admin@example.com +PGADMIN_DEFAULT_PASSWORD=tagbase +POSTGRES_PASSWORD=tagbase +POSTGRES_PORT=5432 PROM_ENDPOINT= -PROM_USERNAME= PROM_PASSWORD= - -LOKI_ENDPOINT= -LOKI_USERNAME= -LOKI_PASSWORD= - +PROM_USERNAME= +SLACK_BOT_TOKEN= TEMPO_ENDPOINT= -TEMPO_USERNAME= TEMPO_PASSWORD= +TEMPO_USERNAME= +WEBHOOK= diff --git a/services/postgis/tagbase_materialized_views.sql b/services/postgis/tagbase_materialized_views.sql index fd87a6ca..69b122aa 100644 --- a/services/postgis/tagbase_materialized_views.sql +++ b/services/postgis/tagbase_materialized_views.sql @@ -3,136 +3,252 @@ -- MATERIALIZED VIEW -CREATE MATERIALIZED VIEW mview_vis_data -AS - SELECT - variable.submission_id AS source_id, - variable.variable_value AS measurement_value, - variable.variable_name AS measurement_name, - variable.variable_units AS measurement_units, - depth.depth, - variable.date_time AS measurement_date_time, - data_position.date_time AS position_date_time, - data_position.lat, - CASE WHEN data_position.lon::double precision > 180 THEN data_position.lon::double precision - 360 ELSE data_position.lon::double precision END, - data_position.lat_err, - data_position.lon_err - FROM ( SELECT x.variable_value, - y.variable_name, - x.date_time, - x.submission_id, - y.variable_units, - x.position_date_time - FROM data_time_series x, - observation_types y - WHERE x.variable_id = y.variable_id AND y.variable_name <> 'depth' AND y.variable_name <> 'datetime') variable, - data_position, - ( SELECT x.variable_value AS depth, - x.date_time, - x.submission_id - FROM data_time_series x, - observation_types y - WHERE x.variable_id = y.variable_id AND y.variable_name = 'depth') depth - WHERE variable.submission_id = data_position.submission_id AND variable.submission_id = depth.submission_id AND variable.position_date_time = data_position.date_time AND depth.date_time = variable.date_time +CREATE MATERIALIZED VIEW mview_vis_data AS +WITH var_rows AS ( + SELECT + x.variable_value, + y.variable_name, + x.date_time, + x.submission_id, + y.variable_units, + x.position_date_time + FROM data_time_series AS x + INNER JOIN observation_types AS y + ON x.variable_id = y.variable_id + WHERE + y.variable_name <> 'depth' + AND y.variable_name <> 'datetime' +), + +depth_rows AS ( + SELECT + x.variable_value AS depth_value, + x.date_time, + x.submission_id + FROM data_time_series AS x + INNER JOIN observation_types AS y + ON x.variable_id = y.variable_id + WHERE y.variable_name = 'depth' +) + +SELECT + var_rows.submission_id AS source_id, + var_rows.variable_value AS measurement_value, + var_rows.variable_name AS measurement_name, + var_rows.variable_units AS measurement_units, + depth_rows.depth_value AS depth, + var_rows.date_time AS measurement_date_time, + data_position.date_time AS position_date_time, + data_position.lat, + CASE + WHEN data_position.lon::double precision > 180 + THEN data_position.lon::double precision - 360 + ELSE data_position.lon::double precision + END AS lon, + data_position.lat_err, + data_position.lon_err +FROM var_rows +INNER JOIN data_position + ON var_rows.submission_id = data_position.submission_id + AND var_rows.position_date_time = data_position.date_time +INNER JOIN depth_rows + ON var_rows.submission_id = depth_rows.submission_id + AND var_rows.date_time = depth_rows.date_time WITH DATA; -CREATE MATERIALIZED VIEW mview_vis_data_histogram -AS - SELECT - data.submission_id AS source_id, - data.min_value AS bin_class, - data.variable_value AS measurement_value, - data.date_time AS measurement_date_time, - data_position.date_time AS position_date_time, - data_position.lat, - CASE WHEN data_position.lon::double precision > 180 THEN data_position.lon::double precision - 360 ELSE data_position.lon::double precision END, - data_position.lat_err, - data_position.lon_err - FROM ( SELECT data_histogram_bin_info.min_value, - data_histogram_bin_data.submission_id, - data_histogram_bin_data.date_time, - data_histogram_bin_data.variable_value, - data_histogram_bin_data.position_date_time - FROM data_histogram_bin_info, - data_histogram_bin_data - WHERE data_histogram_bin_info.bin_id = data_histogram_bin_data.bin_id AND data_histogram_bin_info.bin_class = data_histogram_bin_data.bin_class) data, - data_position - WHERE data.submission_id = data_position.submission_id AND data.position_date_time = data_position.date_time +CREATE MATERIALIZED VIEW mview_vis_data_histogram AS +WITH hist_rows AS ( + SELECT + data_histogram_bin_info.min_value, + data_histogram_bin_data.submission_id, + data_histogram_bin_data.date_time, + data_histogram_bin_data.variable_value, + data_histogram_bin_data.position_date_time + FROM data_histogram_bin_info + INNER JOIN data_histogram_bin_data + ON data_histogram_bin_info.bin_id = data_histogram_bin_data.bin_id + AND data_histogram_bin_info.bin_class = data_histogram_bin_data.bin_class +) + +SELECT + hist_rows.submission_id AS source_id, + hist_rows.min_value AS bin_class, + hist_rows.variable_value AS measurement_value, + hist_rows.date_time AS measurement_date_time, + data_position.date_time AS position_date_time, + data_position.lat, + CASE + WHEN data_position.lon::double precision > 180 + THEN data_position.lon::double precision - 360 + ELSE data_position.lon::double precision + END AS lon, + data_position.lat_err, + data_position.lon_err +FROM hist_rows +INNER JOIN data_position + ON hist_rows.submission_id = data_position.submission_id + AND hist_rows.position_date_time = data_position.date_time WITH DATA; +CREATE MATERIALIZED VIEW mview_vis_data_profile AS +WITH profile_rows AS ( + SELECT + data_profile.submission_id, + data_profile.date_time, + data_profile.depth, + data_profile.variable_value, + data_profile.position_date_time + FROM data_profile +) -CREATE MATERIALIZED VIEW mview_vis_data_profile -AS - SELECT - data.submission_id AS source_id, - data.depth, - data.variable_value AS measurement_value, - data.date_time AS measurement_date_time, - data_position.date_time AS position_date_time, - data_position.lat, - CASE WHEN data_position.lon::double precision > 180 THEN data_position.lon::double precision - 360 ELSE data_position.lon::double precision END, - data_position.lat_err, - data_position.lon_err - FROM ( SELECT data_profile.submission_id, - data_profile.date_time, - data_profile.depth, - data_profile.variable_value, - data_profile.position_date_time - FROM data_profile) data, data_position - WHERE data.submission_id = data_position.submission_id AND data.position_date_time = data_position.date_time +SELECT + profile_rows.submission_id AS source_id, + profile_rows.depth, + profile_rows.variable_value AS measurement_value, + profile_rows.date_time AS measurement_date_time, + data_position.date_time AS position_date_time, + data_position.lat, + CASE + WHEN data_position.lon::double precision > 180 + THEN data_position.lon::double precision - 360 + ELSE data_position.lon::double precision + END AS lon, + data_position.lat_err, + data_position.lon_err +FROM profile_rows +INNER JOIN data_position + ON profile_rows.submission_id = data_position.submission_id + AND profile_rows.position_date_time = data_position.date_time WITH DATA; -CREATE MATERIALIZED VIEW mview_vis_metadata -AS - SELECT metadata.submission_id AS source_id, - 'Global Attributes'::text AS attribute_type, - NULL::character varying AS variable, - metadata_types.category, - metadata_types.attribute_name, - "left"("right"(metadata.attribute_value, length(metadata.attribute_value) - 1), '-1'::integer) AS attribute_value - FROM metadata_types, - metadata - WHERE metadata_types.attribute_id = metadata.attribute_id AND (metadata_types.category::text = 'instrument'::text AND (metadata_types.attribute_name::text = ANY (ARRAY['instrument_name'::character varying, 'instrument_type'::character varying, 'firmware'::character varying, 'manufacturer'::character varying, 'model'::character varying, 'owner_contact'::character varying, 'person_owner'::character varying, 'serial_number'::character varying]::text[])) OR metadata_types.category::text = 'programming'::text AND (metadata_types.attribute_name::text = ANY (ARRAY['programming_report'::character varying, 'programming_software'::character varying]::text[])) OR metadata_types.category::text = 'attachment'::text AND metadata_types.attribute_name::text = 'attachment_method'::text OR metadata_types.category::text = 'deployment'::text AND (metadata_types.attribute_name::text = ANY (ARRAY['geospatial_lat_start'::character varying, 'geospatial_lon_start'::character varying, 'person_tagger_capture'::character varying, 'time_coverage_start'::character varying]::text[])) OR metadata_types.category::text = 'animal'::text AND (metadata_types.attribute_name::text = ANY (ARRAY['condition_capture'::character varying, 'length_capture'::character varying, 'length_method_capture'::character varying, 'length_type_capture'::character varying, 'length_unit_capture'::character varying, 'platform'::character varying, 'taxonomic_serial_number'::character varying]::text[])) OR metadata_types.category::text = 'end_of_mission'::text AND (metadata_types.attribute_name::text = ANY (ARRAY['time_coverage_end'::character varying, 'end_details'::character varying, 'end_type'::character varying, 'geospatial_lat_end'::character varying, 'geospatial_lon_end'::character varying]::text[])) OR metadata_types.category::text = 'waypoints'::text AND metadata_types.attribute_name::text = 'waypoints_source'::text OR metadata_types.category::text = 'quality'::text AND (metadata_types.attribute_name::text = ANY (ARRAY['found_problem'::character varying, 'person_qc'::character varying]::text[]))) +CREATE MATERIALIZED VIEW mview_vis_metadata AS +WITH dts AS ( + SELECT + data_time_series_1.variable_id, + data_time_series_1.submission_id + FROM data_time_series AS data_time_series_1 + GROUP BY data_time_series_1.variable_id, data_time_series_1.submission_id +) + +SELECT + metadata.submission_id AS source_id, + 'Global Attributes'::text AS attribute_type, + NULL::character varying AS variable, + metadata_types.category, + metadata_types.attribute_name, + left( + right(metadata.attribute_value, length(metadata.attribute_value) - 1), + -1 + ) AS attribute_value +FROM metadata_types +INNER JOIN metadata + ON metadata_types.attribute_id = metadata.attribute_id +WHERE + ( + ( + metadata_types.category::text = 'instrument'::text + AND metadata_types.attribute_name::text IN ( + 'instrument_name', + 'instrument_type', + 'firmware', + 'manufacturer', + 'model', + 'owner_contact', + 'person_owner', + 'serial_number' + ) + ) + OR ( + metadata_types.category::text = 'programming'::text + AND metadata_types.attribute_name::text IN ( + 'programming_report', + 'programming_software' + ) + ) + OR ( + metadata_types.category::text = 'attachment'::text + AND metadata_types.attribute_name::text = 'attachment_method'::text + ) + OR ( + metadata_types.category::text = 'deployment'::text + AND metadata_types.attribute_name::text IN ( + 'geospatial_lat_start', + 'geospatial_lon_start', + 'person_tagger_capture', + 'time_coverage_start' + ) + ) + OR ( + metadata_types.category::text = 'animal'::text + AND metadata_types.attribute_name::text IN ( + 'condition_capture', + 'length_capture', + 'length_method_capture', + 'length_type_capture', + 'length_unit_capture', + 'platform', + 'taxonomic_serial_number' + ) + ) + OR ( + metadata_types.category::text = 'end_of_mission'::text + AND metadata_types.attribute_name::text IN ( + 'time_coverage_end', + 'end_details', + 'end_type', + 'geospatial_lat_end', + 'geospatial_lon_end' + ) + ) + OR ( + metadata_types.category::text = 'waypoints'::text + AND metadata_types.attribute_name::text = 'waypoints_source'::text + ) + OR ( + metadata_types.category::text = 'quality'::text + AND metadata_types.attribute_name::text IN ( + 'found_problem', + 'person_qc' + ) + ) + ) UNION - SELECT data_time_series.submission_id AS source_id, - 'Variable Attributes'::text AS attribute_type, - observation_types.standard_name AS variable, - NULL::character varying AS category, - 'units'::character varying AS attribute_name, - observation_types.variable_units AS attribute_value - FROM observation_types, - ( SELECT data_time_series_1.variable_id, - data_time_series_1.submission_id - FROM data_time_series data_time_series_1 - GROUP BY data_time_series_1.variable_id, data_time_series_1.submission_id) data_time_series - WHERE observation_types.standard_name IS NOT NULL AND observation_types.variable_id = data_time_series.variable_id +SELECT + dts.submission_id AS source_id, + 'Variable Attributes'::text AS attribute_type, + observation_types.standard_name AS variable, + NULL::character varying AS category, + 'units'::character varying AS attribute_name, + observation_types.variable_units AS attribute_value +FROM observation_types +INNER JOIN dts + ON observation_types.variable_id = dts.variable_id +WHERE observation_types.standard_name IS NOT NULL UNION - SELECT data_time_series.submission_id AS source_id, - 'Variable Attributes'::text AS attribute_type, - observation_types.standard_name AS variable, - NULL::character varying AS category, - 'standard_name'::character varying AS attribute_name, - observation_types.standard_name AS attribute_value - FROM observation_types, - ( SELECT data_time_series_1.variable_id, - data_time_series_1.submission_id - FROM data_time_series data_time_series_1 - GROUP BY data_time_series_1.variable_id, data_time_series_1.submission_id) data_time_series - WHERE observation_types.standard_name IS NOT NULL AND observation_types.variable_id = data_time_series.variable_id +SELECT + dts.submission_id AS source_id, + 'Variable Attributes'::text AS attribute_type, + observation_types.standard_name AS variable, + NULL::character varying AS category, + 'standard_name'::character varying AS attribute_name, + observation_types.standard_name AS attribute_value +FROM observation_types +INNER JOIN dts + ON observation_types.variable_id = dts.variable_id +WHERE observation_types.standard_name IS NOT NULL UNION - SELECT data_time_series.submission_id AS source_id, - 'Variable Attributes'::text AS attribute_type, - observation_types.standard_name AS variable, - NULL::character varying AS category, - 'long_name'::character varying AS attribute_name, - observation_types.variable_name AS attribute_value - FROM observation_types, - ( SELECT data_time_series_1.variable_id, - data_time_series_1.submission_id - FROM data_time_series data_time_series_1 - GROUP BY data_time_series_1.variable_id, data_time_series_1.submission_id) data_time_series - WHERE observation_types.standard_name IS NOT NULL AND observation_types.variable_id = data_time_series.variable_id -WITH DATA; \ No newline at end of file +SELECT + dts.submission_id AS source_id, + 'Variable Attributes'::text AS attribute_type, + observation_types.standard_name AS variable, + NULL::character varying AS category, + 'long_name'::character varying AS attribute_name, + observation_types.variable_name AS attribute_value +FROM observation_types +INNER JOIN dts + ON observation_types.variable_id = dts.variable_id +WHERE observation_types.standard_name IS NOT NULL +WITH DATA; diff --git a/tagbase_server/.coveragerc b/tagbase_server/.coveragerc index d99990d3..c7bebb74 100644 --- a/tagbase_server/.coveragerc +++ b/tagbase_server/.coveragerc @@ -1,3 +1,3 @@ [run] -omit = - tagbase_server/test/* \ No newline at end of file +omit = + tagbase_server/test/* diff --git a/tagbase_server/.dockerignore b/tagbase_server/.dockerignore index 1c96c39d..35f1dc94 100644 --- a/tagbase_server/.dockerignore +++ b/tagbase_server/.dockerignore @@ -1,8 +1,5 @@ -.travis.yaml -.openapi-generator-ignore README.md tox.ini -git_push.sh test-requirements.txt setup.py pyproject.toml diff --git a/tagbase_server/.openapi-generator-ignore b/tagbase_server/.openapi-generator-ignore deleted file mode 100644 index d2485c6f..00000000 --- a/tagbase_server/.openapi-generator-ignore +++ /dev/null @@ -1,31 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README. - -Dockerfile -setup.py -tagbase_server/__main__.py -requirements.txt -test-requirements.txt -tox.ini -.dockerignore diff --git a/tagbase_server/.openapi-generator/FILES b/tagbase_server/.openapi-generator/FILES deleted file mode 100644 index 71533959..00000000 --- a/tagbase_server/.openapi-generator/FILES +++ /dev/null @@ -1,24 +0,0 @@ -.gitignore -.travis.yml -README.md -git_push.sh -tagbase_server/__init__.py -tagbase_server/controllers/__init__.py -tagbase_server/controllers/ingest_controller.py -tagbase_server/controllers/security_controller_.py -tagbase_server/controllers/tags_controller.py -tagbase_server/encoder.py -tagbase_server/models/__init__.py -tagbase_server/models/base_model_.py -tagbase_server/models/error.py -tagbase_server/models/error_container.py -tagbase_server/models/ingest200.py -tagbase_server/models/tag.py -tagbase_server/models/tag200.py -tagbase_server/models/tag_put200.py -tagbase_server/models/tag_submission.py -tagbase_server/models/tags200.py -tagbase_server/openapi/openapi.yaml -tagbase_server/test/__init__.py -tagbase_server/typing_utils.py -tagbase_server/util.py diff --git a/tagbase_server/.openapi-generator/VERSION b/tagbase_server/.openapi-generator/VERSION deleted file mode 100644 index 4be2c727..00000000 --- a/tagbase_server/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -6.5.0 \ No newline at end of file diff --git a/tagbase_server/README.md b/tagbase_server/README.md index 7cbcaa68..c6c05e7a 100644 --- a/tagbase_server/README.md +++ b/tagbase_server/README.md @@ -1,37 +1,41 @@ # OpenAPI generated server ## Overview + This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the -[OpenAPI-Spec](https://openapis.org) from a remote server, you can easily generate a server stub. This +[OpenAPI-Spec](https://openapis.org) from a remote server, you can easily generate a server stub. This is an example of building a OpenAPI-enabled Flask server. This example uses the [Connexion](https://github.com/zalando/connexion) library on top of Flask. ## Requirements + Python 3.10–3.14 ## Usage + To run the server, please execute the following from the root directory: -``` +```text pip3 install -r requirements.txt python3 -m tagbase_server ``` and open your browser to here: -``` +```text http://localhost:8080/tagbase/api/v0.14.0/ui/ ``` Your OpenAPI definition lives here: -``` +```text http://localhost:8080/tagbase/api/v0.14.0/openapi.json ``` To launch the integration tests, use tox: -``` + +```text sudo pip install tox tox ``` @@ -46,4 +50,4 @@ docker build -t tagbase_server . # starting up a container docker run -p 8080:8080 tagbase_server -``` \ No newline at end of file +``` diff --git a/tagbase_server/git_push.sh b/tagbase_server/git_push.sh deleted file mode 100644 index f53a75d4..00000000 --- a/tagbase_server/git_push.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh -# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ -# -# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" - -git_user_id=$1 -git_repo_id=$2 -release_note=$3 -git_host=$4 - -if [ "$git_host" = "" ]; then - git_host="github.com" - echo "[INFO] No command line input provided. Set \$git_host to $git_host" -fi - -if [ "$git_user_id" = "" ]; then - git_user_id="GIT_USER_ID" - echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" -fi - -if [ "$git_repo_id" = "" ]; then - git_repo_id="GIT_REPO_ID" - echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" -fi - -if [ "$release_note" = "" ]; then - release_note="Minor update" - echo "[INFO] No command line input provided. Set \$release_note to $release_note" -fi - -# Initialize the local directory as a Git repository -git init - -# Adds the files in the local repository and stages them for commit. -git add . - -# Commits the tracked changes and prepares them to be pushed to a remote repository. -git commit -m "$release_note" - -# Sets the new remote -git_remote=$(git remote) -if [ "$git_remote" = "" ]; then # git remote not defined - - if [ "$GIT_TOKEN" = "" ]; then - echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." - git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git - else - git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git - fi - -fi - -git pull origin master - -# Pushes (Forces) the changes in the local repository up to the remote repository -echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" -git push origin master 2>&1 | grep -v 'To https' diff --git a/tagbase_server/tagbase_server/controllers/ingest_controller.py b/tagbase_server/tagbase_server/controllers/ingest_controller.py index f29e5e61..4d0e8fb4 100644 --- a/tagbase_server/tagbase_server/controllers/ingest_controller.py +++ b/tagbase_server/tagbase_server/controllers/ingest_controller.py @@ -1,8 +1,9 @@ import logging -from multiprocessing import cpu_count import os import time from functools import partial +from multiprocessing import cpu_count + import parmap from tqdm import tqdm as std_tqdm from tqdm.contrib.slack import tqdm as slack_tqdm @@ -13,8 +14,8 @@ from tagbase_server.utils.io_utils import ( process_get_input_data, process_post_input_data, + unpack_compressed_binary, ) -from tagbase_server.utils.io_utils import unpack_compressed_binary from tagbase_server.utils.processing_utils import process_etuff_file logger = logging.getLogger(__name__) @@ -26,8 +27,7 @@ def _resolve_ingest_file_type(type): ingest_file_type = type if type is not None else SUPPORTED_INGEST_FILE_TYPE if ingest_file_type != SUPPORTED_INGEST_FILE_TYPE: raise TagbaseClientError( - f"Unsupported ingest file type '{ingest_file_type}'; " - f"only '{SUPPORTED_INGEST_FILE_TYPE}' is supported." + f"Unsupported ingest file type '{ingest_file_type}'; only '{SUPPORTED_INGEST_FILE_TYPE}' is supported." ) return ingest_file_type diff --git a/tagbase_server/tagbase_server/controllers/security_controller_.py b/tagbase_server/tagbase_server/controllers/security_controller_.py deleted file mode 100644 index 7d686cba..00000000 --- a/tagbase_server/tagbase_server/controllers/security_controller_.py +++ /dev/null @@ -1 +0,0 @@ -from typing import List diff --git a/tagbase_server/tagbase_server/controllers/tags_controller.py b/tagbase_server/tagbase_server/controllers/tags_controller.py index 54dcd72f..36340d9e 100644 --- a/tagbase_server/tagbase_server/controllers/tags_controller.py +++ b/tagbase_server/tagbase_server/controllers/tags_controller.py @@ -1,10 +1,9 @@ -from tagbase_server.utils.db_utils import connect +import logging from tagbase_server.models.tag200 import Tag200 # noqa: E501 from tagbase_server.models.tag_put200 import TagPut200 # noqa: E501 from tagbase_server.problem import as_json - -import logging +from tagbase_server.utils.db_utils import connect logger = logging.getLogger(__name__) diff --git a/tagbase_server/tagbase_server/models/__init__.py b/tagbase_server/tagbase_server/models/__init__.py index e2fa364b..65a21d02 100644 --- a/tagbase_server/tagbase_server/models/__init__.py +++ b/tagbase_server/tagbase_server/models/__init__.py @@ -3,9 +3,10 @@ # flake8: noqa from __future__ import absolute_import +from tagbase_server.models.ingest200 import Ingest200 + # import models into model package from tagbase_server.models.problem import Problem -from tagbase_server.models.ingest200 import Ingest200 from tagbase_server.models.tag200 import Tag200 from tagbase_server.models.tag200_collection import Tag200Collection from tagbase_server.models.tag200_summary import Tag200Summary diff --git a/tagbase_server/tagbase_server/models/base_model_.py b/tagbase_server/tagbase_server/models/base_model_.py index 8c0e9a20..a6b67fa9 100644 --- a/tagbase_server/tagbase_server/models/base_model_.py +++ b/tagbase_server/tagbase_server/models/base_model_.py @@ -1,5 +1,4 @@ import pprint - import typing from tagbase_server import util diff --git a/tagbase_server/tagbase_server/models/ingest200.py b/tagbase_server/tagbase_server/models/ingest200.py index 3d220a72..369fb75f 100644 --- a/tagbase_server/tagbase_server/models/ingest200.py +++ b/tagbase_server/tagbase_server/models/ingest200.py @@ -1,12 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class Ingest200(Model): diff --git a/tagbase_server/tagbase_server/models/problem.py b/tagbase_server/tagbase_server/models/problem.py index b99275fd..0e8e976c 100644 --- a/tagbase_server/tagbase_server/models/problem.py +++ b/tagbase_server/tagbase_server/models/problem.py @@ -2,8 +2,8 @@ from __future__ import absolute_import -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class Problem(Model): diff --git a/tagbase_server/tagbase_server/models/response200.py b/tagbase_server/tagbase_server/models/response200.py index 75df9259..659b2188 100644 --- a/tagbase_server/tagbase_server/models/response200.py +++ b/tagbase_server/tagbase_server/models/response200.py @@ -1,12 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class Response200(Model): diff --git a/tagbase_server/tagbase_server/models/response500.py b/tagbase_server/tagbase_server/models/response500.py index c2ff82c5..6c534b6d 100644 --- a/tagbase_server/tagbase_server/models/response500.py +++ b/tagbase_server/tagbase_server/models/response500.py @@ -1,12 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class Response500(Model): diff --git a/tagbase_server/tagbase_server/models/tag200.py b/tagbase_server/tagbase_server/models/tag200.py index c334f93e..cdb00e61 100644 --- a/tagbase_server/tagbase_server/models/tag200.py +++ b/tagbase_server/tagbase_server/models/tag200.py @@ -1,14 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model -from tagbase_server.models.tag_submission import TagSubmission from tagbase_server import util - +from tagbase_server.models.base_model_ import Model from tagbase_server.models.tag_submission import TagSubmission # noqa: E501 diff --git a/tagbase_server/tagbase_server/models/tag200_collection.py b/tagbase_server/tagbase_server/models/tag200_collection.py index 8a6c19c7..d44b5579 100644 --- a/tagbase_server/tagbase_server/models/tag200_collection.py +++ b/tagbase_server/tagbase_server/models/tag200_collection.py @@ -1,14 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model -from tagbase_server.models.tag200_summary import Tag200Summary from tagbase_server import util - +from tagbase_server.models.base_model_ import Model from tagbase_server.models.tag200_summary import Tag200Summary # noqa: E501 diff --git a/tagbase_server/tagbase_server/models/tag200_summary.py b/tagbase_server/tagbase_server/models/tag200_summary.py index 695be935..09261d3e 100644 --- a/tagbase_server/tagbase_server/models/tag200_summary.py +++ b/tagbase_server/tagbase_server/models/tag200_summary.py @@ -1,12 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class Tag200Summary(Model): diff --git a/tagbase_server/tagbase_server/models/tag200_tag.py b/tagbase_server/tagbase_server/models/tag200_tag.py index 8623ea5f..d281d46d 100644 --- a/tagbase_server/tagbase_server/models/tag200_tag.py +++ b/tagbase_server/tagbase_server/models/tag200_tag.py @@ -1,12 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class Tag200Tag(Model): diff --git a/tagbase_server/tagbase_server/models/tag200_tag_inner.py b/tagbase_server/tagbase_server/models/tag200_tag_inner.py index a46eae95..1178d3e1 100644 --- a/tagbase_server/tagbase_server/models/tag200_tag_inner.py +++ b/tagbase_server/tagbase_server/models/tag200_tag_inner.py @@ -1,12 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class Tag200TagInner(Model): diff --git a/tagbase_server/tagbase_server/models/tag_put200.py b/tagbase_server/tagbase_server/models/tag_put200.py index 1f604cde..dfec61fe 100644 --- a/tagbase_server/tagbase_server/models/tag_put200.py +++ b/tagbase_server/tagbase_server/models/tag_put200.py @@ -1,12 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class TagPut200(Model): diff --git a/tagbase_server/tagbase_server/models/tag_submission.py b/tagbase_server/tagbase_server/models/tag_submission.py index 44aa07cc..d50b6b56 100644 --- a/tagbase_server/tagbase_server/models/tag_submission.py +++ b/tagbase_server/tagbase_server/models/tag_submission.py @@ -1,12 +1,12 @@ # coding: utf-8 from __future__ import absolute_import -from datetime import date, datetime # noqa: F401 -from typing import List, Dict # noqa: F401 +from datetime import date, datetime # noqa: F401 +from typing import Dict, List # noqa: F401 -from tagbase_server.models.base_model_ import Model from tagbase_server import util +from tagbase_server.models.base_model_ import Model class TagSubmission(Model): diff --git a/tagbase_server/tagbase_server/openapi/openapi.yaml b/tagbase_server/tagbase_server/openapi/openapi.yaml index 405b6374..45277469 100644 --- a/tagbase_server/tagbase_server/openapi/openapi.yaml +++ b/tagbase_server/tagbase_server/openapi/openapi.yaml @@ -15,59 +15,59 @@ info: title: tagbase-server API version: v0.14.0 servers: -- description: Local development tagbase-server - url: https://localhost/tagbase/api/v0.14.0 -- description: ICCAT Test tagbase-server - url: https://162.13.162.49/tagbase/api/v0.14.0 + - description: Local development tagbase-server + url: https://localhost/tagbase/api/v0.14.0 + - description: ICCAT Test tagbase-server + url: https://162.13.162.49/tagbase/api/v0.14.0 tags: -- description: Ingestion operations. - name: ingest -- description: Tag Operations. - name: tags + - description: Ingestion operations. + name: ingest + - description: Tag Operations. + name: tags paths: /ingest: get: description: Get network accessible file and execute ingestion operationId: ingest_get parameters: - - description: Location of a network accessible (file, ftp, http, https) - file e.g. 'file:///usr/src/app/data/eTUFF-sailfish-117259.txt'. - explode: true - in: query - name: file - required: true - schema: - maxLength: 100 - minLength: 10 - pattern: ^(?!\s*$).+$ - type: string - style: form - - $ref: '#/components/parameters/notes' - - $ref: '#/components/parameters/type' - - $ref: '#/components/parameters/version' + - description: Location of a network accessible (file, ftp, http, https) + file e.g. 'file:///usr/src/app/data/eTUFF-sailfish-117259.txt'. + explode: true + in: query + name: file + required: true + schema: + maxLength: 100 + minLength: 10 + pattern: ^(?!\s*$).+$ + type: string + style: form + - $ref: "#/components/parameters/notes" + - $ref: "#/components/parameters/type" + - $ref: "#/components/parameters/version" responses: "200": content: application/json: schema: - $ref: '#/components/schemas/Ingest200' + $ref: "#/components/schemas/Ingest200" description: A success message confirming ingestion. "400": - $ref: '#/components/responses/BadRequest' + $ref: "#/components/responses/BadRequest" "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Get network accessible file and execute ingestion tags: - - ingest + - ingest x-openapi-router-controller: tagbase_server.controllers.ingest_controller post: description: Post a local file and perform a ingest operation operationId: ingest_post parameters: - - $ref: '#/components/parameters/filename' - - $ref: '#/components/parameters/notes' - - $ref: '#/components/parameters/type' - - $ref: '#/components/parameters/version' + - $ref: "#/components/parameters/filename" + - $ref: "#/components/parameters/notes" + - $ref: "#/components/parameters/type" + - $ref: "#/components/parameters/version" requestBody: content: application/octet-stream: @@ -91,15 +91,15 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Ingest200' + $ref: "#/components/schemas/Ingest200" description: A success message confirming ingestion. "400": - $ref: '#/components/responses/BadRequest' + $ref: "#/components/responses/BadRequest" "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Post a local file and perform a ingest operation tags: - - ingest + - ingest x-codegen-request-body-name: body x-openapi-router-controller: tagbase_server.controllers.ingest_controller /tags: @@ -110,10 +110,10 @@ paths: "204": description: Successful delete operation. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Delete all tags tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller get: description: Get information about all tags @@ -123,17 +123,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Tag200Collection' + $ref: "#/components/schemas/Tag200Collection" description: A success message confirming ingestion. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Get information about all tags tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller /tags/{tag_id}: parameters: - - $ref: '#/components/parameters/tagId' + - $ref: "#/components/parameters/tagId" delete: description: Delete an individual tag operationId: delete_tag @@ -141,10 +141,10 @@ paths: "204": description: Successful delete operation. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Delete an individual tag tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller get: description: Get information about an individual tag @@ -154,18 +154,18 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Tag200' + $ref: "#/components/schemas/Tag200" description: A success message confirming ingestion. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Get information about an individual tag tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller /tags/{tag_id}/subs/{sub_id}: parameters: - - $ref: '#/components/parameters/subId' - - $ref: '#/components/parameters/tagId' + - $ref: "#/components/parameters/subId" + - $ref: "#/components/parameters/tagId" delete: description: Delete a tag submission operationId: delete_tag_sub @@ -173,29 +173,29 @@ paths: "204": description: Successful delete operation. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Delete a tag submission tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller put: description: Update a tag submission operationId: replace_tag_sub parameters: - - $ref: '#/components/parameters/notes' - - $ref: '#/components/parameters/version' + - $ref: "#/components/parameters/notes" + - $ref: "#/components/parameters/version" responses: "200": content: application/json: schema: - $ref: '#/components/schemas/TagPut200' + $ref: "#/components/schemas/TagPut200" description: A success message confirming ingestion. "500": - $ref: '#/components/responses/InternalError' + $ref: "#/components/responses/InternalError" summary: Update the 'notes' and/or 'version' associated with a tag submission tags: - - tags + - tags x-openapi-router-controller: tagbase_server.controllers.tags_controller components: responses: @@ -204,13 +204,13 @@ components: content: application/problem+json: schema: - $ref: '#/components/schemas/Problem' + $ref: "#/components/schemas/Problem" InternalError: description: Unexpected server error. Contact admin detailed in openapi.yaml. content: application/problem+json: schema: - $ref: '#/components/schemas/Problem' + $ref: "#/components/schemas/Problem" parameters: filename: description: Free-form text field to explicitly define the name of the file to be persisted @@ -357,8 +357,9 @@ components: description: HTTP 200 success response example: code: "200" - elapsed: '0:00:06.506691' - message: Data file eTUFF-sailfish-117259.txt successfully ingested into Tagbase + elapsed: "0:00:06.506691" + message: + Data file eTUFF-sailfish-117259.txt successfully ingested into Tagbase DB. properties: code: @@ -376,7 +377,8 @@ components: pattern: ^(?!\s*$).+$ message: description: A string detailing specifics of an HTTP operation - example: Data file eTUFF-sailfish-117259.txt successfully ingested into + example: + Data file eTUFF-sailfish-117259.txt successfully ingested into Tagbase DB. type: string minLength: 1 @@ -404,45 +406,45 @@ components: - tag example: tag_id: 3 - filename: 'eTUFF-sailfish-117259_2.txt' + filename: "eTUFF-sailfish-117259_2.txt" tag: - - dataset_id: 1 - date_time: '2022-04-01T04:58:21.319061+00:00' - filename: 'eTUFF-sailfish-117259_2.txt' - hash_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b977' - metadata: - person_owner: John Do - owner_contect: john@do.net - manufacturer: telemetry inc. - model: new_gen - attachment_method: anchor - notes: 'Ingested by admin on 2022-06-01 for Sailfish tagging campaign.' - submission_id: 5 - tag_id: 3 - version: '1' - - dataset_id: 1 - date_time: '2022-06-01T05:39:46.896088+00:00' - filename: 'eTUFF-sailfish-117259_2.txt' - hash_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' - metadata: - person_owner: Jane Do - owner_contect: jane@do.net - manufacturer: telemetry inc. - model: newer_gen - attachment_method: anchor - notes: 'Ingested by admin on 2022-06-01 for version 2 of the Sailfish tagging campaign.' - submission_id: 6 - tag_id: 3 - version: '2' + - dataset_id: 1 + date_time: "2022-04-01T04:58:21.319061+00:00" + filename: "eTUFF-sailfish-117259_2.txt" + hash_sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b977" + metadata: + person_owner: John Do + owner_contect: john@do.net + manufacturer: telemetry inc. + model: new_gen + attachment_method: anchor + notes: "Ingested by admin on 2022-06-01 for Sailfish tagging campaign." + submission_id: 5 + tag_id: 3 + version: "1" + - dataset_id: 1 + date_time: "2022-06-01T05:39:46.896088+00:00" + filename: "eTUFF-sailfish-117259_2.txt" + hash_sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + metadata: + person_owner: Jane Do + owner_contect: jane@do.net + manufacturer: telemetry inc. + model: newer_gen + attachment_method: anchor + notes: "Ingested by admin on 2022-06-01 for version 2 of the Sailfish tagging campaign." + submission_id: 6 + tag_id: 3 + version: "2" properties: tag_id: - $ref: '#/components/schemas/TagId' + $ref: "#/components/schemas/TagId" filename: - $ref: '#/components/schemas/Filename' + $ref: "#/components/schemas/Filename" tag: description: List containing one or more submissions for a given tag items: - $ref: '#/components/schemas/TagSubmission' + $ref: "#/components/schemas/TagSubmission" minItems: 1 maxItems: 100 type: array @@ -477,9 +479,9 @@ components: filename: eTUFF-sailfish-117259_2.txt properties: tag_id: - $ref: '#/components/schemas/TagId' + $ref: "#/components/schemas/TagId" filename: - $ref: '#/components/schemas/Filename' + $ref: "#/components/schemas/Filename" type: object TagSubmission: type: object @@ -493,22 +495,22 @@ components: maximum: 2147483647 date_time: description: Local datetime stamp at the time of eTUFF tag data file ingestion - example: '2022-04-01T04:58:21.319061+00:00' + example: "2022-04-01T04:58:21.319061+00:00" format: date-time type: string minLength: 20 maxLength: 64 filename: - $ref: '#/components/schemas/Filename' + $ref: "#/components/schemas/Filename" hash_sha256: description: SHA256 hash representing the contents of the submission eTUFF file - example: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + example: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" type: string minLength: 64 maxLength: 64 pattern: ^[0-9a-fA-F]{64}$ metadata: - $ref: '#/components/schemas/Metadata' + $ref: "#/components/schemas/Metadata" notes: description: Free-form text field where details of submitted eTUFF file for ingest can be provided e.g. submitter name, etuff data contents (tag metadata and measurements + primary position data, or just secondary solutionpositional meta/data) example: "Ingested by admin on XXXX-XX-XX to back-process campaign XYZ." @@ -524,7 +526,7 @@ components: minimum: 1 maximum: 2147483647 tag_id: - $ref: '#/components/schemas/TagId' + $ref: "#/components/schemas/TagId" version: description: Version identifier for the eTUFF tag data file ingested example: "1" @@ -539,10 +541,10 @@ components: example: count: 2 tags: - - tag_id: 1 - filename: eTUFF-sailfish-117259_2.txt - - tag_id: 2 - filename: eTUFF-sailfish-117259.txt + - tag_id: 1 + filename: eTUFF-sailfish-117259_2.txt + - tag_id: 2 + filename: eTUFF-sailfish-117259.txt properties: count: description: Total count of unique tags @@ -555,7 +557,7 @@ components: description: List of unique numeric Tag IDs and associated filename type: array items: - $ref: '#/components/schemas/Tag200Summary' + $ref: "#/components/schemas/Tag200Summary" minItems: 1 maxItems: 100000 title: Tag200Collection diff --git a/tagbase_server/tagbase_server/problem.py b/tagbase_server/tagbase_server/problem.py index 93e6adc5..8b058fc4 100644 --- a/tagbase_server/tagbase_server/problem.py +++ b/tagbase_server/tagbase_server/problem.py @@ -134,8 +134,7 @@ def _handle_unexpected(exc): status=500, title="Internal Server Error", detail=( - "An unexpected error occurred while processing the request. " - f"Reference trace_id={trace_id}." + f"An unexpected error occurred while processing the request. Reference trace_id={trace_id}." ), type_=TYPE_INTERNAL, trace_id=trace_id, diff --git a/tagbase_server/tagbase_server/test/integration/test_processing_utils_db.py b/tagbase_server/tagbase_server/test/integration/test_processing_utils_db.py index 9e461201..3a178c82 100644 --- a/tagbase_server/tagbase_server/test/integration/test_processing_utils_db.py +++ b/tagbase_server/tagbase_server/test/integration/test_processing_utils_db.py @@ -137,8 +137,7 @@ def test_is_only_metadata_change_and_update(clean_db): metadata = [(str(submission_id), "1", '"new-value"')] # attribute_id 1 must exist from seed cur.execute( - "INSERT INTO metadata (submission_id, attribute_id, attribute_value, tag_id) " - "VALUES (%s, %s, %s, %s)", + "INSERT INTO metadata (submission_id, attribute_id, attribute_value, tag_id) VALUES (%s, %s, %s, %s)", (submission_id, 1, "old-value", tag_id), ) pu.update_submission_metadata( diff --git a/tagbase_server/tagbase_server/test/stack/test_fswatch_contract.py b/tagbase_server/tagbase_server/test/stack/test_fswatch_contract.py index 07f57f72..ce970be4 100644 --- a/tagbase_server/tagbase_server/test/stack/test_fswatch_contract.py +++ b/tagbase_server/tagbase_server/test/stack/test_fswatch_contract.py @@ -274,8 +274,7 @@ def test_fswatch_ingests_multiple_files_dropped_quickly(mock_ingest_server): else: got = [(r["query"].get("filename") or [None])[0] for r in handler.requests] pytest.fail( - f"expected POSTs for {names}, got {got}. " - f"post.sh output:\n{_read_log(log_path)}" + f"expected POSTs for {names}, got {got}. post.sh output:\n{_read_log(log_path)}" ) by_name = { (r["query"].get("filename") or [None])[0]: r["body"] diff --git a/tagbase_server/tagbase_server/test/stack/test_fswatch_smoke.py b/tagbase_server/tagbase_server/test/stack/test_fswatch_smoke.py index 875c940d..57a1eff8 100644 --- a/tagbase_server/tagbase_server/test/stack/test_fswatch_smoke.py +++ b/tagbase_server/tagbase_server/test/stack/test_fswatch_smoke.py @@ -43,8 +43,7 @@ def _stack_ready(): def require_full_stack(): if not _stack_ready(): pytest.skip( - "full stack not ready; " - "docker compose -f docker-compose.test.yml --profile stack up -d --build" + "full stack not ready; docker compose -f docker-compose.test.yml --profile stack up -d --build" ) diff --git a/tagbase_server/tagbase_server/test/stack/test_nginx_auth.py b/tagbase_server/tagbase_server/test/stack/test_nginx_auth.py index aed348fc..19b4a1a1 100644 --- a/tagbase_server/tagbase_server/test/stack/test_nginx_auth.py +++ b/tagbase_server/tagbase_server/test/stack/test_nginx_auth.py @@ -92,8 +92,7 @@ def test_grafana_health_with_basic_auth_does_not_rechallenge(): response = client.get(_base() + "/grafana/api/health", auth=_auth()) if response.status_code in (502, 503, 504): pytest.skip("Grafana not reachable; start with --profile observability") - assert response.status_code == 200, ( - f"expected 200 from Grafana health, got {response.status_code}: " - f"{response.text[:200]}" - ) + assert ( + response.status_code == 200 + ), f"expected 200 from Grafana health, got {response.status_code}: {response.text[:200]}" assert "invalid username or password" not in response.text.lower() diff --git a/tagbase_server/tagbase_server/test/test_ingest.py b/tagbase_server/tagbase_server/test/test_ingest.py index 16a21e9e..0eb86618 100644 --- a/tagbase_server/tagbase_server/test/test_ingest.py +++ b/tagbase_server/tagbase_server/test/test_ingest.py @@ -4,8 +4,9 @@ from unittest import mock import psycopg2 -import tagbase_server.utils.processing_utils as pu + import tagbase_server.utils.io_utils as io_utils +import tagbase_server.utils.processing_utils as pu class TestIngest(unittest.TestCase): @@ -83,9 +84,10 @@ def test_insert_metadata_continues_after_database_error(self): None, ] - with mock.patch.object(pu.logger, "error") as mock_error, mock.patch.object( - pu, "record_db_error" - ) as mock_record_db_error: + with ( + mock.patch.object(pu.logger, "error") as mock_error, + mock.patch.object(pu, "record_db_error") as mock_record_db_error, + ): pu.insert_metadata( cur, [ @@ -107,9 +109,10 @@ def test_update_metadata_continues_after_database_error(self): None, # second metadata update ] - with mock.patch.object(pu.logger, "error") as mock_error, mock.patch.object( - pu, "record_db_error" - ) as mock_record_db_error: + with ( + mock.patch.object(pu.logger, "error") as mock_error, + mock.patch.object(pu, "record_db_error") as mock_record_db_error, + ): pu.update_submission_metadata( cur, tag_id=10, diff --git a/tagbase_server/tagbase_server/test/test_slack_and_schema.py b/tagbase_server/tagbase_server/test/test_slack_and_schema.py index 5f5b88c2..dd90adb2 100644 --- a/tagbase_server/tagbase_server/test/test_slack_and_schema.py +++ b/tagbase_server/tagbase_server/test/test_slack_and_schema.py @@ -67,11 +67,12 @@ def test_connect_reraises_when_schema_missing(monkeypatch): fake_conn.cursor.return_value.__exit__ = mock.Mock(return_value=False) monkeypatch.setattr(db_utils.psycopg2, "connect", mock.Mock(return_value=fake_conn)) - monkeypatch.setattr(db_utils, "record_db_error", mock.Mock()) + record_db_error = mock.Mock() + monkeypatch.setattr(db_utils, "record_db_error", record_db_error) with pytest.raises(RuntimeError, match="submission"): db_utils.connect() - db_utils.record_db_error.assert_called_with("schema") + record_db_error.assert_called_with("schema") def test_assert_schema_ready_raises_when_missing(monkeypatch): diff --git a/tagbase_server/tagbase_server/test/test_sonar_cleanup.py b/tagbase_server/tagbase_server/test/test_sonar_cleanup.py index c7f0fdb5..2cbc38d5 100644 --- a/tagbase_server/tagbase_server/test/test_sonar_cleanup.py +++ b/tagbase_server/tagbase_server/test/test_sonar_cleanup.py @@ -14,9 +14,8 @@ from tagbase_server.controllers.ingest_controller import _resolve_ingest_file_type from tagbase_server.models.base_model_ import Model from tagbase_server.models.ingest200 import Ingest200 -from tagbase_server.utils import io_utils +from tagbase_server.utils import io_utils, slack_utils from tagbase_server.utils import processing_utils as pu -from tagbase_server.utils import slack_utils def test_parse_cors_origins_empty_and_list(): diff --git a/tagbase_server/tagbase_server/test/test_telemetry.py b/tagbase_server/tagbase_server/test/test_telemetry.py index 29d481fc..5996f616 100644 --- a/tagbase_server/tagbase_server/test/test_telemetry.py +++ b/tagbase_server/tagbase_server/test/test_telemetry.py @@ -75,18 +75,19 @@ def _stub_setup_dependencies(monkeypatch): "Psycopg2Instrumentor", mock.Mock(return_value=mock.Mock()), ) - monkeypatch.setattr(telemetry, "_ensure_instruments", mock.Mock()) - return otel_handler + ensure_instruments = mock.Mock() + monkeypatch.setattr(telemetry, "_ensure_instruments", ensure_instruments) + return otel_handler, ensure_instruments def test_setup_telemetry_initializes_and_is_idempotent(monkeypatch): monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") monkeypatch.setenv("OTEL_SERVICE_NAME", " ") # blank → default SERVICE_NAME - _stub_setup_dependencies(monkeypatch) + _, ensure_instruments = _stub_setup_dependencies(monkeypatch) assert telemetry.setup_telemetry() is True assert telemetry.setup_telemetry() is True # already initialized - telemetry._ensure_instruments.assert_called_once() + ensure_instruments.assert_called_once() def test_setup_telemetry_instruments_flask_app(monkeypatch): diff --git a/tagbase_server/tagbase_server/util.py b/tagbase_server/tagbase_server/util.py index 533429eb..5a5d76b6 100644 --- a/tagbase_server/tagbase_server/util.py +++ b/tagbase_server/tagbase_server/util.py @@ -1,6 +1,6 @@ import datetime - import typing + from tagbase_server import typing_utils diff --git a/tagbase_server/tagbase_server/utils/db_utils.py b/tagbase_server/tagbase_server/utils/db_utils.py index db1040cb..d3cabe97 100644 --- a/tagbase_server/tagbase_server/utils/db_utils.py +++ b/tagbase_server/tagbase_server/utils/db_utils.py @@ -1,5 +1,6 @@ import logging import os + import psycopg2 from tagbase_server.models.response500 import Response500 # noqa: E501 diff --git a/tagbase_server/tagbase_server/utils/io_utils.py b/tagbase_server/tagbase_server/utils/io_utils.py index b2eba09f..fc752fc6 100644 --- a/tagbase_server/tagbase_server/utils/io_utils.py +++ b/tagbase_server/tagbase_server/utils/io_utils.py @@ -4,9 +4,10 @@ import re import tempfile from time import time -from pyunpack import Archive from urllib.request import urlopen +from pyunpack import Archive + logger = logging.getLogger(__name__) TEMP_DIR = tempfile.gettempdir() diff --git a/tagbase_server/tagbase_server/utils/processing_utils.py b/tagbase_server/tagbase_server/utils/processing_utils.py index f63c9d12..c8e1a03a 100644 --- a/tagbase_server/tagbase_server/utils/processing_utils.py +++ b/tagbase_server/tagbase_server/utils/processing_utils.py @@ -1,8 +1,8 @@ import logging +import time from datetime import datetime as dt from datetime import timezone from io import StringIO -import time import pandas as pd import psycopg2.extras @@ -41,9 +41,8 @@ def process_global_attributes_metadata( attributes_names = ", ".join( ["'{}'".format(attrib_name) for attrib_name in attributes_map.keys()] ) - attribute_ids_query = ( - "SELECT attribute_id, attribute_name FROM metadata_types " - "WHERE attribute_name IN ({})".format(attributes_names) + attribute_ids_query = "SELECT attribute_id, attribute_name FROM metadata_types WHERE attribute_name IN ({})".format( + attributes_names ) logger.debug("Query=%s", attribute_ids_query) cur.execute(attribute_ids_query) @@ -91,7 +90,7 @@ def get_tag_id(cur, dataset_id): def get_dataset_id(cur, instrument_name, serial_number, ptt, platform): """ - Retreive or create a dataset entry for a submission. If a dataset entry exists then grab the existing + Retrieve or create a dataset entry for a submission. If a dataset entry exists then grab the existing id, if not, create a new one. :param cur: A database cursor @@ -131,9 +130,9 @@ def get_dataset_id(cur, instrument_name, serial_number, ptt, platform): def get_submission_id(cur, tag_id, dataset_id, data_sha256): cur.execute( - "SELECT submission_id FROM submission" - " WHERE tag_id = '{}' AND dataset_id = '{}'" - " AND data_sha256 = '{}'".format(tag_id, dataset_id, data_sha256) + "SELECT submission_id FROM submission WHERE tag_id = '{}' AND dataset_id = '{}' AND data_sha256 = '{}'".format( + tag_id, dataset_id, data_sha256 + ) ) db_results = cur.fetchone() if not db_results: @@ -435,10 +434,7 @@ def _build_proc_observations( ) else: stripped_line = line.strip("\n") - msg = ( - f"*{submission_filename}* _line:{line_number}_ - " - f"No datetime... skipping line: {stripped_line}" - ) + msg = f"*{submission_filename}* _line:{line_number}_ - No datetime... skipping line: {stripped_line}" post_msg(msg) continue proc_obs.append( diff --git a/tagbase_server/test-requirements.txt b/tagbase_server/test-requirements.txt index db116842..5753bb9e 100644 --- a/tagbase_server/test-requirements.txt +++ b/tagbase_server/test-requirements.txt @@ -2,4 +2,3 @@ pytest~=9.1.1 pytest-cov>=2.8.1 pytest-randomly==4.1.0 httpx>=0.27.0 -diff-cover==10.4.1 diff --git a/trivy.yaml b/trivy.yaml new file mode 100644 index 00000000..cbfcaf9f --- /dev/null +++ b/trivy.yaml @@ -0,0 +1,12 @@ +# Trivy config for Super Linter — skip intentional local TLS fixtures. +scan: + skip-files: + - "**/ssl/key.pem" + - "**/ssl/cert.pem" + - "**/fixtures/nginx/key.pem" + - "**/fixtures/nginx/cert.pem" + - "**/staging_data/**" + - "**/staging_data.bk/**" +severity: + exit-code: 1 + ignore-unfixed: true