diff --git a/.coverage b/.coverage
deleted file mode 100644
index 94f7fdc..0000000
Binary files a/.coverage and /dev/null differ
diff --git a/.gitignore b/.gitignore
index 139db12..39614ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,10 @@
*.egg-info
**/__pycache__
-.artifacts_cache.json
-.artifacts_index.json
.envrc
.venv
_build
dist/
+glrd/artifacts-cache.json
shell.nix
releases.json
releases.yaml
diff --git a/Containerfile b/Containerfile
index 53114b2..3484c62 100644
--- a/Containerfile
+++ b/Containerfile
@@ -12,18 +12,10 @@ ENV POETRY_HOME=/opt/poetry
ENV DEBIAN_FRONTEND noninteractive
ENV PATH=${POETRY_HOME}/bin:/app/.venv/bin:$PATH
-# install runtime dependencies and GitHub CLI
+# install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
${PYTHON}-venv \
- git \
- curl \
- && mkdir -p -m 755 /etc/apt/keyrings \
- && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
- && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
- && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
- && apt-get update \
- && apt-get install -y --no-install-recommends gh \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Install poetry
diff --git a/docs/explanation/.gitkeep b/docs/explanation/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/docs/explanation/release-lifecycle.md b/docs/explanation/release-lifecycle.md
new file mode 100644
index 0000000..768c180
--- /dev/null
+++ b/docs/explanation/release-lifecycle.md
@@ -0,0 +1,95 @@
+---
+title: "GLRD Release Lifecycle"
+description: "Explains lifecycle dependencies between major and minor releases, default date calculations, nightly and dev date rules, and the GLRD versioning scheme."
+github_org: gardenlinux
+github_repo: glrd
+github_source_path: docs/explanation/release-lifecycle.md
+github_target_path: docs/explanation/glrd-release-lifecycle.md
+related_topics:
+ - /reference/supporting_tools/glrd/
+ - /reference/supporting_tools/glrd/cli.md
+ - /reference/supporting_tools/glrd/schema.md
+ - /how-to/releases/glrd/
+ - /how-to/releases/glrd/run-glrd.md
+ - /how-to/releases/glrd/query-releases.md
+ - /how-to/releases/glrd/manage-releases.md
+ - /explanation/glrd-release-lifecycle.md
+---
+
+# GLRD Release Lifecycle
+
+This page explains the lifecycle model used by the Garden Linux Release Database (GLRD): why lifecycle phases are structured the way they are, how dates are calculated by default, and how the versioning scheme works.
+
+## Lifecycle phases
+
+Every release in GLRD has a `lifecycle` object. The phases present depend on the release type:
+
+| Phase | Present for types | Meaning |
+| ---------- | ------------------------ | ------------------------------------------------------------------ |
+| `released` | all | The date the release was published. |
+| `extended` | `major`, `next` | The date extended maintenance begins (after standard maintenance). |
+| `eol` | `major`, `next`, `minor` | The date support ends entirely. |
+
+`nightly` and `dev` releases do not have `extended` or `eol` phases because they are not supported beyond their immediate build date.
+
+## Default major dates
+
+When you create a major release without explicitly setting `extended` or `eol` dates, GLRD calculates defaults based on the [Garden Linux Release Plan](/reference/releases/release-lifecycle.md):
+
+- **`extended`** = `released` date + 6 months
+- **`eol`** = `released` date + 9 months
+
+**Example — `major-1443`:**
+
+| Phase | Date |
+| ---------- | ----------------------- |
+| `released` | 2024-03-13 |
+| `extended` | 2024-09-13 (+ 6 months) |
+| `eol` | 2025-01-13 (+ 9 months) |
+
+You can always override the calculated defaults by passing `--lifecycle-extended-isodatetime` and `--lifecycle-eol-isodatetime` to `glrd-manage --create`.
+
+## Major and minor release dependencies
+
+There is a dependency between the `lifecycle.eol` fields of major and minor releases within the same major version:
+
+- **Intermediate minor releases**: The `eol` of each minor release is set to the `released` date of the next minor release in the same major series. This means support for a given minor ends when the next patch is available.
+- **Latest minor release**: The `eol` of the most recent minor release always matches the `eol` of the parent major release. This ensures the final patch remains supported until the major version reaches end-of-life.
+- **Major release `eol`**: Marks the end of support for the entire major version, including all its minor releases.
+
+When you create a new minor release with `glrd-manage --create minor`, the previous minor release's `eol` is automatically updated to the new minor release's `released` date.
+
+:::info Note on the extended phase
+The `extended` lifecycle field applies to major releases only. It has no direct technical effect on minor releases and is an administrative date that tracks when extended maintenance begins for the parent major version.
+:::
+
+**Example — `major-1312` series:**
+
+| Release | `released` | `eol` | Reason |
+| -------------- | ---------- | ---------- | ------------------------------- |
+| `major-1312` | 2023-11-16 | 2024-08-03 | Major EOL |
+| `minor-1312.1` | 2023-11-23 | 2024-01-15 | Next minor release date |
+| `minor-1312.2` | 2024-01-15 | 2024-02-14 | Next minor release date |
+| `minor-1312.7` | 2024-07-03 | 2024-08-03 | Inherits major EOL (last minor) |
+
+## Nightly and development release dates
+
+`nightly` and `dev` releases only carry a `released` date. They have no `extended` or `eol` phase because:
+
+- Nightly builds are ephemeral: each nightly is superseded by the next nightly run.
+- Development releases are manual snapshots for testing; they are not formally supported.
+
+## Versioning scheme
+
+Garden Linux [introduced semantic versioning](https://github.com/gardenlinux/gardenlinux/issues/3069) starting at major version `2017`. GLRD stores both pre- and post-threshold releases and selects the correct schema automatically:
+
+| Scheme | Applies to | Version format | Example |
+| ------ | ------------ | ------------------- | ---------------------- |
+| v1 | major < 2017 | `major.minor` | `1592.6`, `27.0` |
+| v2 | major ≥ 2017 | `major.minor.patch` | `2017.0.0`, `2222.1.5` |
+
+The threshold is defined as `V2_SCHEMA_THRESHOLD = 2017` in the GLRD codebase (`glrd/util.py`). Any release with a `version.major` value of 2017 or higher uses the v2 schema and requires the `patch` field in `version` objects for `minor`, `nightly`, and `dev` releases.
+
+For `major` and `next` releases, the version only ever stores `major`; the distinction between v1 and v2 does not affect their schema shape.
+
+
diff --git a/docs/how-to/.gitkeep b/docs/how-to/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/docs/how-to/index.md b/docs/how-to/index.md
new file mode 100644
index 0000000..a43e27a
--- /dev/null
+++ b/docs/how-to/index.md
@@ -0,0 +1,26 @@
+---
+title: "Garden Linux Release Database (GLRD)"
+description: "How-to guides for running, querying, and managing Garden Linux Release Database (GLRD) entries."
+order: 4
+github_org: gardenlinux
+github_repo: glrd
+github_source_path: docs/how-to/index.md
+github_target_path: docs/how-to/releases/glrd/index.md
+related_topics:
+ - /how-to/releases/glrd/
+ - /how-to/releases/glrd/run-glrd.md
+ - /how-to/releases/glrd/query-releases.md
+ - /how-to/releases/glrd/manage-releases.md
+ - /reference/supporting_tools/glrd/
+ - /reference/supporting_tools/glrd/cli.md
+ - /reference/supporting_tools/glrd/schema.md
+ - /explanation/glrd-release-lifecycle.md
+---
+
+# GLRD How-To Guides
+
+These guides cover the day-to-day tasks for working with the Garden Linux Release Database (GLRD). GLRD stores and provides release information for the Garden Linux distribution, accessed and managed through two command-line tools: `glrd` (query) and `glrd-manage` (create and update).
+
+## Related topics
+
+
diff --git a/docs/how-to/manage-releases.md b/docs/how-to/manage-releases.md
new file mode 100644
index 0000000..6d8afe5
--- /dev/null
+++ b/docs/how-to/manage-releases.md
@@ -0,0 +1,231 @@
+---
+title: "Manage GLRD entries"
+description: "Step-by-step examples for creating, updating, and deleting Garden Linux releases database entries using the glrd-manage CLI tool."
+order: 3
+github_org: gardenlinux
+github_repo: glrd
+github_source_path: docs/how-to/manage-releases.md
+github_target_path: docs/how-to/releases/glrd/manage-releases.md
+related_topics:
+ - /how-to/releases/glrd/
+ - /how-to/releases/glrd/run-glrd.md
+ - /how-to/releases/glrd/query-releases.md
+ - /how-to/releases/glrd/manage-releases.md
+ - /reference/supporting_tools/glrd/
+ - /reference/supporting_tools/glrd/cli.md
+ - /reference/supporting_tools/glrd/schema.md
+ - /explanation/glrd-release-lifecycle.md
+---
+
+# Manage Garden Linux Releases
+
+This guide shows how to use `glrd-manage` to create, update, and delete releases in the Garden Linux Release Database (GLRD).
+
+## Prerequisites
+
+- `glrd-manage` installed. See [Run GLRD](/how-to/glrd-run.md).
+- AWS credentials with write access to the S3 bucket when you intend to push changes with `--s3-update`. See [AWS authentication](/how-to/glrd-run.md#aws-authentication-for-glrd-manage).
+
+:::warning Safe testing
+All examples on this page omit `--s3-update`. Without that flag, `glrd-manage` writes output files to the current directory and makes no changes to S3. Add `--s3-update` only when you are ready to publish.
+:::
+
+## Test locally
+
+By default, `glrd-manage` writes release files (for example, `releases-minor.json`) to the current working directory and does not touch S3. You can inspect the generated files to verify the output before running with `--s3-update`.
+
+## Generate initial release data
+
+:::info GITHUB_TOKEN required
+`--create-initial-releases` fetches release history from the GitHub API via
+[python-gardenlinux-lib](https://github.com/gardenlinux/python-gardenlinux-lib).
+Set the `GITHUB_TOKEN` environment variable before running this command. The
+token needs no special scopes for the public Garden Linux repository.
+
+```bash
+export GITHUB_TOKEN=
+# Or, if you have the GitHub CLI installed:
+export GITHUB_TOKEN=$(gh auth token)
+```
+
+:::
+
+Use `--create-initial-releases` to fetch the full release history from GitHub and generate all initial release files. Pass the types you want to populate as a comma-separated list:
+
+```bash
+glrd-manage --create-initial-releases major,minor,nightly --input
+```
+
+This generates `releases-major.json`, `releases-minor.json`, `releases-nightly.json`, and processes any manual lifecycle entries from `releases-input.yaml`.
+
+## Create a major release
+
+When you create a major release, GLRD automatically calculates default `extended` and `eol` dates (released + 6 months and + 9 months respectively). See [Default major dates](/explanation/glrd-release-lifecycle.md#default-major-dates) for details.
+
+```bash
+# Use default dates
+glrd-manage --create major --version 1312
+
+# Override dates explicitly
+glrd-manage --create major --version 1312 \
+ --lifecycle-released-isodatetime 2023-11-16T00:00:00 \
+ --lifecycle-extended-isodatetime 2024-05-03T00:00:00 \
+ --lifecycle-eol-isodatetime 2024-08-03T00:00:00
+```
+
+## Create a minor release
+
+When you create a new minor release, `glrd-manage` automatically sets the `eol` of the previous minor release to the new release's `released` date. See [Major and minor release dependencies](/explanation/glrd-release-lifecycle.md#major-and-minor-release-dependencies) for details.
+
+```bash
+glrd-manage --create minor --version 1312.7
+```
+
+## Create a nightly release
+
+Without additional parameters, `glrd-manage` uses the current timestamp and the current git commit to create a nightly release. The next available minor version number is chosen automatically:
+
+```bash
+glrd-manage --create nightly
+```
+
+## Update an existing release
+
+Use `--update` to modify lifecycle dates or the commit hash of an existing release. Specify the release by name and provide at least one modifier flag.
+
+Supported modifiers:
+
+- `--lifecycle-released-isodatetime`: Update the release date (all types).
+- `--lifecycle-extended-isodatetime`: Update the extended maintenance date (`major` and `next` only).
+- `--lifecycle-eol-isodatetime`: Update the end-of-life date (`next`, `major`, and `minor` only).
+- `--commit`: Update the git commit hash (`minor`, `nightly`, and `dev` only).
+
+```bash
+# Update the EOL date on a major release
+glrd-manage --update major-1592 \
+ --lifecycle-eol-isodatetime 2026-02-27T00:00:00 \
+ --input-stdin < releases.json
+
+# Update the extended maintenance date on a major release
+glrd-manage --update major-1592 \
+ --lifecycle-extended-isodatetime 2025-06-15T00:00:00 \
+ --input-stdin < releases.json
+
+# Update the commit hash on a minor release
+glrd-manage --update minor-1592.6 \
+ --commit deadbeef1234567890abcdef1234567890abcdef \
+ --input-stdin < releases.json
+
+# Update multiple fields at once
+glrd-manage --update major-1592 \
+ --lifecycle-eol-isodatetime 2026-02-27T00:00:00 \
+ --lifecycle-extended-isodatetime 2025-06-15T00:00:00 \
+ --input-stdin < releases.json
+```
+
+:::info
+The `--update` action requires existing release data to be provided via `--input-stdin` (as in the examples above) or loaded from the default query source. It cannot be combined with `--no-query`.
+:::
+
+## Create or update a release from JSON (stdin)
+
+You can provide release data directly from standard input in JSON format. This is useful for scripted workflows or when the release data is generated by another tool:
+
+```bash
+echo '{
+ "releases": [
+ {
+ "name": "minor-1592.1",
+ "type": "minor",
+ "version": {
+ "major": 1592,
+ "minor": 1
+ },
+ "lifecycle": {
+ "released": {
+ "isodate": "2024-08-22",
+ "timestamp": 1724277600
+ },
+ "eol": {
+ "isodate": "2025-08-12",
+ "timestamp": 1754949600
+ }
+ },
+ "git": {
+ "commit": "ec945aa995d0f08d64303ff6045b313b40b665fb",
+ "commit_short": "ec945aa"
+ },
+ "github": {
+ "release": "https://github.com/gardenlinux/gardenlinux/releases/tag/1592.1"
+ },
+ "flavors": [
+ "ali-gardener_prod",
+ "azure-gardener_prod",
+ "aws-gardener_prod",
+ "gcp-gardener_prod"
+ ],
+ "attributes": {
+ "source_repo": false
+ }
+ }
+ ]
+}' | glrd-manage --input-stdin
+```
+
+## Create or update a release from a YAML file
+
+Write the release data to a YAML input file and pass it with `--input` and `--input-file`:
+
+```yaml
+# releases-input.yaml
+releases:
+ - name: minor-1592.1
+ type: minor
+ version:
+ major: 1592
+ minor: 1
+ lifecycle:
+ released:
+ isodate: "2024-08-22"
+ timestamp: 1724277600
+ eol:
+ isodate: "2025-08-27"
+ timestamp: 1754949600
+ git:
+ commit: ec945aa995d0f08d64303ff6045b313b40b66fff
+ commit_short: ec945aa
+ github:
+ release: https://github.com/gardenlinux/gardenlinux/releases/tag/1592.1
+ flavors:
+ - ali-gardener_prod
+ - azure-gardener_prod
+ - aws-gardener_prod
+ - gcp-gardener_prod
+ attributes:
+ source_repo: false
+```
+
+```bash
+glrd-manage --input --input-file releases-input.yaml
+```
+
+## Run fully offline
+
+To run `glrd-manage` without any network access, combine `--input-type file` with `--no-flavors`:
+
+```bash
+glrd-manage --create nightly \
+ --input-type file \
+ --no-flavors \
+ --no-query
+```
+
+- `--input-type file` reads existing releases from local files instead of the S3 endpoint.
+- `--no-flavors` skips the Git clone and S3 lookup for flavor resolution.
+- `--no-query` skips the initial query for existing releases entirely. Use with care — without the existing data, `glrd-manage` may overwrite releases.
+
+You can also set `GLRD_SKIP_FLAVORS=1` in the environment as an alternative to `--no-flavors`.
+
+## Related topics
+
+
diff --git a/docs/how-to/query-releases.md b/docs/how-to/query-releases.md
new file mode 100644
index 0000000..1818476
--- /dev/null
+++ b/docs/how-to/query-releases.md
@@ -0,0 +1,183 @@
+---
+title: "Query Garden Linux Releases"
+description: "Step-by-step examples for querying the Garden Linux Release Database (GLRD) with the glrd CLI tool."
+order: 2
+github_org: gardenlinux
+github_repo: glrd
+github_source_path: docs/how-to/query-releases.md
+github_target_path: docs/how-to/releases/glrd/query-releases.md
+related_topics:
+ - /how-to/releases/glrd/
+ - /how-to/releases/glrd/run-glrd.md
+ - /how-to/releases/glrd/query-releases.md
+ - /how-to/releases/glrd/manage-releases.md
+ - /reference/supporting_tools/glrd/
+ - /reference/supporting_tools/glrd/cli.md
+ - /reference/supporting_tools/glrd/schema.md
+ - /explanation/glrd-release-lifecycle.md
+---
+
+# Query Garden Linux Releases
+
+This guide shows how to use `glrd` to retrieve release information from the Garden Linux Release Database (GLRD).
+
+## Prerequisites
+
+- `glrd` installed. See [Run GLRD](/how-to/glrd-run.md).
+- A network connection to the GLRD Amazon S3 bucket (default input source).
+
+To use local release files instead of the S3 endpoint, add `--input-type file` to any command. See the [CLI reference](/reference/supporting_tools/glrd/cli.md) for all available input flags.
+
+## Get the latest release
+
+Get the latest active minor release with default shell output:
+
+```bash
+glrd --latest
+```
+
+Example output:
+
+```text
+Name Version Type GitCommitShort ReleaseDate ExtendedMaintenance EndOfMaintenance
+minor-1592.6 1592.6 minor cb05e11f 2025-02-19 N/A 2025-08-12
+```
+
+## Get only the version number
+
+Use `--fields` and `--no-header` to extract a single field for scripting:
+
+```bash
+glrd --latest --fields Version --no-header
+```
+
+Example output:
+
+```text
+1592.6
+```
+
+## Get JSON output
+
+Use `--output-format json` to retrieve the full release object:
+
+```bash
+glrd --latest --output-format json
+```
+
+The JSON output includes all schema fields for the release, including `version`, `lifecycle`, `git`, `github`, `flavors`, `oci`, and `attributes`. See the [GLRD Release Schema](/reference/supporting_tools/glrd/schema.md) for field definitions.
+
+:::info Version note
+For v2 releases (major ≥ 2017.0.0), the `version` object includes a `patch` field. For v1 releases (major < 2017.0.0), no `patch` field is present.
+:::
+
+## Extract a version string from JSON output
+
+Pipe JSON output to `jq` to extract a specific field:
+
+```bash
+glrd --latest --output-format json | jq -r '.releases[] | "\(.version.major).\(.version.minor)"'
+```
+
+Example output:
+
+```text
+1592.6
+```
+
+For v2 releases, include the patch component:
+
+```bash
+glrd --latest --output-format json | jq -r '.releases[] | "\(.version.major).\(.version.minor).\(.version.patch)"'
+```
+
+## List all active releases
+
+Show all currently active major and minor releases (end-of-life date in the future):
+
+```bash
+glrd --active
+```
+
+Example output:
+
+```text
+Name Version Type GitCommitShort ReleaseDate ExtendedMaintenance EndOfMaintenance
+major-1443 1443 major N/A 2024-03-13 2024-09-13 2025-01-13
+minor-1443.15 1443.15 minor 5d33a69 2024-10-10 N/A 2025-01-13
+major-1592 1592 major N/A 2024-08-12 2025-05-12 2025-08-12
+minor-1592.1 1592.1 minor ec945aa 2024-08-22 N/A 2025-08-12
+```
+
+## Filter by release type
+
+Show only nightly releases:
+
+```bash
+glrd --type nightly
+```
+
+Show major and next releases:
+
+```bash
+glrd --type major,next
+```
+
+## Filter by version
+
+Show all releases for a specific major version:
+
+```bash
+glrd --version 1592
+```
+
+Show a specific minor release:
+
+```bash
+glrd --version 1592.6
+```
+
+## Generate a Mermaid Gantt chart
+
+Use `--output-format mermaid_gantt` to produce a [Mermaid Gantt chart](https://mermaid.js.org/syntax/gantt.html) of active releases. This is useful for embedding in documentation or dashboards.
+
+```bash
+glrd --active --type next,major --output-format mermaid_gantt --output-description "Garden Linux active Releases"
+```
+
+Example output:
+
+```mermaid
+gantt
+ title Garden Linux active Releases
+ axisFormat %m.%y
+ section 1443
+ Release: milestone, 2024-03-13, 0m
+ Standard maintenance: task, 2024-03-13, 6M
+ Extended maintenance: milestone, 2024-09-13, 0m
+ Extended maintenance: task, 2024-09-13, 4M
+ End of maintenance: milestone, 2025-01-13, 0m
+ section 1592
+ Release: milestone, 2024-08-12, 0m
+ Standard maintenance: task, 2024-08-12, 9M
+ Extended maintenance: milestone, 2025-05-12, 0m
+ Extended maintenance: task, 2025-05-12, 3M
+ End of maintenance: milestone, 2025-08-12, 0m
+```
+
+## Query from a local file
+
+To run `glrd` without network access, download the JSON files first and point `glrd` at them:
+
+```bash
+# Download release data
+curl -s https://gardenlinux-glrd.s3.eu-central-1.amazonaws.com/releases-major.json -o releases-major.json
+curl -s https://gardenlinux-glrd.s3.eu-central-1.amazonaws.com/releases-minor.json -o releases-minor.json
+
+# Query from local files
+glrd --input-type file --latest
+```
+
+## Related topics
+
+
diff --git a/docs/how-to/run-glrd.md b/docs/how-to/run-glrd.md
new file mode 100644
index 0000000..a508416
--- /dev/null
+++ b/docs/how-to/run-glrd.md
@@ -0,0 +1,154 @@
+---
+title: "Run GLRD"
+description: "How to install and run the GLRD tools locally, in a container, or via GitHub Actions."
+order: 1
+github_org: gardenlinux
+github_repo: glrd
+github_source_path: docs/how-to/run-glrd.md
+github_target_path: docs/how-to/releases/glrd/run-glrd.md
+related_topics:
+ - /how-to/releases/glrd/
+ - /how-to/releases/glrd/run-glrd.md
+ - /how-to/releases/glrd/query-releases.md
+ - /how-to/releases/glrd/manage-releases.md
+ - /reference/supporting_tools/glrd/
+ - /reference/supporting_tools/glrd/cli.md
+ - /reference/supporting_tools/glrd/schema.md
+ - /explanation/glrd-release-lifecycle.md
+---
+
+# Run GLRD
+
+This guide covers how to set up and run the `glrd` and `glrd-manage` tools. Choose the method that fits your environment.
+
+## Prerequisites
+
+All methods require:
+
+- A network connection to the Garden Linux GLRD Amazon S3 bucket for read queries (unless using local file input).
+- Amazon Web Services (AWS) credentials with write access to the S3 bucket when using `glrd-manage --s3-update`.
+
+For local Python installation, you also need:
+
+- **Python 3.10 or later**
+- A **`GITHUB_TOKEN`** environment variable — required only if you plan to generate release data from GitHub history (the `--create-initial-releases` flag). The token needs no special scopes for the public Garden Linux repository. You can obtain one from [GitHub Settings → Developer settings → Personal access tokens](https://github.com/settings/tokens) or, if you have the GitHub CLI installed, by running `export GITHUB_TOKEN=$(gh auth token)`.
+
+## Install locally with Poetry
+
+1. Clone the repository:
+
+ ```bash
+ git clone https://github.com/gardenlinux/glrd.git
+ cd glrd
+ ```
+
+1. Install dependencies:
+
+ ```bash
+ poetry install
+ ```
+
+1. Verify the installation:
+
+ ```bash
+ poetry run glrd --help
+ poetry run glrd-manage --help
+ ```
+
+After installation the tools are available as `glrd` and `glrd-manage` within the Poetry virtual environment, or via `.venv/bin/glrd` and `.venv/bin/glrd-manage` if you activate the environment.
+
+## Run in a container
+
+You can run both tools from the pre-built container image without installing Python or Poetry locally.
+
+### Use the pre-built image
+
+```bash
+# Query releases (read-only, no credentials needed)
+podman run -it --rm ghcr.io/gardenlinux/glrd glrd
+
+# Manage releases (requires AWS credentials)
+podman run -it --rm \
+ -e AWS_ACCESS_KEY_ID \
+ -e AWS_SECRET_ACCESS_KEY \
+ -e AWS_SESSION_TOKEN \
+ ghcr.io/gardenlinux/glrd glrd-manage
+```
+
+### Build and run locally
+
+```bash
+podman build -t glrd .
+
+# Query releases
+podman run -it --rm glrd glrd
+
+# Manage releases
+podman run -it --rm \
+ -e AWS_ACCESS_KEY_ID \
+ -e AWS_SECRET_ACCESS_KEY \
+ -e AWS_SESSION_TOKEN \
+ glrd glrd-manage
+```
+
+## Run in a GitHub Actions workflow
+
+The GLRD repository publishes a GitHub Action you can use directly in your workflows. The following example retrieves the latest minor and nightly versions:
+
+```yaml
+steps:
+ - name: Get latest Garden Linux minor version
+ id: gl_version_latest
+ uses: gardenlinux/glrd@main
+ with:
+ cmd: glrd --no-header --type minor --latest --fields Version
+
+ - name: Use latest Garden Linux minor version
+ run: echo "${{ steps.gl_version_latest.outputs.result }}"
+
+ - name: Get latest Garden Linux nightly version
+ id: gl_version_nightly
+ uses: gardenlinux/glrd@main
+ with:
+ cmd: glrd --no-header --type nightly --latest --fields Version
+
+ - name: Use latest Garden Linux nightly version
+ run: echo "${{ steps.gl_version_nightly.outputs.result }}"
+```
+
+The `result` output contains the raw text output of the `cmd` command.
+
+## Query JSON data manually
+
+You can fetch raw JSON release data directly from the S3 bucket without using the `glrd` CLI. The available files are:
+
+- `releases-major.json`
+- `releases-minor.json`
+- `releases-nightly.json`
+- `releases-dev.json`
+
+Example — fetch all major releases:
+
+```bash
+curl -s https://gardenlinux-glrd.s3.eu-central-1.amazonaws.com/releases-major.json
+```
+
+## AWS authentication for `glrd-manage`
+
+`glrd-manage` requires write access to the S3 bucket when `--s3-update` is passed. Configure credentials using any method supported by the [AWS Boto3 credentials chain](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html).
+
+When running in a container, pass credentials via environment variables:
+
+```bash
+podman run -it --rm \
+ -e AWS_ACCESS_KEY_ID \
+ -e AWS_SECRET_ACCESS_KEY \
+ -e AWS_SESSION_TOKEN \
+ ghcr.io/gardenlinux/glrd glrd-manage --create minor --version 1592.7
+```
+
+The environment variables are forwarded from the host shell into the container without exposing their values in the command line.
+
+## Related topics
+
+
diff --git a/docs/overview/README.md b/docs/overview/README.md
deleted file mode 100644
index 348c42c..0000000
--- a/docs/overview/README.md
+++ /dev/null
@@ -1,869 +0,0 @@
----
-title: "Release Database"
-description: "How to use the Garden Linux Release Database"
-github_org: gardenlinux
-github_repo: glrd
-github_source_path: docs/overview/README.md
-github_target_path: docs/reference/supporting_tools/glrd.md
----
-
-# Garden Linux Release Database (GLRD)
-
-The [GLRD repository](https://github.com/gardenlinux/glrd) contains tooling and configuration to create the Garden Linux Release Database (GLRD), a system designed to manage and query release information for the Garden Linux distribution.
-
-This repository contains these tools:
-
-- **`glrd-manage`**: A tool for generating and populating the GLRD with initial release data and for creating individual release entries.
-- **`glrd`**: A command-line client for querying the GLRD to retrieve release information based on various criteria.
-
-## Table of Contents
-
-- [Garden Linux Releases](#garden-linux-releases)
-- [Overview of GLRD](#overview)
-- [Prerequisites](#prerequisites)
-- [Run GLRD](#run-glrd)
-- [glrd](#glrd)
-- [glrd-manage](#glrd-manage)
-- [Release Schema](#release-schema)
-- [Contributing](#contributing)
-- [License](#license)
-
-## Garden Linux Releases
-
-For a general overview about Garden Linux releases and their lifecycle, have a look at the [Garden Linux Release Lifecycle](/reference/releases/release-lifecycle).
-
-## Overview
-
-The GLRD provides a structured way to store and access release data for Garden Linux, including different release types such as major, minor, nightly, and development releases. It uses JSON and YAML formats to store release information and supports integration with AWS S3 for storage to host release data..
-
-
-
-## Run GLRD
-
-### Manually run Python scripts
-
-
- Details
-
-### Prerequisites
-
-- **Python 3.10+**
-
-You can install the required Python packages using:
-
-```bash
-poetry install
-```
-
-- **AWS CLI** configured with appropriate permissions if you plan to use S3 integration.
-- **Git** and **GitHub CLI (gh)** installed and configured if you plan to generate release data from GitHub.
-
-### Installation
-
-Clone the repository and ensure that the scripts are executable:
-
-```bash
-git clone https://github.com/gardenlinux/glrd.git
-cd glrd
-poetry install
-```
-
-
-
-### Run in container
-
-
- Details
-
-You can also use the GLRD tools by building or running a container image.
-
-#### Run pre-build image
-
-```
-podman run -it --rm ghcr.io/gardenlinux/glrd glrd
-podman run -it --rm -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN ghcr.io/gardenlinux/glrd glrd-manage
-
-```
-
-#### Build and run image locally
-
-```
-podman build -t glrd .
-podman run -it --rm glrd glrd
-podman run -it --rm -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN glrd glrd-manage
-```
-
-
-
-### Run in GitHub action
-
-
- Details
-
-```
- - name: Get latest GL minor version
- id: gl_version_latest
- uses: gardenlinux/glrd@main
- with:
- cmd: glrd --no-header --type minor --latest --fields Version
- - name: Use latest GL minor version
- run: echo ${{ steps.gl_version_latest.outputs.result }}
-
- - name: Get latest GL nightly version
- id: gl_version_nightly
- uses: gardenlinux/glrd@main
- with:
- cmd: glrd --no-header --type nightly --latest --fields Version
- - name: Use latest GL nightly version
- run: echo ${{ steps.gl_version_nightly.outputs.result }}
-```
-
-
-
-### Manually query JSON data
-
-
- Details
-
-You can manually query the release JSON data by getting them from our production S3 bucket.
-
-Query the release type that you want:
-
-- `releases-major.json`
-- `releases-minor.json`
-- `releases-nightly.json`
-- `releases-dev.json`
-
-To e.g. query the major releases:
-
-```bash
-❯ curl -s https://gardenlinux-glrd.s3.eu-central-1.amazonaws.com/releases-major.json
-{"releases":[{"name":"major-27","type":"major","version":{"major":27},"lifecycle":{"released":{"isodate":"2020-06-09","timestamp":1591694693},"extended":{"isodate":"2020-12-09","timestamp":1607472000},"eol":{"isodate":"2021-03-09","timestamp":1615248000}}},{"name":"major-184","type":"major","version":{"major":184},"lifecycle":{"released":{"isodate":"2020-10-29","timestamp":1603984625},"extended":{"isodate":"2020-04-29","timestamp":1588118400},"eol":{"isodate":"2021-07-29","timestamp":1627516800}}},{"name":"major-318","type":"major","version":{"major":318},"lifecycle":{"released":{"isodate":"2021-04-28","timestamp":1619614135},"extended":{"isodate":"2021-10-28","timestamp":1635379200},"eol":{"isodate":"2023-01-28","timestamp":1674864000}}},{"name":"major-576","type":"major","version":{"major":576},"lifecycle":{"released":{"isodate":"2021-11-17","timestamp":1637142852},"extended":{"isodate":"2023-05-17","timestamp":1684281600},"eol":{"isodate":"2023-08-17","timestamp":1692230400}}},{"name":"major-934","type":"major","version":{"major":934},"lifecycle":{"released":{"isodate":"2023-06-05","timestamp":1685968163},"extended":{"isodate":"2023-12-05","timestamp":1701734400},"eol":{"isodate":"2024-03-05","timestamp":1709596800}}},{"name":"major-1312","type":"major","version":{"major":1312},"lifecycle":{"released":{"isodate":"2023-11-16","timestamp":1700136050},"extended":{"isodate":"2024-05-03","timestamp":1714694400},"eol":{"isodate":"2024-08-03","timestamp":1722643200}}},{"name":"major-1443","type":"major","version":{"major":1443},"lifecycle":{"released":{"isodate":"2024-03-13","timestamp":1710341636},"extended":{"isodate":"2024-09-13","timestamp":1726185600},"eol":{"isodate":"2025-01-13","timestamp":1736726400}}},{"name":"major-1592","type":"major","version":{"major":1592},"lifecycle":{"released":{"isodate":"2024-08-12","timestamp":1723457202},"extended":{"isodate":"2025-05-12","timestamp":1747008000},"eol":{"isodate":"2025-08-12","timestamp":1754956800}}}]}
-```
-
-
-
-### AWS Authentication
-
-`glrd-manage` needs write access to an AWS S3 bucket to create and manage releases. See [Boto3 Credentials documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) and ways to configure this.
-If you are running `glrd-manage` in a container, you might want to use [Environment variables](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#environment-variables) and pass those via e.g. `podman run ... -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN`.
-
-## glrd
-
-The `glrd` script is a command-line utility for querying the GLRD. It allows you to filter and display release information based on various criteria.
-
-### Usage
-
-#### Show help
-
-```
-❯ glrd --help
-usage: glrd [-h] [--input-format {yaml,json}] [--input-file-prefix INPUT_FILE_PREFIX] [--input-type {file,url}] [--input-url INPUT_URL] [--no-input-split] [--output-format {json,yaml,markdown,mermaid_gantt,shell}] [--output-description OUTPUT_DESCRIPTION] [--active] [--archived] [--latest] [--type TYPE] [--version VERSION] [--fields FIELDS] [--no-header] [-V]
-
-Process and filter releases data from a file or URL.
-
-options:
- -h, --help show this help message and exit
- --input-format {yaml,json}
- Input format: 'yaml' or 'json' (default: json).
- --input-file-prefix INPUT_FILE_PREFIX
- The prefix to get input files (default: releases).
- --input-type {file,url}
- Specify if the input type (default: url).
- --input-url INPUT_URL
- Input URL to the releases data. Defaults to gardenlinux-glrd S3 URL.
- --no-input-split Do not split Input into major+minor and nightly. No additional input-files *-nightly and *-dev will be parsed.
- --output-format {json,yaml,markdown,mermaid_gantt,shell}
- Output format: json, yaml, markdown, mermaid_gantt, shell (default).
- --output-description OUTPUT_DESCRIPTION
- Description, added to certain outputs, e.g. mermaid (default: 'Garden Linux Releases').
- --active Show only active releases.
- --archived Show only archived releases.
- --latest Show the latest active major.minor release.
- --type TYPE Filter by release types (comma-separated list, default: major,minor). E.g., --type major,minor,nightly,dev,next
- --version VERSION Filter by a specific version (major, major.minor, or major.minor.patch). E.g., --version 1312, --version 1312.0, or --version 2017.0.0
- --fields FIELDS Comma-separated list of fields to output. Possible fields: Name,Version,Type,GitCommit,GitCommitShort,ReleaseDate,ReleaseTime,ExtendedMaintenance,EndOfMaintenance,Flavors,OCI,AttributesSourceRepo (default: Name,Version,Type,GitCommitShort,ReleaseDate,ExtendedMaintenance,EndOfMaintenance)
- --no-header Omit the header in shell output.
- -V show program's version number and exit
-```
-
-### Get latest Garden Linux version
-
-#### Default shell output
-
-```
-# default shell output
-❯ glrd --latest
-Name Version Type GitCommitShort ReleaseDate ExtendedMaintenance EndOfMaintenance
-minor-1592.6 1592.6 minor cb05e11f 2025-02-19 N/A 2025-08-12
-```
-
-#### Get only version field
-
-```
-❯ glrd --latest --fields Version --no-header
-1592.6
-```
-
-#### Get JSON output
-
-
- Details
-
-```
-❯ glrd --latest --output-format json
-{
- "releases": [
- {
- "name": "minor-1592.6",
- "type": "minor",
- "version": {
- "major": 1592,
- "minor": 6
- // Note: patch field is only present for versions ≥ 2017.0.0
- },
- "lifecycle": {
- "released": {
- "isodate": "2025-02-19",
- "timestamp": 1739951325
- },
- "eol": {
- "isodate": "2025-08-12",
- "timestamp": 1754956800
- }
- },
- "git": {
- "commit": "cb05e11f0481b72d0a30da3662295315b220a436",
- "commit_short": "cb05e11f"
- },
- "github": {
- "release": "https://github.com/gardenlinux/gardenlinux/releases/tag/1592.6"
- },
- "flavors": {
- "ali-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/ali-gardener_prod-amd64-1592.6-cb05e11f/ali-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/ali-gardener_prod-amd64-1592.6-cb05e11f/ali-gardener_prod-amd64-1592.6-cb05e11f.qcow2"
- },
- "ali-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/ali-gardener_prod-arm64-1592.6-cb05e11f/ali-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/ali-gardener_prod-arm64-1592.6-cb05e11f/ali-gardener_prod-arm64-1592.6-cb05e11f.qcow2"
- },
- "aws-gardener_persistence_prod_readonly_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f/aws-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f/aws-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "aws-gardener_persistence_prod_readonly_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f/aws-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f/aws-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "aws-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod-amd64-1592.6-cb05e11f/aws-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod-amd64-1592.6-cb05e11f/aws-gardener_prod-amd64-1592.6-cb05e11f.raw"
- },
- "aws-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod-arm64-1592.6-cb05e11f/aws-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod-arm64-1592.6-cb05e11f/aws-gardener_prod-arm64-1592.6-cb05e11f.raw"
- },
- "aws-gardener_prod_readonly_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f/aws-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f/aws-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "aws-gardener_prod_readonly_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f/aws-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f/aws-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "aws-gardener_prod_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod_secureboot-amd64-1592.6-cb05e11f/aws-gardener_prod_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod_secureboot-amd64-1592.6-cb05e11f/aws-gardener_prod_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "aws-gardener_prod_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod_secureboot-arm64-1592.6-cb05e11f/aws-gardener_prod_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/aws-gardener_prod_secureboot-arm64-1592.6-cb05e11f/aws-gardener_prod_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "azure-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/azure-gardener_prod-amd64-1592.6-cb05e11f/azure-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/azure-gardener_prod-amd64-1592.6-cb05e11f/azure-gardener_prod-amd64-1592.6-cb05e11f.vhd"
- },
- "azure-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/azure-gardener_prod-arm64-1592.6-cb05e11f/azure-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/azure-gardener_prod-arm64-1592.6-cb05e11f/azure-gardener_prod-arm64-1592.6-cb05e11f.vhd"
- },
- "gcp-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/gcp-gardener_prod-amd64-1592.6-cb05e11f/gcp-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/gcp-gardener_prod-amd64-1592.6-cb05e11f/gcp-gardener_prod-amd64-1592.6-cb05e11f.gcpimage.tar.gz"
- },
- "gcp-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/gcp-gardener_prod-arm64-1592.6-cb05e11f/gcp-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/gcp-gardener_prod-arm64-1592.6-cb05e11f/gcp-gardener_prod-arm64-1592.6-cb05e11f.gcpimage.tar.gz"
- },
- "gdch-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/gdch-gardener_prod-amd64-1592.6-cb05e11f/gdch-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/gdch-gardener_prod-amd64-1592.6-cb05e11f/gdch-gardener_prod-amd64-1592.6-cb05e11f.gcpimage.tar.gz"
- },
- "gdch-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/gdch-gardener_prod-arm64-1592.6-cb05e11f/gdch-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/gdch-gardener_prod-arm64-1592.6-cb05e11f/gdch-gardener_prod-arm64-1592.6-cb05e11f.gcpimage.tar.gz"
- },
- "kvm-gardener_persistence_prod_readonly_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f/kvm-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f/kvm-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "kvm-gardener_persistence_prod_readonly_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f/kvm-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f/kvm-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "kvm-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod-amd64-1592.6-cb05e11f/kvm-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod-amd64-1592.6-cb05e11f/kvm-gardener_prod-amd64-1592.6-cb05e11f.raw"
- },
- "kvm-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod-arm64-1592.6-cb05e11f/kvm-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod-arm64-1592.6-cb05e11f/kvm-gardener_prod-arm64-1592.6-cb05e11f.raw"
- },
- "kvm-gardener_prod_readonly_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f/kvm-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f/kvm-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "kvm-gardener_prod_readonly_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f/kvm-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f/kvm-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "kvm-gardener_prod_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod_secureboot-amd64-1592.6-cb05e11f/kvm-gardener_prod_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod_secureboot-amd64-1592.6-cb05e11f/kvm-gardener_prod_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "kvm-gardener_prod_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod_secureboot-arm64-1592.6-cb05e11f/kvm-gardener_prod_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/kvm-gardener_prod_secureboot-arm64-1592.6-cb05e11f/kvm-gardener_prod_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_persistence_prod_readonly_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f/metal-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f/metal-gardener_persistence_prod_readonly_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_persistence_prod_readonly_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f/metal-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f/metal-gardener_persistence_prod_readonly_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod-amd64-1592.6-cb05e11f/metal-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod-amd64-1592.6-cb05e11f/metal-gardener_prod-amd64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod-arm64-1592.6-cb05e11f/metal-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod-arm64-1592.6-cb05e11f/metal-gardener_prod-arm64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_prod_pxe-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_pxe-amd64-1592.6-cb05e11f/metal-gardener_prod_pxe-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_pxe-amd64-1592.6-cb05e11f/metal-gardener_prod_pxe-amd64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_prod_pxe-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_pxe-arm64-1592.6-cb05e11f/metal-gardener_prod_pxe-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_pxe-arm64-1592.6-cb05e11f/metal-gardener_prod_pxe-arm64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_prod_readonly_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f/metal-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f/metal-gardener_prod_readonly_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_prod_readonly_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f/metal-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f/metal-gardener_prod_readonly_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_prod_secureboot-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_secureboot-amd64-1592.6-cb05e11f/metal-gardener_prod_secureboot-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_secureboot-amd64-1592.6-cb05e11f/metal-gardener_prod_secureboot-amd64-1592.6-cb05e11f.raw"
- },
- "metal-gardener_prod_secureboot-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_secureboot-arm64-1592.6-cb05e11f/metal-gardener_prod_secureboot-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/metal-gardener_prod_secureboot-arm64-1592.6-cb05e11f/metal-gardener_prod_secureboot-arm64-1592.6-cb05e11f.raw"
- },
- "openstack-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/openstack-gardener_prod-amd64-1592.6-cb05e11f/openstack-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/openstack-gardener_prod-amd64-1592.6-cb05e11f/openstack-gardener_prod-amd64-1592.6-cb05e11f.qcow2"
- },
- "openstack-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/openstack-gardener_prod-arm64-1592.6-cb05e11f/openstack-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/openstack-gardener_prod-arm64-1592.6-cb05e11f/openstack-gardener_prod-arm64-1592.6-cb05e11f.qcow2"
- },
- "openstackbaremetal-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/openstackbaremetal-gardener_prod-amd64-1592.6-cb05e11f/openstackbaremetal-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/openstackbaremetal-gardener_prod-amd64-1592.6-cb05e11f/openstackbaremetal-gardener_prod-amd64-1592.6-cb05e11f.qcow2"
- },
- "openstackbaremetal-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/openstackbaremetal-gardener_prod-arm64-1592.6-cb05e11f/openstackbaremetal-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/openstackbaremetal-gardener_prod-arm64-1592.6-cb05e11f/openstackbaremetal-gardener_prod-arm64-1592.6-cb05e11f.qcow2"
- },
- "vmware-gardener_prod-amd64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/vmware-gardener_prod-amd64-1592.6-cb05e11f/vmware-gardener_prod-amd64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/vmware-gardener_prod-amd64-1592.6-cb05e11f/vmware-gardener_prod-amd64-1592.6-cb05e11f.ova"
- },
- "vmware-gardener_prod-arm64": {
- "metadata": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/vmware-gardener_prod-arm64-1592.6-cb05e11f/vmware-gardener_prod-arm64-1592.6-cb05e11f.manifest",
- "image": "https://gardenlinux-github-releases.s3.amazonaws.com/objects/vmware-gardener_prod-arm64-1592.6-cb05e11f/vmware-gardener_prod-arm64-1592.6-cb05e11f.ova"
- }
- },
- "oci": "ghcr.io/gardenlinux/gardenlinux:1592.6",
- "attributes": {
- "source_repo": true
- }
- }
- ]
-}
-```
-
-
-
-#### Get json output and filter for version
-
-```
-❯ glrd --latest --output-format json | jq -r '.releases[] | "\(.version.major).\(.version.minor)"'
-1592.6
-```
-
-**Note**: For versions ≥ 2017.0.0, you can also filter by patch version: `jq -r '.releases[] | "\(.version.major).\(.version.minor).\(.version.patch)"'`
-
-### Get all active and supported Garden Linux versions
-
-```
-❯ glrd --active
-Name Version Type Git Commit Release date Extended maintenance End of maintenance
-major-1443 1443 major N/A 2024-03-13 2024-09-13 2025-01-13
-minor-1443.15 1443.15 minor 5d33a69 2024-10-10 N/A 2025-01-13
-major-1592 1592 major N/A 2024-08-12 2025-05-12 2025-08-12
-minor-1592.1 1592.1 minor ec945aa 2024-08-22 N/A 2025-08-12
-```
-
-### Create [Mermaid Gantt Chart](https://mermaid.js.org/syntax/gantt.html) for active releases
-
-```
-❯ glrd --active --type next,major --output-format mermaid_gantt --output-description "Garden Linux active Releases"
-gantt
- title Garden Linux active Releases
- axisFormat %m.%y
- section 1443
- Release: milestone, 2024-03-13, 0m
- Standard maintenance: task, 2024-03-13, 6M
- Extended maintenance: milestone, 2024-09-13, 0m
- Extended maintenance: task, 2024-09-13, 4M
- End of maintenance: milestone, 2025-01-13, 0m
- section 1592
- Release: milestone, 2024-08-12, 0m
- Standard maintenance: task, 2024-08-12, 9M
- Extended maintenance: milestone, 2025-05-12, 0m
- Extended maintenance: task, 2025-05-12, 3M
- End of maintenance: milestone, 2025-08-12, 0m
- section next
- Release: milestone, 2024-12-01, 0m
- Standard maintenance: task, 2024-12-01, 6M
- Extended maintenance: milestone, 2025-06-01, 0m
- Extended maintenance: task, 2025-06-01, 3M
- End of maintenance: milestone, 2025-09-01, 0m
-```
-
-## glrd-manage
-
-The `glrd-manage` script is used to generate release data for Garden Linux. It can create initial releases by fetching data from GitHub, generate individual release entries, and manage release data files.
-
-### Show help
-
-```
-❯ glrd-manage --help
-usage: glrd-manage [-h] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--input-file INPUT_FILE] [--output-format {yaml,json}] [--output-file-prefix OUTPUT_FILE_PREFIX] [--s3-bucket-name S3_BUCKET_NAME] [--s3-bucket-region S3_BUCKET_REGION] [--s3-bucket-prefix S3_BUCKET_PREFIX] [--delete DELETE] [--create-initial-releases CREATE_INITIAL_RELEASES] [--create CREATE]
- [--version VERSION] [--commit COMMIT] [--lifecycle-released-isodatetime LIFECYCLE_RELEASED_ISODATETIME] [--lifecycle-extended-isodatetime LIFECYCLE_EXTENDED_ISODATETIME] [--lifecycle-eol-isodatetime LIFECYCLE_EOL_ISODATETIME] [--no-query] [--input-stdin] [--input] [--no-output-split] [--s3-create-bucket] [--s3-update] [--output-all] [--input-all] [-V]
-
-Manage Garden Linux releases data.
-
-options:
- -h, --help show this help message and exit
- --log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
- Set the logging level
- --input-file INPUT_FILE
- The name of the input file (default: releases-input.yaml).
- --output-format {yaml,json}
- Output format: yaml or json (default: yaml).
- --output-file-prefix OUTPUT_FILE_PREFIX
- The prefix for output files (default: releases).
- --s3-bucket-name S3_BUCKET_NAME
- Name of S3 bucket. Defaults to 'gardenlinux-glrd'.
- --s3-bucket-region S3_BUCKET_REGION
- Region for S3 bucket. Defaults to 'eu-central-1'.
- --s3-bucket-prefix S3_BUCKET_PREFIX
- Prefix for S3 bucket objects. Defaults to empty string.
- --delete DELETE Delete a release by name (format: type-major.minor). Requires --s3-update.
- --create-initial-releases CREATE_INITIAL_RELEASES
- Comma-separated list of initial releases to retrieve and generate: 'major,minor,nightly'.
- --create CREATE Create a release for this type using the current timestamp and git information (choose one of: major,minor,nightly,dev,next)'.
- --version VERSION Manually specify the version (format: major.minor for versions < 2017.0.0, major.minor.patch for versions ≥ 2017.0.0).
- --commit COMMIT Manually specify the git commit hash (40 characters).
- --lifecycle-released-isodatetime LIFECYCLE_RELEASED_ISODATETIME
- Manually specify the release date and time in ISO format (YYYY-MM-DDTHH:MM:SS).
- --lifecycle-extended-isodatetime LIFECYCLE_EXTENDED_ISODATETIME
- Manually specify the extended maintenance date and time in ISO format (YYYY-MM-DDTHH:MM:SS).
- --lifecycle-eol-isodatetime LIFECYCLE_EOL_ISODATETIME
- Manually specify the EOL date and time in ISO format (YYYY-MM-DDTHH:MM:SS).
- --no-query Do not query and use existing releases using glrd command. Be careful, this can delete your releases.
- --input-stdin Process a single input from stdin (JSON data).
- --input Process input from --input-file.
- --no-output-split Do not split Output into major+minor and nightly. Additional output-files *-nightly and *-dev will not be created.
- --s3-create-bucket Create an S3 bucket.
- --s3-update Update (merge) the generated files with S3.
- --output-all Download and write all release files found in S3 to local disk
- --input-all Upload all local release files to S3
- -V show program's version number and exit
-```
-
-### Testing release creation
-
-Files named `releases-${type}.json` will be created in the current working directory.
-
-Without passing `--s3-update`, no actual update will be made and changes can safely be tested and verified locally.
-
-### Generate and populate initial release data
-
-This will generate the following initial release data ...
-
-- major releases
-- minor releases
-- nightly releases
-- releases from `releases-input.yaml` (contain manual lifecycle fields)
-
-... and upload it to the default S3 bucket (if `--s3-update` is passed).
-
-```
-❯ glrd-manage --create-initial-releases major,minor,nightly --input
-```
-
-### Generate/Update an arbitrary release from JSON/YAML data
-
-This will generate/update a release from JSON data and upload it to the default S3 bucket.
-
-```
-❯ echo '{
- "releases": [
- {
- "name": "minor-1592.1",
- "type": "minor",
- "version": {
- "major": 1592,
- "minor": 1
- },
- "lifecycle": {
- "released": {
- "isodate": "2024-08-22",
- "timestamp": 1724277600
- },
- "eol": {
- "isodate": "2025-08-12",
- "timestamp": 1754949600
- }
- },
- "git": {
- "commit": "ec945aa995d0f08d64303ff6045b313b40b665fb",
- "commit_short": "ec945aa"
- },
- "github": {
- "release": "https://github.com/gardenlinux/gardenlinux/releases/tag/1592.1"
- },
- "flavors": [
- "ali-gardener_prod",
- "azure-gardener_prod",
- "aws-gardener_prod",
- "gcp-gardener_prod"
- ],
- "attributes": {
- "source_repo": false
- }
- }
- ]
-}' | glrd-manage --input-stdin
-```
-
-Another approach is writing release YAML data to an input file and use this file as input parameter.
-
-```
-❯ cat releases-input.yaml
-releases:
- - name: minor-1592.1
- type: minor
- version:
- major: 1592
- minor: 1
- lifecycle:
- released:
- isodate: "2024-08-22"
- timestamp: 1724277600
- eol:
- isodate: "2025-08-27"
- timestamp: 1754949600
- git:
- commit: ec945aa995d0f08d64303ff6045b313b40b66fff
- commit_short: ec945aa
- github:
- release: https://github.com/gardenlinux/gardenlinux/releases/tag/1592.1
- flavors:
- - ali-gardener_prod
- - azure-gardener_prod
- - aws-gardener_prod
- - gcp-gardener_prod
- attributes:
- source_repo: false
-
-❯ glrd-manage --input --input-file releases-input.yaml
-```
-
-### Create or update a major release
-
-https://github.com/gardenlinux/glrd?tab=readme-ov-file#default-major-dates
-When creating a new major release, [Default Major dates](#default-major-dates) can be automatically set for you. In addition to that, you can also overwrite the dates by hand.
-
-```
-# use default dates
-❯ glrd-manage --create major --version 1312
-
-# overwrite default dates
-❯ glrd-manage --create major --version 1312 --date-time-released 2023-11-16T00:00:00 --date-time-extended 2024-05-03T00:00:00 --date-time-eol 2024-08-03T00:00:00
-```
-
-### Create or update a minor release
-
-When creating a new minor release, the previous minor release of the same major release gets automatically updated. See [Lifecycle Dependencies](#major-and-minor-releases) for details.
-
-```
-# create new minor
-❯ glrd-manage --create minor --version 1312.7
-```
-
-### Create a new nightly release
-
-Without any additional parameters, the current timestamp and git information will be used to create releases. For minor, nightly and dev releases, the next free minor version is automatically chosen.
-
-```
-❯ glrd-manage --create nightly
-```
-
-### Update an existing release
-
-The `--update` action allows you to modify lifecycle fields and commit hashes on existing releases. You must specify the release by name (format: `type-major`, `type-major.minor`, or `type-major.minor.patch`) and provide at least one modifier flag.
-
-**Available modifiers:**
-
-- `--lifecycle-released-isodatetime`: Update the release date (valid for all types)
-- `--lifecycle-extended-isodatetime`: Update the extended maintenance date (only for `major` and `next` types)
-- `--lifecycle-eol-isodatetime`: Update the end-of-life date (only for `next`, `major`, and `minor` types)
-- `--commit`: Update the git commit hash (only for `minor`, `nightly`, and `dev` types)
-
-**Examples:**
-
-```bash
-# Update EOL date on a major release
-❯ glrd-manage --update major-1592 --lifecycle-eol-isodatetime 2026-02-27T00:00:00 --input-stdin < releases.json
-
-# Update extended maintenance date on a major release
-❯ glrd-manage --update major-1592 --lifecycle-extended-isodatetime 2025-06-15T00:00:00 --input-stdin < releases.json
-
-# Update commit hash on a minor release
-❯ glrd-manage --update minor-1592.6 --commit deadbeef1234567890abcdef1234567890abcdef --input-stdin < releases.json
-
-# Update multiple fields at once
-❯ glrd-manage --update major-1592 --lifecycle-eol-isodatetime 2026-02-27T00:00:00 --lifecycle-extended-isodatetime 2025-06-15T00:00:00 --input-stdin < releases.json
-```
-
-**Notes:**
-
-- The `--update` action requires existing release data to be provided via `--input-stdin` or loaded from the default query source
-- The `--update` action cannot be used with `--no-query`
-- Date/time values must be in ISO format: `YYYY-MM-DDTHH:MM:SS`
-- Commit hashes must be exactly 40 characters (full SHA-1 hash)
-
-## Release schema
-
-The Garden Linux Release Database (GLRD) uses structured JSON schemas to represent different types of releases: **major**, **minor**, **nightly**, and **development** releases. Each release type has specific fields that capture essential information about the release.
-
-### Versioning scheme
-
-Gardenlinux [introduced semantic Versioning](https://github.com/gardenlinux/gardenlinux/issues/3069) in [TODO!!! name commit !!!]. GLRD supports both versioning schemes based on the major version number:
-
-- **v1: Versions < 2017.0.0**: Use the `major.minor` format (e.g., `27.0`, `1592.6`)
-- **v2: Versions ≥ 2017.0.0**: Use the `major.minor.patch` format (e.g., `2017.0.0`, `2222.1.5`)
-
-### Major releases
-
-[Major releases](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#major-releases) are major releases that are supported over an extended period of time.
-
-#### Schema fields
-
-- **`name`**: A string representing the release name (e.g., `major-1312`).
-- **`type`**: `major`.
-- **`version`**:
- - **`major`**: An integer indicating the major version number (e.g. `1312`).
-- **`lifecycle`**:
- - **`released`**:
- - **`isodate`**: The release date in ISO format (`YYYY-MM-DD`).
- - **`timestamp`**: The UNIX timestamp of the release date.
- - **`extended`**:
- - **`isodate`**: Optional extended maintenance date in ISO format.
- - **`timestamp`**: Optional UNIX timestamp for the extended maintenance date.
- - **`eol`**:
- - **`isodate`**: End-of-life date in ISO format.
- - **`timestamp`**: UNIX timestamp for the end-of-life date.
-
-### Minor releases
-
-[Minor Releases](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#minores) are updates delivered during the standard and extended mainteance periods of [Major releases](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#major-releases).
-
-#### Schema fields
-
-- **`name`**: A string representing the release name (e.g., `minor-1312.1` for v1 versions, `minor-2017.0.0` for v2 versions).
-- **`type`**: `minor`.
-- **`version`**:
- - **`major`**: An integer indicating the major version number (e.g. `1312`).
- - **`minor`**: An integer indicating the minor version number (e.g. `1`).
- - **`patch`**: An integer indicating the patch version number (only present for versions ≥ 2017.0.0, e.g. `0`).
-- **`lifecycle`**:
- - **`released`**:
- - **`isodate`**: The release date in ISO format.
- - **`timestamp`**: The UNIX timestamp of the release date.
- - **`eol`**:
- - **`isodate`**: End-of-life date in ISO format.
- - **`timestamp`**: UNIX timestamp for the end-of-life date.
-- **`git`**:
- - **`commit`**: The full git commit hash associated with the release.
- - **`commit_short`**: The short git commit hash (first 7 characters).
-- **`github`**:
- - **`release`**: The URL to the GitHub release page.
-- **`flavors`**: A list of flavors that are included in this release. `glrd` creates `metadata`, `image` and/or `oci` links for each flavor.
-- **`attributes`**: An object that does contain additional metadata about the release.
- - **`source_repo`**: A boolean indicating whether the release has debian source repoitories (default: true).
-
-### Nightly releases
-
-[Nightly releases](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#nightly-releases) are automated builds that are generated every night, reflecting the latest state of the codebase.
-
-#### Schema fields
-
-- **`name`**: A string representing the release name (e.g., `nightly-1312.0` for v1 versions, `nightly-2017.0.0` for v2 versions).
-- **`type`**: `nightly`.
-- **`version`**:
- - **`major`**: An integer indicating the major version number.
- - **`minor`**: An integer indicating the minor version number.
- - **`patch`**: An integer indicating the patch version number (only present for versions ≥ 2017.0.0, e.g. `0`).
-- **`lifecycle`**:
- - **`released`**:
- - **`isodate`**: The release date in ISO format.
- - **`timestamp`**: The UNIX timestamp of the release date.
-- **`git`**:
- - **`commit`**: The full git commit hash associated with the release.
- - **`commit_short`**: The short git commit hash.
-- **`flavors`**: A list of flavors that are included in this release. `glrd` creates `metadata`, `image` and/or `oci` links for each flavor.
-- **`attributes`**: An object that does contain additional metadata about the release.
- - **`source_repo`**: A boolean indicating whether the release has debian source repoitories (default: true).
-
-### Development releases
-
-[Development releases](TODO: define and link) are used for testing and development purposes, representing the latest changes that may not yet be included in a major or minor release. These can be manually created by developers.
-
-#### Schema fields
-
-- **`name`**: A string representing the release name (e.g., `dev-1312.0` for v1 versions, `dev-2017.0.0` for v2 versions).
-- **`type`**: `dev`.
-- **`version`**:
- - **`major`**: An integer indicating the major version number.
- - **`minor`**: An integer indicating the minor version number.
- - **`patch`**: An integer indicating the patch version number (only present for versions ≥ 2017.0.0, e.g. `0`).
-- **`lifecycle`**:
- - **`released`**:
- - **`isodate`**: The release date in ISO format.
- - **`timestamp`**: The UNIX timestamp of the release date.
-- **`git`**:
- - **`commit`**: The full git commit hash associated with the release.
- - **`commit_short`**: The short git commit hash.
-- **`flavors`**: A list of flavors that are included in this release. `glrd` creates `metadata`, `image` and/or `oci` links for each flavor.
-- **`attributes`**: An object that does contain additional metadata about the release.
- - **`source_repo`**: A boolean indicating whether the release has debian source repoitories (default: true).
-
-### Next release
-
-[Next release](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#next-release) is the projected next major releases. There can only be a single `next` release.
-
-#### Schema fields
-
-- **`name`**: A string representing the release name (`next`).
-- **`type`**: `next`.
-- **`version`**:
- - **`major`**: `next`.
-- **`lifecycle`**:
- - **`released`**:
- - **`isodate`**: The release date in ISO format (`YYYY-MM-DD`).
- - **`timestamp`**: The UNIX timestamp of the release date.
- - **`extended`**:
- - **`isodate`**: Optional extended maintenance date in ISO format.
- - **`timestamp`**: Optional UNIX timestamp for the extended maintenance date.
- - **`eol`**:
- - **`isodate`**: End-of-life date in ISO format.
- - **`timestamp`**: UNIX timestamp for the end-of-life date.
-
-### Lifecycle dependencies
-
-The lifecycle fields in the release schemas help track the release dates, extended maintenance dates and end-of-life (EOL) dates for each release.
-
-#### Default major dates
-
-The defaults for `extended` and `eol` dates are based on the [Garden Linux Release Plan Overview](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md) and defined to be:
-
-- `extended`
- - `released` + 6 month
-- `eol`
- - `released` + 9 month
-
-For example:
-
-- **Major Release `major-1443`**:
- - `released`: 2024-03-13
- - `extended`: 2024-09-13
- - `eol`: 2025-01-13
-
-#### Major and minor releases
-
-There is a dependency between the `lifecycle` fields of **major** and **minor** releases:
-
-- **EOL of Minor Releases**: The `eol` date of a minor release is set to the `released` date of the next minor release. If there is no subsequent minor release, the `eol` date is aligned with the `eol` date of the corresponding major release.
-- **EOL of Latest Minor Release**: The latest minor release's `eol` date matches the `eol` date of the major release.
-- **EOL of Major Releases**: The `eol` date for a major release marks the end of support for that major version.
-
-This ensures that all minor updates within a major release adhere to the same overall support timeline.
-
-For example:
-
-- **Major Release `major-1312`**:
- - `released`: 2023-11-16
- - `extended`: 2024-05-03
- - `eol`: 2024-08-03
-- **Minor Release `minor-1312.1`**:
- - `released`: 2023-11-23
- - `eol`: 2024-01-15 (next minor release date)
-- **Minor Release `minor-1312.2`**:
- - `released`: 2024-01-15
- - `eol`: 2024-02-14 (next minor release date)
-- ...
-- **Minor Release `minor-1312.7`**:
- - `released`: 2024-07-03
- - `eol`: 2024-08-03 (inherits from major release eol)
-
-In this example, the `eol` of `minor-1312.1` is set to the `released` date of `minor-1312.2`, and the `eol` of the latest minor release (`minor-1312.7`) is set to the `eol` of the major release (`minor-1312`).
-
-Note that the `extended` lifecycle field is not taken into account for minor releases. This is simply an administrative date that has no technical implications.
-
-#### Nightly and dev dates
-
-Due to the nature of the `nightly` and `dev` releases, those do not have `extended` and `eol` dates.
-
-## Contributing
-
-Contributions are welcome! Open an issue or submit a pull request for any improvements or bug fixes.
-
-## License
-
-This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
-
-## Funding information
-
-
diff --git a/docs/overview/assets/glrd-overview.png b/docs/overview/assets/glrd-overview.png
deleted file mode 100644
index 55a3646..0000000
Binary files a/docs/overview/assets/glrd-overview.png and /dev/null differ
diff --git a/docs/overview/assets/glrd-overview.excalidraw b/docs/reference/assets/glrd-overview.excalidraw
similarity index 89%
rename from docs/overview/assets/glrd-overview.excalidraw
rename to docs/reference/assets/glrd-overview.excalidraw
index 0ec7f37..1ce90f7 100644
--- a/docs/overview/assets/glrd-overview.excalidraw
+++ b/docs/reference/assets/glrd-overview.excalidraw
@@ -288,16 +288,20 @@
"link": null,
"locked": false,
"startBinding": {
+ "mode": "orbit",
"elementId": "oGp3gDIowEf0ERjU64X7W",
- "focus": 0.000018815675508124198,
- "gap": 1,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.5001,
+ 1
+ ]
},
"endBinding": {
+ "mode": "orbit",
"elementId": "vgiASgTEJV_fZJ3b-wYQS",
- "focus": 0.000027328006585552104,
- "gap": 5.2999999999999545,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.5001,
+ 0
+ ]
},
"lastCommittedPoint": null,
"startArrowhead": null,
@@ -353,16 +357,20 @@
"link": null,
"locked": false,
"startBinding": {
+ "mode": "orbit",
"elementId": "vgiASgTEJV_fZJ3b-wYQS",
- "focus": -0.39821509355663853,
- "gap": 1,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.6991075467783207,
+ 0.6991075467783244
+ ]
},
"endBinding": {
+ "mode": "orbit",
"elementId": "Ja2YwuSfTNbOWarmcKJvD",
- "focus": 0.7085122328658864,
- "gap": 4.193621663720023,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.9999999999999999,
+ 0.5001
+ ]
},
"lastCommittedPoint": null,
"startArrowhead": null,
@@ -418,16 +426,20 @@
"link": null,
"locked": false,
"startBinding": {
+ "mode": "orbit",
"elementId": "NvPxjYidT47CfP8AvmSkA",
- "focus": -0.0000019013873514802402,
- "gap": 1,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.5001,
+ 1
+ ]
},
"endBinding": {
+ "mode": "orbit",
"elementId": "QkW758QBmPZaI2wwGzswp",
- "focus": -0.000004947918725731112,
- "gap": 5.2999999999999545,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.5001,
+ 0
+ ]
},
"lastCommittedPoint": null,
"startArrowhead": null,
@@ -483,16 +495,20 @@
"link": null,
"locked": false,
"startBinding": {
+ "mode": "orbit",
"elementId": "QkW758QBmPZaI2wwGzswp",
- "focus": 0.000004947918725731112,
- "gap": 1,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.5001,
+ 1
+ ]
},
"endBinding": {
+ "mode": "orbit",
"elementId": "oGp3gDIowEf0ERjU64X7W",
- "focus": -0.000018815675508124198,
- "gap": 5.2999999999999545,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.5001,
+ 0
+ ]
},
"lastCommittedPoint": null,
"startArrowhead": null,
@@ -548,16 +564,20 @@
"link": null,
"locked": false,
"startBinding": {
+ "mode": "orbit",
"elementId": "8IV3nVg7OdJdXtZWZbeGT",
- "focus": -0.11130127747141697,
- "gap": 1.6353931620028561,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.5001,
+ 1
+ ]
},
"endBinding": {
+ "mode": "orbit",
"elementId": "QkW758QBmPZaI2wwGzswp",
- "focus": 0.8552810381755851,
- "gap": 9.952536539447237,
- "fixedPoint": null
+ "fixedPoint": [
+ 1.0000000000000009,
+ 0.5001
+ ]
},
"lastCommittedPoint": null,
"startArrowhead": null,
@@ -987,8 +1007,8 @@
},
{
"type": "text",
- "version": 675,
- "versionNonce": 796977407,
+ "version": 692,
+ "versionNonce": 562934999,
"index": "b1P",
"isDeleted": false,
"id": "B4GOUrfNKunbU2Teev9wH",
@@ -1002,8 +1022,8 @@
"y": 810.25,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
- "width": 696.4033749745981,
- "height": 720,
+ "width": 677.6009650316732,
+ "height": 920,
"seed": 876634341,
"groupIds": [],
"frameId": null,
@@ -1014,16 +1034,16 @@
"type": "arrow"
}
],
- "updated": 1729585783352,
+ "updated": 1784637372633,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 8,
- "text": "❯ glrd --type major --version 1312 --output-format yaml\n- name: major-1312\n type: major\n version:\n major: 1312\n lifecycle:\n released:\n isodate: '2023-11-16'\n timestamp: 1700136050\n extended:\n isodate: '2024-05-03'\n timestamp: 1714687200\n eol:\n isodate: '2024-08-03'\n timestamp: 1722636000\n\n❯ glrd --type minor --version 1312.0 --output-format yaml\n- name: minor-1312.0\n type: minor\n version:\n major: 1312\n minor: 0\n lifecycle:\n released:\n isodate: '2023-11-16'\n timestamp: 1700089200\n eol:\n isodate: '2023-11-23'\n timestamp: 1700694000\n git:\n commit: 40b9db2c54c9cac78bcf62a96f5386ac33c1bdab\n commit_short: 40b9db2\n github:\n release: https://github.com/gardenlinux/gardenlinux/releases/tag/1312.0\n\n",
+ "text": "❯ glrd --type major --version 2150 --output-format yaml\nreleases:\n- name: major-2150\n type: major\n version:\n major: 2150\n lifecycle:\n released:\n isodate: '2026-02-23'\n timestamp: 1771835792\n extended:\n isodate: '2026-08-23'\n timestamp: 1787474192\n eol:\n isodate: '2027-02-23'\n timestamp: 1803340800\n flavors: {}\n oci: ghcr.io/gardenlinux/gardenlinux:2150\n\n\n❯ glrd --type minor --version 2150.7.0 --output-format yaml\nreleases:\n- name: minor-2150.7.0\n type: minor\n version:\n major: 2150\n minor: 7\n patch: 0\n lifecycle:\n released:\n isodate: '2026-07-15'\n timestamp: 1784115456\n eol:\n isodate: '2027-02-23'\n timestamp: 1803340800\n git:\n commit: 5e66e8da34e90e9c257662f4a80142ebe11ca346\n commit_short: 5e66e8da\n github:\n release: https://github.com/gardenlinux/gardenlinux/releases/tag/2150.7.0\n flavors: {}\n oci: ghcr.io/gardenlinux/gardenlinux:2150.7.0\n attributes:\n source_repo: true\n\n",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
- "originalText": "❯ glrd --type major --version 1312 --output-format yaml\n- name: major-1312\n type: major\n version:\n major: 1312\n lifecycle:\n released:\n isodate: '2023-11-16'\n timestamp: 1700136050\n extended:\n isodate: '2024-05-03'\n timestamp: 1714687200\n eol:\n isodate: '2024-08-03'\n timestamp: 1722636000\n\n❯ glrd --type minor --version 1312.0 --output-format yaml\n- name: minor-1312.0\n type: minor\n version:\n major: 1312\n minor: 0\n lifecycle:\n released:\n isodate: '2023-11-16'\n timestamp: 1700089200\n eol:\n isodate: '2023-11-23'\n timestamp: 1700694000\n git:\n commit: 40b9db2c54c9cac78bcf62a96f5386ac33c1bdab\n commit_short: 40b9db2\n github:\n release: https://github.com/gardenlinux/gardenlinux/releases/tag/1312.0\n\n",
+ "originalText": "❯ glrd --type major --version 2150 --output-format yaml\nreleases:\n- name: major-2150\n type: major\n version:\n major: 2150\n lifecycle:\n released:\n isodate: '2026-02-23'\n timestamp: 1771835792\n extended:\n isodate: '2026-08-23'\n timestamp: 1787474192\n eol:\n isodate: '2027-02-23'\n timestamp: 1803340800\n flavors: {}\n oci: ghcr.io/gardenlinux/gardenlinux:2150\n\n\n❯ glrd --type minor --version 2150.7.0 --output-format yaml\nreleases:\n- name: minor-2150.7.0\n type: minor\n version:\n major: 2150\n minor: 7\n patch: 0\n lifecycle:\n released:\n isodate: '2026-07-15'\n timestamp: 1784115456\n eol:\n isodate: '2027-02-23'\n timestamp: 1803340800\n git:\n commit: 5e66e8da34e90e9c257662f4a80142ebe11ca346\n commit_short: 5e66e8da\n github:\n release: https://github.com/gardenlinux/gardenlinux/releases/tag/2150.7.0\n flavors: {}\n oci: ghcr.io/gardenlinux/gardenlinux:2150.7.0\n attributes:\n source_repo: true\n\n",
"autoResize": true,
"lineHeight": 1.25
},
@@ -1099,16 +1119,20 @@
"link": null,
"locked": false,
"startBinding": {
+ "mode": "inside",
"elementId": "vgiASgTEJV_fZJ3b-wYQS",
- "focus": -0.5250123551058682,
- "gap": 1,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.7427461602544223,
+ 0.9601372874197254
+ ]
},
"endBinding": {
+ "mode": "orbit",
"elementId": "6NPB-pPlaLq_OppZlAFUO",
- "focus": 0.6422555999580892,
- "gap": 3.8714285714288508,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.9999999999999999,
+ 0.5001
+ ]
},
"lastCommittedPoint": null,
"startArrowhead": null,
@@ -1275,16 +1299,20 @@
"link": null,
"locked": false,
"startBinding": {
+ "mode": "inside",
"elementId": "vgiASgTEJV_fZJ3b-wYQS",
- "focus": -0.7475494417564867,
- "gap": 1,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.8635590370696222,
+ 0.9974939683851858
+ ]
},
"endBinding": {
+ "mode": "orbit",
"elementId": "ApafzRm37lRl_3pKEtTI7",
- "focus": 0.6467335558946289,
- "gap": 4.193621663720251,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.9999999999999999,
+ 0.5001
+ ]
},
"lastCommittedPoint": null,
"startArrowhead": null,
@@ -1451,16 +1479,20 @@
"link": null,
"locked": false,
"startBinding": {
+ "mode": "inside",
"elementId": "vgiASgTEJV_fZJ3b-wYQS",
- "focus": -0.9571021220589234,
- "gap": 1,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.9797317112030263,
+ 0.955248138921785
+ ]
},
"endBinding": {
+ "mode": "orbit",
"elementId": "_qB7ZIHNTLwQK9IHTA-N5",
- "focus": 0.42417147654495535,
- "gap": 3.8714285714288508,
- "fixedPoint": null
+ "fixedPoint": [
+ 0.9999999999999999,
+ 0.5001
+ ]
},
"lastCommittedPoint": null,
"startArrowhead": null,
@@ -1557,8 +1589,8 @@
},
{
"type": "text",
- "version": 1126,
- "versionNonce": 89202097,
+ "version": 1159,
+ "versionNonce": 1622190775,
"index": "b1u",
"isDeleted": false,
"id": "rKCTpOb0-cHiOk_Nkdn_t",
@@ -1572,23 +1604,23 @@
"y": 805.9642857142856,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
- "width": 398.2014996387941,
+ "width": 413.5967720387391,
"height": 180,
"seed": 575922865,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
- "updated": 1729585810063,
+ "updated": 1784637329966,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 8,
- "text": "❯ glrd-manage --s3-update \\\n --create major \\\n --version 1312\nINFO: major-1312 - release will be created.\n\n❯ glrd-manage --s3-update \\\n --create minor \\\n --version 1312.0\nINFO: minor-1312.0 - release will be created.",
+ "text": "❯ glrd-manage --s3-update \\\n --create major \\\n --version 2150\nINFO: major-2150 - release will be created.\n\n❯ glrd-manage --s3-update \\\n --create minor \\\n --version 2150.7.0\nINFO: minor-2150.7.0 - release will be created.",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
- "originalText": "❯ glrd-manage --s3-update \\\n --create major \\\n --version 1312\nINFO: major-1312 - release will be created.\n\n❯ glrd-manage --s3-update \\\n --create minor \\\n --version 1312.0\nINFO: minor-1312.0 - release will be created.",
+ "originalText": "❯ glrd-manage --s3-update \\\n --create major \\\n --version 2150\nINFO: major-2150 - release will be created.\n\n❯ glrd-manage --s3-update \\\n --create minor \\\n --version 2150.7.0\nINFO: minor-2150.7.0 - release will be created.",
"autoResize": true,
"lineHeight": 1.25
}
@@ -1597,7 +1629,8 @@
"gridSize": 20,
"gridStep": 5,
"gridModeEnabled": false,
- "viewBackgroundColor": "#ffffff"
+ "viewBackgroundColor": "#ffffff",
+ "lockedMultiSelections": {}
},
"files": {}
}
\ No newline at end of file
diff --git a/docs/reference/assets/glrd-overview.svg b/docs/reference/assets/glrd-overview.svg
new file mode 100644
index 0000000..b1758d3
--- /dev/null
+++ b/docs/reference/assets/glrd-overview.svg
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/docs/reference/assets/glrd-overview_dark.svg b/docs/reference/assets/glrd-overview_dark.svg
new file mode 100644
index 0000000..db0d5b1
--- /dev/null
+++ b/docs/reference/assets/glrd-overview_dark.svg
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
new file mode 100644
index 0000000..6fd0f9c
--- /dev/null
+++ b/docs/reference/cli.md
@@ -0,0 +1,189 @@
+---
+title: "GLRD CLI Reference"
+description: "Complete flag and option reference for the glrd and glrd-manage command-line tools."
+github_org: gardenlinux
+github_repo: glrd
+github_source_path: docs/reference/cli.md
+github_target_path: docs/reference/supporting_tools/glrd/cli.md
+related_topics:
+ - /reference/supporting_tools/glrd/
+ - /reference/supporting_tools/glrd/cli.md
+ - /reference/supporting_tools/glrd/schema.md
+ - /how-to/glrd/run-glrd.md
+ - /how-to/glrd/query-releases.md
+ - /how-to/glrd-manage-releases.md
+ - /explanation/glrd-release-lifecycle.md
+---
+
+# GLRD CLI Reference
+
+This page documents all flags and options for the `glrd` and `glrd-manage` command-line tools.
+
+## `glrd`
+
+`glrd` queries the Garden Linux Release Database (GLRD) and outputs release information in various formats.
+
+### Usage
+
+```text
+usage: glrd [-h] [--input-format {yaml,json}]
+ [--input-file-prefix INPUT_FILE_PREFIX] [--input-type {file,url}]
+ [--input-url INPUT_URL] [--no-input-split]
+ [--output-format {json,yaml,markdown,mermaid_gantt,shell}]
+ [--output-description OUTPUT_DESCRIPTION] [--active] [--archived]
+ [--latest] [--type TYPE] [--version VERSION] [--fields FIELDS]
+ [--no-header] [-V]
+
+Process and filter releases data from a file or URL.
+```
+
+### Options
+
+| Flag | Default | Description |
+| ---------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `-h`, `--help` | — | Show the help message and exit. |
+| `--input-format {yaml,json}` | `json` | Input format for release data. |
+| `--input-file-prefix PREFIX` | `releases` | Prefix used to locate input files when `--input-type file` is set. |
+| `--input-type {file,url}` | `url` | Source of release data. Use `file` to read local files; use `url` to query the S3 endpoint. |
+| `--input-url URL` | gardenlinux-glrd S3 URL | Base URL for release data. Only used when `--input-type url`. |
+| `--no-input-split` | — | Read a single input file instead of separate `*-nightly` and `*-dev` files. |
+| `--output-format {json,yaml,markdown,mermaid_gantt,shell}` | `shell` | Output format for results. |
+| `--output-description TEXT` | `Garden Linux Releases` | Description string included in outputs that support it (for example, Mermaid Gantt chart titles). |
+| `--active` | — | Show only currently active releases (end-of-life date in the future). |
+| `--archived` | — | Show only archived releases (end-of-life date in the past). |
+| `--latest` | — | Show the single latest active `major.minor.patch` release. |
+| `--type TYPE` | `major,minor` | Comma-separated list of release types to include. Valid values: `major`, `minor`, `nightly`, `dev`, `next`. |
+| `--version VERSION` | — | Filter by a specific version. Accepts `major` (for example, `1312`), `major.minor` (for example, `1312.0`), or `major.minor.patch` (for example, `1312.0.0`). |
+| `--fields FIELDS` | see below | Comma-separated list of output fields. |
+| `--no-header` | — | Omit the column header row in shell output. |
+| `-V` | — | Show the program version number and exit. |
+
+### Default output fields
+
+When `--fields` is not specified, `glrd` outputs these columns:
+
+```text
+Name,Version,Type,GitCommitShort,ReleaseDate,ExtendedMaintenance,EndOfMaintenance
+```
+
+### Available output fields
+
+| Field name | Description |
+| ---------------------- | ---------------------------------------------------------- |
+| `Name` | Release name (for example, `minor-1592.6`). |
+| `Version` | Version string (for example, `1592.6`). |
+| `Type` | Release type (`major`, `minor`, `nightly`, `dev`, `next`). |
+| `GitCommit` | Full 40-character git commit SHA. |
+| `GitCommitShort` | Short git commit hash. |
+| `ReleaseDate` | Release date in `YYYY-MM-DD` format. |
+| `ReleaseTime` | Release Unix timestamp. |
+| `ExtendedMaintenance` | Extended maintenance start date, or `N/A`. |
+| `EndOfMaintenance` | End-of-life date. |
+| `Flavors` | Comma-separated list of build flavors. |
+| `OCI` | OCI container image reference. |
+| `AttributesSourceRepo` | Whether Debian source repositories are available. |
+
+---
+
+## `glrd-manage`
+
+`glrd-manage` creates, updates, and manages Garden Linux release entries in GLRD.
+
+### Usage
+
+```text
+usage: glrd-manage [-h] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
+ [--input-file INPUT_FILE] [--input-type {file,url}]
+ [--input-url INPUT_URL]
+ [--input-file-prefix INPUT_FILE_PREFIX]
+ [--query-input-format {yaml,json}]
+ [--output-format {yaml,json}]
+ [--output-file-prefix OUTPUT_FILE_PREFIX]
+ [--s3-bucket-name S3_BUCKET_NAME]
+ [--s3-bucket-region S3_BUCKET_REGION]
+ [--s3-bucket-prefix S3_BUCKET_PREFIX] [--delete DELETE]
+ [--update UPDATE]
+ [--create-initial-releases CREATE_INITIAL_RELEASES]
+ [--create CREATE] [--version VERSION] [--commit COMMIT]
+ [--no-flavors]
+ [--lifecycle-released-isodatetime LIFECYCLE_RELEASED_ISODATETIME]
+ [--lifecycle-extended-isodatetime LIFECYCLE_EXTENDED_ISODATETIME]
+ [--lifecycle-eol-isodatetime LIFECYCLE_EOL_ISODATETIME]
+ [--no-query] [--input-stdin] [--input] [--no-output-split]
+ [--s3-create-bucket] [--s3-update] [--output-all]
+ [--input-all] [-V]
+
+Manage Garden Linux releases data.
+```
+
+### Options
+
+#### General
+
+| Flag | Default | Description |
+| ------------------------------------------------- | ------- | ----------------------------------------- |
+| `-h`, `--help` | — | Show the help message and exit. |
+| `--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}` | — | Set the logging verbosity level. |
+| `-V` | — | Show the program version number and exit. |
+
+#### Input
+
+| Flag | Default | Description |
+| ---------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------- |
+| `--input-file FILE` | `releases-input.yaml` | Input file path for `--input` mode. |
+| `--input-type {file,url}` | `url` | Source for querying existing releases when `--no-query` is not set. Use `file` for fully offline operation. |
+| `--input-url URL` | gardenlinux-glrd S3 URL | Base URL for querying existing releases. Only used with `--input-type url`. |
+| `--input-file-prefix PREFIX` | `releases` | File prefix for querying existing releases from local files. Only used with `--input-type file`. |
+| `--query-input-format {yaml,json}` | `json` | Format of existing release data queried when not using `--no-query`. |
+| `--input-stdin` | — | Read a single release entry from standard input (JSON). |
+| `--input` | — | Read release data from `--input-file`. |
+| `--input-all` | — | Upload all local release files to S3. |
+
+#### Output
+
+| Flag | Default | Description |
+| ----------------------------- | ---------- | ----------------------------------------------------------------------------- |
+| `--output-format {yaml,json}` | `yaml` | Output format for generated release files. |
+| `--output-file-prefix PREFIX` | `releases` | Prefix for generated output files. |
+| `--no-output-split` | — | Write a single output file instead of separate `*-nightly` and `*-dev` files. |
+| `--output-all` | — | Download all release files from S3 to local disk. |
+
+#### S3 configuration
+
+| Flag | Default | Description |
+| --------------------------- | ------------------ | -------------------------------------------------------------------------------- |
+| `--s3-bucket-name NAME` | `gardenlinux-glrd` | Name of the Amazon S3 bucket. |
+| `--s3-bucket-region REGION` | `eu-central-1` | AWS region of the S3 bucket. |
+| `--s3-bucket-prefix PREFIX` | _(empty)_ | Object key prefix within the S3 bucket. |
+| `--s3-create-bucket` | — | Create the S3 bucket if it does not exist. |
+| `--s3-update` | — | Push generated files to S3. Without this flag, changes are written locally only. |
+
+#### Release actions
+
+| Flag | Format | Description |
+| -------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
+| `--create-initial-releases LIST` | `major,minor,nightly` | Fetch release history from GitHub and generate initial release data for the specified types. |
+| `--create TYPE` | `major\|minor\|nightly\|dev\|next` | Create a new release of the given type using the current timestamp and git state. |
+| `--update RELEASE` | `type-major`, `type-major.minor`, or `type-major.minor.patch` | Update lifecycle fields or commit hash on an existing release. Requires at least one modifier flag. |
+| `--delete RELEASE` | `type-major.minor` or `type-major.minor.patch` | Delete a release by name. Requires `--s3-update`. |
+
+#### Release modifiers (used with `--create` or `--update`)
+
+| Flag | Format | Description |
+| ------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `--version VERSION` | `major.minor` or `major.minor.patch` | Override the version. Use `major.minor` for versions < 2017.0.0; use `major.minor.patch` for versions ≥ 2017.0.0. |
+| `--commit HASH` | 40-character hex string | Override the git commit hash. Valid for `minor`, `nightly`, and `dev` types only. |
+| `--no-flavors` | — | Skip the Git clone and S3 lookup for flavor resolution. Useful for offline use and tests. Can also be set via the `GLRD_SKIP_FLAVORS` environment variable. |
+| `--lifecycle-released-isodatetime DATETIME` | `YYYY-MM-DDTHH:MM:SS` | Override the release date and time. |
+| `--lifecycle-extended-isodatetime DATETIME` | `YYYY-MM-DDTHH:MM:SS` | Override the extended maintenance date. Valid for `major` and `next` types only. |
+| `--lifecycle-eol-isodatetime DATETIME` | `YYYY-MM-DDTHH:MM:SS` | Override the end-of-life date. Valid for `next`, `major`, and `minor` types only. |
+
+#### Query control
+
+| Flag | Description |
+| ------------ | --------------------------------------------------------------------------------------------------------------------------- |
+| `--no-query` | Do not query existing releases before writing. Use with care — omitting the existing data can overwrite or delete releases. |
+
+## Related topics
+
+
diff --git a/docs/reference/index.md b/docs/reference/index.md
new file mode 100644
index 0000000..a14da8a
--- /dev/null
+++ b/docs/reference/index.md
@@ -0,0 +1,40 @@
+---
+title: "Garden Linux Release Database (GLRD)"
+description: "Overview of the Garden Linux Release Database (GLRD): a system for managing and querying Garden Linux release information."
+github_org: gardenlinux
+github_repo: glrd
+github_source_path: docs/reference/index.md
+github_target_path: docs/reference/supporting_tools/glrd/index.md
+related_topics:
+ - /reference/supporting_tools/glrd/
+ - /reference/supporting_tools/glrd/cli.md
+ - /reference/supporting_tools/glrd/schema.md
+ - /how-to/glrd/run-glrd.md
+ - /how-to/glrd/query-releases.md
+ - /how-to/glrd/manage-releases.md
+ - /explanation/glrd-release-lifecycle.md
+---
+
+# Garden Linux Release Database (GLRD)
+
+The [GLRD repository](https://github.com/gardenlinux/glrd) contains tooling and configuration to manage and query release information for the Garden Linux distribution.
+
+GLRD provides two command-line tools:
+
+- **`glrd-manage`**: Generates and populates the GLRD with initial release data and creates or updates individual release entries.
+- **`glrd`**: Queries the GLRD to retrieve release information based on various criteria.
+
+## Garden Linux Releases
+
+For a general overview of Garden Linux releases and their lifecycle, see the [Garden Linux Release Lifecycle](/reference/releases/release-lifecycle).
+
+## Overview
+
+The GLRD provides a structured way to store and access release data for Garden Linux, including release types such as major, minor, nightly, and development releases. It uses JSON and YAML formats to store release information and supports integration with Amazon Web Services (AWS) S3 for hosting release data.
+
+
+
+
+## Related topics
+
+
diff --git a/docs/reference/schema.md b/docs/reference/schema.md
new file mode 100644
index 0000000..1305201
--- /dev/null
+++ b/docs/reference/schema.md
@@ -0,0 +1,132 @@
+---
+title: "GLRD Release Schema"
+description: "JSON schema reference for Garden Linux Release Database (GLRD) release types: major, minor, nightly, dev, and next."
+github_org: gardenlinux
+github_repo: glrd
+github_source_path: docs/reference/schema.md
+github_target_path: docs/reference/supporting_tools/glrd/schema.md
+related_topics:
+ - /reference/supporting_tools/glrd/
+ - /reference/supporting_tools/glrd/cli.md
+ - /reference/supporting_tools/glrd/schema.md
+ - /how-to/glrd/run-glrd.md
+ - /how-to/glrd/query-releases.md
+ - /how-to/glrd/manage-releases.md
+ - /explanation/glrd-release-lifecycle.md
+---
+
+# GLRD Release Schema
+
+The Garden Linux Release Database (GLRD) uses structured JSON schemas to represent different types of releases. The five release types are: `major`, `minor`, `nightly`, `dev`, and `next`. Each type has specific required fields that capture version, lifecycle, and build information.
+
+## Versioning scheme
+
+Garden Linux [introduced semantic versioning](https://github.com/gardenlinux/gardenlinux/issues/3069) starting at major version `2017`. GLRD supports both versioning schemes based on the major version number:
+
+- **v1 — versions < 2017.0.0**: Use the `major.minor` format (for example, `27.0`, `1592.6`).
+- **v2 — versions ≥ 2017.0.0**: Use the `major.minor.patch` format (for example, `2017.0.0`, `2222.1.5`).
+
+The schema variant is selected automatically based on the `version.major` field value.
+
+## Major releases
+
+[Major releases](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#major-releases) are long-term supported releases.
+
+### Schema fields
+
+| Field | Type | Required | Description |
+| ------------------------------ | ------------- | -------- | ------------------------------------------------ |
+| `name` | string | yes | Release name, for example `major-1312`. |
+| `type` | `"major"` | yes | Release type discriminator. |
+| `version.major` | integer | yes | Major version number, for example `1312`. |
+| `lifecycle.released.isodate` | string (date) | yes | Release date in `YYYY-MM-DD` format. |
+| `lifecycle.released.timestamp` | integer | yes | Unix timestamp of the release date. |
+| `lifecycle.extended.isodate` | string (date) | yes | Extended maintenance start date. |
+| `lifecycle.extended.timestamp` | integer | yes | Unix timestamp of the extended maintenance date. |
+| `lifecycle.eol.isodate` | string (date) | yes | End-of-life date. |
+| `lifecycle.eol.timestamp` | integer | yes | Unix timestamp of the end-of-life date. |
+
+## Minor releases
+
+[Minor releases](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#minor-releases) are patch updates delivered during the standard and extended maintenance periods of a major release.
+
+### Schema fields
+
+| Field | Type | Required | Description |
+| ------------------------------ | ---------------- | -------- | ------------------------------------------------------------------------------- |
+| `name` | string | yes | Release name, for example `minor-1312.1` (v1) or `minor-2017.0.0` (v2). |
+| `type` | `"minor"` | yes | Release type discriminator. |
+| `version.major` | integer | yes | Major version number. |
+| `version.minor` | integer | yes | Minor version number. |
+| `version.patch` | integer | v2 only | Patch version number. Only present for versions ≥ 2017.0.0. |
+| `lifecycle.released.isodate` | string (date) | yes | Release date. |
+| `lifecycle.released.timestamp` | integer | yes | Unix timestamp of the release date. |
+| `lifecycle.eol.isodate` | string (date) | yes | End-of-life date. |
+| `lifecycle.eol.timestamp` | integer | yes | Unix timestamp of the end-of-life date. |
+| `git.commit` | string | yes | Full 40-character SHA-1 git commit hash. |
+| `git.commit_short` | string | yes | Short git commit hash (7–8 characters). |
+| `github.release` | URI string | yes | URL to the GitHub release page. |
+| `flavors` | array of strings | yes | List of build flavor identifiers included in the release. |
+| `attributes.source_repo` | boolean | yes | Whether the release has Debian source repositories available (default: `true`). |
+
+## Nightly releases
+
+[Nightly releases](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#nightly-releases) are automated builds generated every night from the latest state of the codebase.
+
+### Schema fields
+
+| Field | Type | Required | Description |
+| ------------------------------ | ---------------- | -------- | --------------------------------------------------------------------------- |
+| `name` | string | yes | Release name, for example `nightly-1312.0` (v1) or `nightly-2017.0.0` (v2). |
+| `type` | `"nightly"` | yes | Release type discriminator. |
+| `version.major` | integer | yes | Major version number. |
+| `version.minor` | integer | yes | Minor version number. |
+| `version.patch` | integer | v2 only | Patch version number. Only present for versions ≥ 2017.0.0. |
+| `lifecycle.released.isodate` | string (date) | yes | Build date. |
+| `lifecycle.released.timestamp` | integer | yes | Unix timestamp of the build date. |
+| `git.commit` | string | yes | Full 40-character SHA-1 git commit hash. |
+| `git.commit_short` | string | yes | Short git commit hash. |
+| `flavors` | array of strings | yes | List of build flavor identifiers. |
+| `attributes.source_repo` | boolean | yes | Whether Debian source repositories are available (default: `true`). |
+
+## Development releases
+
+Development releases are used for testing and development purposes. They represent the latest changes that may not yet be included in a major or minor release, and can be created manually by developers.
+
+### Schema fields
+
+| Field | Type | Required | Description |
+| ------------------------------ | ---------------- | -------- | ------------------------------------------------------------------- |
+| `name` | string | yes | Release name, for example `dev-1312.0` (v1) or `dev-2017.0.0` (v2). |
+| `type` | `"dev"` | yes | Release type discriminator. |
+| `version.major` | integer | yes | Major version number. |
+| `version.minor` | integer | yes | Minor version number. |
+| `version.patch` | integer | v2 only | Patch version number. Only present for versions ≥ 2017.0.0. |
+| `lifecycle.released.isodate` | string (date) | yes | Build date. |
+| `lifecycle.released.timestamp` | integer | yes | Unix timestamp of the build date. |
+| `git.commit` | string | yes | Full 40-character SHA-1 git commit hash. |
+| `git.commit_short` | string | yes | Short git commit hash. |
+| `flavors` | array of strings | yes | List of build flavor identifiers. |
+| `attributes.source_repo` | boolean | yes | Whether Debian source repositories are available (default: `true`). |
+
+## Next release
+
+The [next release](https://github.com/gardenlinux/gardenlinux/blob/main/docs/00_introduction/release.md#next-release) represents the projected upcoming major release. Only one `next` entry exists at any time.
+
+### Schema fields
+
+| Field | Type | Required | Description |
+| ------------------------------ | ------------- | -------- | ---------------------------------------------------------- |
+| `name` | `"next"` | yes | Always the literal string `next`. |
+| `type` | `"next"` | yes | Release type discriminator. |
+| `version.major` | `"next"` | yes | Always the literal string `next`. |
+| `lifecycle.released.isodate` | string (date) | yes | Projected release date. |
+| `lifecycle.released.timestamp` | integer | yes | Unix timestamp of the projected release date. |
+| `lifecycle.extended.isodate` | string (date) | yes | Projected extended maintenance start date. |
+| `lifecycle.extended.timestamp` | integer | yes | Unix timestamp of the projected extended maintenance date. |
+| `lifecycle.eol.isodate` | string (date) | yes | Projected end-of-life date. |
+| `lifecycle.eol.timestamp` | integer | yes | Unix timestamp of the projected end-of-life date. |
+
+## Related topics
+
+
diff --git a/glrd/git.py b/glrd/git.py
new file mode 100644
index 0000000..b80d1f6
--- /dev/null
+++ b/glrd/git.py
@@ -0,0 +1,431 @@
+"""
+Git operations for GLRD.
+
+This module provides functions for interacting with Git repositories
+and GitHub APIs to retrieve commit information and release data.
+
+Authentication note: GitHub API access (get_github_releases,
+get_git_commit_from_tag) requires the GITHUB_TOKEN environment variable to be
+set.
+"""
+
+import logging
+import shutil
+import sys
+import tempfile
+from datetime import datetime
+from typing import Optional, Tuple
+
+import pygit2
+import pytz
+
+from gardenlinux.git import Repository
+from gardenlinux.github import Client
+
+from glrd.util import DEFAULTS, ERROR_CODES, extract_version_data, isodate_to_timestamp
+
+# Global variable to cache the repository clone path
+_repo_clone_path: Optional[str] = None
+
+# Cached pygit2 Repository object (kept open while the clone is alive)
+_repo_instance: Optional[Repository] = None
+
+
+def get_github_releases() -> list:
+ """Fetch releases from the GitHub API via python-gardenlinux-lib (PyGithub).
+
+ Requires the GITHUB_TOKEN environment variable to be set.
+
+ Returns:
+ List of dicts with keys ``tag_name``, ``published_at``, ``html_url``
+ — the same shape previously returned by the ``gh`` CLI.
+ """
+ try:
+ client = Client()
+ except ValueError as exc:
+ logging.error(f"GitHub authentication error: {exc}")
+ sys.exit(ERROR_CODES["subprocess_output_error"])
+
+ try:
+ gh_repo = client.get_repo(
+ f"{DEFAULTS['GL_REPO_OWNER']}/{DEFAULTS['GL_REPO_NAME']}"
+ )
+ releases = []
+ for release in gh_repo.get_releases():
+ published_at = ""
+ if release.published_at is not None:
+ published_at = release.published_at.strftime("%Y-%m-%dT%H:%M:%SZ")
+ releases.append(
+ {
+ "tag_name": release.tag_name,
+ "published_at": published_at,
+ "html_url": release.html_url,
+ }
+ )
+ return releases
+ except Exception as exc:
+ logging.error(f"Error fetching GitHub releases: {exc}")
+ sys.exit(ERROR_CODES["subprocess_output_error"])
+
+
+def get_git_commit_from_tag(tag: str) -> Tuple[str, str]:
+ """Fetch the git commit hash for a given tag via PyGithub.
+
+ Handles both lightweight and annotated tags: for annotated tags the
+ function dereferences the tag object to the underlying commit SHA.
+
+ Requires the GITHUB_TOKEN environment variable to be set.
+
+ Args:
+ tag: Git tag name
+
+ Returns:
+ Tuple of (full_commit_sha, short_commit_sha)
+ """
+ try:
+ client = Client()
+ except ValueError as exc:
+ logging.error(f"GitHub authentication error: {exc}")
+ sys.exit(ERROR_CODES["subprocess_output_error"])
+
+ try:
+ gh_repo = client.get_repo(
+ f"{DEFAULTS['GL_REPO_OWNER']}/{DEFAULTS['GL_REPO_NAME']}"
+ )
+ ref = gh_repo.get_git_ref(f"tags/{tag}")
+ sha = ref.object.sha
+ obj_type = ref.object.type
+
+ # Annotated tags point to a tag object, not directly to a commit.
+ # Dereference to reach the actual commit SHA.
+ if obj_type == "tag":
+ tag_obj = gh_repo.get_git_tag(sha)
+ sha = tag_obj.object.sha
+ logging.debug(f"Tag {tag} is annotated; dereferencing to commit {sha}")
+
+ return sha, sha[:8]
+ except Exception as exc:
+ logging.error(f"Error fetching git commit for tag {tag}: {exc}")
+ sys.exit(ERROR_CODES["subprocess_output_error"])
+
+
+def get_git_commit_at_time(
+ date: str,
+ time: str = "06:00",
+ branch: str = "main",
+ remote_repo: str = DEFAULTS["GL_REPO_URL"],
+) -> Tuple[str, str]:
+ """Fetch the git commit that was current at a specific date and time.
+
+ Uses a module-level cached clone of the repository so that repeated calls
+ (e.g. when generating many nightly releases) only clone once. The clone
+ is performed via ``gardenlinux.git.Repository.checkout_repo`` (pygit2)
+ and commit history is walked with ``pygit2``.
+
+ Args:
+ date: Date string in YYYY-MM-DD format
+ time: Time string in HH:MM format (default: "06:00")
+ branch: Git branch to check (default: "main")
+ remote_repo: URL of the remote repository
+
+ Returns:
+ Tuple of (full_commit_sha, short_commit_sha)
+ """
+ global _repo_clone_path, _repo_instance
+
+ # Convert the input date and time to UTC epoch for comparison.
+ target_time = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M").astimezone(
+ pytz.timezone("UTC")
+ )
+ target_epoch = int(target_time.astimezone(pytz.UTC).timestamp())
+
+ # Clone once and reuse on subsequent calls.
+ if _repo_clone_path is None or _repo_instance is None:
+ temp_dir = tempfile.mkdtemp(prefix="glrd_temp_repo_")
+ logging.debug(f"Cloning {remote_repo} into {temp_dir}")
+ try:
+ _repo_instance = Repository.checkout_repo(
+ git_directory=temp_dir,
+ repo_url=remote_repo,
+ branch=branch,
+ )
+ _repo_clone_path = temp_dir
+ except Exception as exc:
+ _repo_instance = None
+ shutil.rmtree(temp_dir, ignore_errors=True)
+ logging.error(f"Error cloning remote repository: {exc}")
+ sys.exit(ERROR_CODES["subprocess_output_error"])
+
+ # Walk commits in time order and find the latest one at or before the
+ # target time.
+ try:
+ head_commit = _repo_instance.revparse_single(f"origin/{branch}")
+ walker = _repo_instance.walk(head_commit.id, pygit2.enums.SortMode.TIME)
+ commit = None
+ for c in walker:
+ if c.commit_time <= target_epoch:
+ commit = str(c.id)
+ break
+ except Exception as exc:
+ logging.error(f"Error walking git history for {date} at {time}: {exc}")
+ sys.exit(ERROR_CODES["subprocess_output_error"])
+
+ logging.debug(f"Found commit {commit} for {date} at {time}")
+
+ if not commit:
+ logging.error(f"No commit found for {date} at {time}")
+ sys.exit(ERROR_CODES["subprocess_output_missing"])
+
+ return commit, commit[:8]
+
+
+def cleanup_temp_repo() -> None:
+ """Clean up the temporary repository clone.
+
+ Dereferences the cached pygit2 Repository object before removing the
+ directory so that pygit2 releases all file handles first.
+
+ This function can be registered as an atexit handler.
+ """
+ global _repo_clone_path, _repo_instance
+
+ # Dereference the pygit2 object before rmtree so file handles are released.
+ if _repo_instance is not None:
+ _repo_instance = None
+
+ if _repo_clone_path:
+ shutil.rmtree(_repo_clone_path, ignore_errors=True)
+ _repo_clone_path = None
+
+
+def get_garden_version_for_date(
+ release_type: str, date: datetime, existing_releases: list
+) -> Tuple[int, int, int]:
+ """
+ Create major.minor.patch version based on the Garden Linux base_date.
+ Logic is taken from `gardenlinux/bin/garden-version`.
+
+ Major: days since base date.
+ Minor: Next available minor version based on existing releases.
+ Patch: Next available patch version based on existing releases.
+
+ Args:
+ release_type: Type of release
+ date: Release date
+ existing_releases: List of existing release dicts
+
+ Returns:
+ Tuple of (major, minor, patch)
+ """
+ # Calculate major version
+ base_date = datetime(2020, 3, 31, tzinfo=pytz.UTC)
+ major = (date - base_date).days
+
+ if release_type == "next":
+ minor = 0
+ patch = 0
+ elif release_type == "major":
+ minor = 0
+ patch = 0
+ else:
+ # Collect existing minor versions for the given major version and
+ # release type
+ existing_minor_versions = [
+ release["version"].get("minor", -1)
+ for release in existing_releases
+ if (
+ release["type"] == release_type and release["version"]["major"] == major
+ )
+ ]
+
+ # For patch versions, we need to find the latest minor first
+ if existing_minor_versions:
+ minor = max(existing_minor_versions)
+ else:
+ minor = 0
+
+ existing_patch_versions = [
+ release["version"].get("patch", -1)
+ for release in existing_releases
+ if (
+ release["type"] == release_type
+ and release["version"]["major"] == major
+ and release["version"].get("minor") == minor
+ )
+ ]
+
+ logging.debug(
+ f"Existing patch versions for major {major} and minor "
+ f"{minor}: {existing_patch_versions}"
+ )
+
+ if existing_minor_versions:
+ # Increment minor if we have existing minor versions
+ minor = max(existing_minor_versions) + 1
+ else:
+ minor = 0
+
+ if existing_patch_versions:
+ patch = max(existing_patch_versions) + 1
+ else:
+ patch = 0
+
+ logging.debug(
+ f"New {release_type} version for {date} is " f"{major}.{minor}.{patch}"
+ )
+
+ return major, minor, patch
+
+
+def create_initial_releases(releases: list) -> Tuple[list, list, dict, dict]:
+ """
+ Generate initial major and minor releases from GitHub releases.
+
+ Args:
+ releases: List of GitHub release objects
+
+ Returns:
+ Tuple of (release_data_major, release_data_minor,
+ latest_minor_versions, latest_patch_versions)
+ """
+ release_data_major = []
+ release_data_minor = []
+ latest_minor_versions = {}
+ latest_patch_versions = {}
+
+ releases.sort(key=lambda r: extract_version_data(r["tag_name"]))
+
+ for release in releases:
+ tag_name = release.get("tag_name")
+ major, minor, patch = extract_version_data(tag_name)
+ if major is None:
+ continue
+
+ # Determine release type: "minor" if minor exists, otherwise "major"
+ release_type = "minor" if minor is not None and patch is not None else "major"
+
+ release_info = {
+ "name": f"{release_type}-{tag_name}",
+ "type": release_type,
+ "version": {"major": major},
+ "lifecycle": {
+ "released": {
+ "isodate": release["published_at"][:10],
+ "timestamp": isodate_to_timestamp(release["published_at"]),
+ },
+ "eol": {"isodate": None, "timestamp": None},
+ },
+ }
+ if release_type == "major":
+ release_data_major.append(release_info)
+ logging.debug(f"Initial major release '{release_info['name']}' created.")
+ else:
+ # For minor releases, add git and github data
+ if release_type == "minor":
+ commit, commit_short = get_git_commit_from_tag(tag_name)
+ release_info["version"]["minor"] = minor
+ release_info["version"]["patch"] = patch
+ release_info["git"] = {
+ "commit": commit,
+ "commit_short": commit_short,
+ }
+ release_info["github"] = {"release": release["html_url"]}
+ release_data_minor.append(release_info)
+ logging.debug(
+ f"Initial minor release '{release_info['name']}' created."
+ )
+
+ if major not in latest_minor_versions or (
+ (minor is not None and minor > latest_minor_versions[major]["minor"])
+ and (patch is not None and patch > latest_minor_versions[major]["patch"])
+ ):
+ latest_minor_versions[major] = {
+ "index": len(
+ release_data_minor
+ if release_type == "minor"
+ else release_data_major
+ )
+ - 1,
+ "minor": minor,
+ "patch": patch,
+ }
+
+ return (
+ release_data_major,
+ release_data_minor,
+ latest_minor_versions,
+ latest_patch_versions,
+ )
+
+
+def create_initial_nightly_releases(major_releases: list) -> list:
+ """
+ Create initial nightly releases based on major releases.
+
+ Args:
+ major_releases: List of major release dicts
+
+ Returns:
+ List of nightly release dicts
+ """
+ from datetime import timedelta
+
+ nightly_releases = []
+
+ # Sort major releases by released timestamp
+ if not major_releases:
+ return nightly_releases
+
+ sorted_major_releases = sorted(
+ major_releases,
+ key=lambda r: r["lifecycle"]["released"]["timestamp"],
+ )
+
+ # Use the first major release's timestamp to determine the start date
+ first_major_release = sorted_major_releases[0]
+ start_date = datetime.fromtimestamp(
+ first_major_release["lifecycle"]["released"]["timestamp"], pytz.UTC
+ ).replace(hour=7, minute=0, second=0, microsecond=0, tzinfo=pytz.UTC)
+
+ # Calculate the number of nightly releases to create (one per day)
+ for major_release in sorted_major_releases:
+ current_date = start_date
+
+ # Create nightly releases until the next major release
+ while current_date < datetime.fromtimestamp(
+ major_release["lifecycle"]["released"]["timestamp"], pytz.UTC
+ ):
+ major, minor, patch = get_garden_version_for_date(
+ "nightly", current_date, nightly_releases
+ )
+ commit, commit_short = get_git_commit_at_time(
+ current_date.strftime("%Y-%m-%d")
+ )
+
+ release = {
+ "name": (
+ f"nightly-{major}.{minor}.{patch}"
+ if major >= 2017
+ else f"nightly-{major}.{minor}"
+ ),
+ "type": "nightly",
+ "version": {"major": major, "minor": minor},
+ "lifecycle": {
+ "released": {
+ "isodate": current_date.strftime("%Y-%m-%d"),
+ "timestamp": int(current_date.timestamp()),
+ }
+ },
+ "git": {"commit": commit, "commit_short": commit_short},
+ }
+
+ # Add patch field for v2 schema (>= 2017)
+ if major >= 2017:
+ release["version"]["patch"] = patch
+
+ nightly_releases.append(release)
+ logging.debug(f"Nightly release '{release['name']}' created.")
+ # Move to next day
+ current_date = current_date + timedelta(days=1)
+
+ return nightly_releases
diff --git a/glrd/manage.py b/glrd/manage.py
index 9491b3a..8385745 100755
--- a/glrd/manage.py
+++ b/glrd/manage.py
@@ -1,143 +1,69 @@
import argparse
-import fnmatch
import json
import logging
import os
-import shutil
-import subprocess
import sys
-import tempfile
-from datetime import datetime, timedelta
+from datetime import datetime
import boto3
import pytz
import yaml
-from botocore.exceptions import ClientError
from dateutil.relativedelta import relativedelta
-from deepdiff import DeepDiff
-from jsonschema import validate, ValidationError
-
-from glrd.schema_v1 import SCHEMA_V1
-from glrd.schema_v2 import SCHEMA_V2
-
+from deepdiff.diff import DeepDiff
+
+from glrd.git import (
+ get_github_releases,
+ get_git_commit_at_time,
+ get_garden_version_for_date,
+ create_initial_releases,
+ create_initial_nightly_releases,
+)
from glrd.query import load_all_releases
+from glrd.release import (
+ GitInfo,
+ Lifecycle,
+ LifecyclePhase,
+ Release,
+ ReleaseType,
+ Version,
+ parse_release_name as model_parse_release_name,
+)
+from glrd.s3 import (
+ create_s3_bucket,
+ download_all_s3_files,
+ merge_existing_s3_data,
+ save_output_file,
+ upload_all_local_files,
+ upload_to_s3,
+)
+from glrd.validation import validate_input_version_format, validate_all_releases
from glrd.util import (
DEFAULTS,
ERROR_CODES,
- extract_version_data,
- isodate_to_timestamp,
- timestamp_to_isodate,
get_version,
- NoAliasDumper,
-)
-from python_gardenlinux_lib.flavors.parse_flavors import (
- parse_flavors_commit,
+ merge_input_data,
+ resolve_flavors,
+ split_releases_by_type,
)
-from python_gardenlinux_lib.s3.s3 import get_s3_artifacts, get_s3_client
# silence boto3 logging
boto3.set_stream_logger(name="botocore.credentials", level=logging.ERROR)
-def validate_input_version_format(version, release_type):
- """
- Validate that the input version format matches schema requirements.
-
- Args:
- version: Version from user input
-
- Returns:
- tuple: (is_valid, error_message)
- """
-
- version_parts = version.split(".")
-
- if release_type in ["major", "next"]:
- # major and next don't use major.minor.patch
- return True, None
-
- if len(version_parts) == 2:
- major = int(version_parts[0])
- # Check if this version requires v2 schema (with patch field)
- if major >= 2017:
- return (
- False,
- f"Version {'.'.join(version_parts)} requires v2 schema "
- f"but missing patch version. Use format: major.minor.patch",
- )
- return True, None
- elif len(version_parts) == 3:
- major = int(version_parts[0])
- # Check if this version should use v1 schema (without patch field)
- if major < 2017:
- return (
- False,
- f"Version {'.'.join(version_parts)} uses v1 schema but "
- f"includes patch version. Use format: major.minor",
- )
- return True, None
- else:
- return (
- False,
- "Invalid version format. Expected major.minor or " "major.minor.patch",
- )
-
-
-def get_schema_for_release(release):
- """
- Get the appropriate schema version for a release based on its version number.
-
- Args:
- release: Release dictionary containing type and version information
-
- Returns:
- Schema dictionary appropriate for the release version
- """
- release_type = release.get("type")
- version = release.get("version", {})
-
- # For major and next releases, always use v2 schema
- # (they don't have major.minor.patch version numbers >= 2017)
- if release_type in ["major", "next"]:
- return SCHEMA_V2[release_type]
-
- # For minor, nightly, and dev releases, determine schema version based
- # on version number
- if release_type in ["minor", "nightly", "dev"]:
- major = version.get("major", 0)
- # minor = version.get("minor", 0) # unused
- # patch = version.get("patch", 0) # unused
-
- # Use v2 schema (with patch field) for versions >= 2017.0.0
- if major >= 2017:
- return SCHEMA_V2[release_type]
- else:
- return SCHEMA_V1[release_type]
-
- return None
-
-
# Global variable to store the path of the cloned gardenlinux repository (cached)
repo_clone_path = None
-def cleanup_temp_repo():
- """Cleanup function to delete the temporary directory at the end of the
- script."""
- # global repo_clone_path # unused
- if repo_clone_path and os.path.exists(repo_clone_path):
- shutil.rmtree(repo_clone_path)
-
-
def glrd_query_type(args, release_type):
"""Retrieve releases of a specific type."""
try:
releases = load_all_releases(
release_type,
- DEFAULTS["QUERY_INPUT_TYPE"],
- DEFAULTS["QUERY_INPUT_URL"],
- DEFAULTS["QUERY_INPUT_FILE_PREFIX"],
- DEFAULTS["QUERY_INPUT_FORMAT"],
+ getattr(args, "input_type", None) or DEFAULTS["QUERY_INPUT_TYPE"],
+ getattr(args, "input_url", None) or DEFAULTS["QUERY_INPUT_URL"],
+ getattr(args, "input_file_prefix", None)
+ or DEFAULTS["QUERY_INPUT_FILE_PREFIX"],
+ getattr(args, "query_input_format", None) or DEFAULTS["QUERY_INPUT_FORMAT"],
)
if not releases:
logging.warning(
@@ -154,356 +80,30 @@ def glrd_query_type(args, release_type):
return []
-def get_github_releases():
- """Fetch releases from the GitHub API using the 'gh' command."""
- command = [
- "gh",
- "api",
- "--paginate",
- f"/repos/{DEFAULTS['GL_REPO_OWNER']}/" f"{DEFAULTS['GL_REPO_NAME']}/releases",
- ]
- result = subprocess.run(
- command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
- )
- if result.returncode != 0:
- logging.error(f"Error fetching GitHub releases: {result.stderr}")
- sys.exit(ERROR_CODES["subprocess_output_error"])
- return json.loads(result.stdout)
-
-
-def get_git_commit_from_tag(tag):
- """
- Fetch the git commit hash for a given tag using the GitHub API.
- """
- try:
- # Use the GitHub API to get the commit hash from the tag name
- command = [
- "gh",
- "api",
- f"/repos/{DEFAULTS['GL_REPO_OWNER']}/"
- f"{DEFAULTS['GL_REPO_NAME']}/git/refs/tags/{tag}",
- "--jq",
- ".object.sha",
- ]
- result = subprocess.run(
- command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
- )
-
- if result.returncode != 0:
- logging.error(f"Error fetching git commit for tag: {tag}, {result.stderr}")
- sys.exit(ERROR_CODES["subprocess_output_missing"])
-
- commit = result.stdout.strip()
- return (
- commit,
- commit[:8],
- ) # Return full commit and shortened version
- except Exception as e:
- logging.error(f"Error fetching git commit for tag {tag}: {e}")
- sys.exit(ERROR_CODES["subprocess_output_error"])
-
-
def ensure_isodate_and_timestamp(lifecycle):
"""
Ensure both isodate and timestamp are set for all lifecycle fields
(released, extended, eol). If only one is present, the other is
computed.
- """
- for key in ["released", "extended", "eol"]:
- if key in lifecycle and lifecycle[key]:
- entry = lifecycle[key]
- # Ensure if 'isodate' exists, 'timestamp' is computed
- if "isodate" in entry and entry["isodate"] and not entry.get("timestamp"):
- entry["timestamp"] = isodate_to_timestamp(entry["isodate"])
- # Ensure if 'timestamp' exists, 'isodate' is computed
- elif (
- "timestamp" in entry and entry["timestamp"] and not entry.get("isodate")
- ):
- entry["isodate"] = timestamp_to_isodate(entry["timestamp"])
-
-
-def get_git_commit_at_time(
- date, time="06:00", branch="main", remote_repo=DEFAULTS["GL_REPO_URL"]
-):
- """Fetch the git commit that was at a specific date and time in the
- main branch, using a temporary cached git clone."""
- global repo_clone_path
-
- # Convert the input date and time to the target timezone (UTC) and
- # then to UTC
- target_time = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M").astimezone(
- pytz.timezone("UTC")
- )
- target_time_utc = target_time.astimezone(pytz.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
-
- # If the repository hasn't been cloned yet, clone it to a dynamically
- # created temp directory
- if not repo_clone_path:
- # Create a temporary directory for cloning the repository
- temp_dir = tempfile.mkdtemp(prefix="glrd_temp_repo_")
-
- # Perform the shallow clone
- clone_command = [
- "git",
- "clone",
- "--depth",
- "1",
- "--branch",
- branch,
- remote_repo,
- temp_dir,
- ]
- clone_result = subprocess.run(
- clone_command,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- )
-
- if clone_result.returncode != 0:
- logging.error(f"Error cloning remote repository: {clone_result.stderr}")
- sys.exit(ERROR_CODES["subprocess_output_error"])
-
- # Fetch full history to enable searching through commits by time
- fetch_command = ["git", "fetch", "--unshallow"]
- fetch_result = subprocess.run(
- fetch_command,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- cwd=temp_dir,
- )
-
- if fetch_result.returncode != 0:
- logging.error(
- f"Error fetching full history from remote repository: {fetch_result.stderr}"
- )
- sys.exit(ERROR_CODES["subprocess_output_error"])
-
- # Cache the clone path to reuse it later
- repo_clone_path = temp_dir
-
- # Use `git rev-list` to find the commit before the specified time
- rev_list_command = [
- "git",
- "rev-list",
- "-n",
- "1",
- "--before",
- target_time_utc,
- branch,
- ]
- rev_list_result = subprocess.run(
- rev_list_command,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- cwd=repo_clone_path,
- )
-
- if rev_list_result.returncode != 0:
- logging.error(
- f"Error fetching git commit for {date} at {time}: {rev_list_result.stderr}"
- )
- sys.exit(ERROR_CODES["subprocess_output_error"])
-
- commit = rev_list_result.stdout.strip()
-
- # Example of a debug message
- logging.debug(f"Found commit {commit} for {date} at {time}")
- if not commit:
- logging.error(f"No commit found for {date} at {time}")
- sys.exit(ERROR_CODES["subprocess_output_missing"])
-
- return commit, commit[:8]
-
-
-def get_garden_version_for_date(release_type, date, existing_releases):
- """
- Create major.minor.patch version based on the Garden Linux base_date.
- Logic is taken from `gardenlinux/bin/garden-version`.
-
- Major: days since base date.
- Minor: Next available minor version based on existing releases.
- Patch: Next available patch version based on existing releases.
+ This operates on the raw lifecycle dict (the JSON boundary) but delegates
+ the isodate<->timestamp conversion to the release model's
+ ``LifecyclePhase.ensure_complete`` so there is a single source of truth for
+ that logic.
"""
- # Calculate major version
- base_date = datetime(2020, 3, 31, tzinfo=pytz.UTC)
- major = (date - base_date).days
-
- if release_type == "next":
- minor = 0
- patch = 0
- elif release_type == "major":
- minor = 0
- patch = 0
- else:
- # Collect existing minor versions for the given major version and
- # release type
- existing_minor_versions = [
- release["version"].get("minor", -1)
- for release in existing_releases
- if (
- release["type"] == release_type and release["version"]["major"] == major
- )
- ]
- existing_patch_versions = [
- release["version"].get("patch", -1)
- for release in existing_releases
- if (
- release["type"] == release_type
- and release["version"]["major"] == major
- and release["version"]["minor"] == minor
- )
- ]
-
- logging.debug(
- f"Existing patch versions for major {major} and minor "
- f"{minor}: {existing_patch_versions}"
- )
-
- if existing_minor_versions:
- minor = max(existing_minor_versions) + 1
- else:
- minor = 0
-
- if existing_patch_versions:
- patch = max(existing_patch_versions) + 1
- else:
- patch = 0
-
- logging.debug(
- f"New {release_type} version for {date} is " f"{major}.{minor}.{patch}"
- )
-
- return major, minor, patch
-
-
-def create_initial_releases(releases):
- """Generate initial major and minor releases."""
- release_data_major = []
- release_data_minor = []
- latest_minor_versions = {}
- latest_patch_versions = {}
-
- releases.sort(key=lambda r: extract_version_data(r["tag_name"]))
-
- for release in releases:
- tag_name = release.get("tag_name")
- major, minor, patch = extract_version_data(tag_name)
- if major is None:
+ for key in ["released", "extended", "eol"]:
+ entry = lifecycle.get(key)
+ if not entry:
continue
-
- # Determine release type: "minor" if minor exists, otherwise "major"
- release_type = "minor" if minor is not None and patch is not None else "major"
-
- release_info = {
- "name": f"{release_type}-{tag_name}",
- "type": release_type,
- "version": {"major": major},
- "lifecycle": {
- "released": {
- "isodate": release["published_at"][:10],
- "timestamp": isodate_to_timestamp(release["published_at"]),
- },
- "eol": {"isodate": None, "timestamp": None},
- },
- }
- if release_type == "major":
- release_data_major.append(release_info)
- logging.debug(f"Initial major release '{release_info['name']}' created.")
- else:
- # For minor releases, add git and github data
- if release_type == "minor":
- commit, commit_short = get_git_commit_from_tag(tag_name)
- release_info["version"]["minor"] = minor
- release_info["version"]["patch"] = patch
- release_info["git"] = {
- "commit": commit,
- "commit_short": commit_short,
- }
- release_info["github"] = {"release": release["html_url"]}
- release_data_minor.append(release_info)
- logging.debug(
- f"Initial minor release '{release_info['name']}' created."
- )
-
- if major not in latest_minor_versions or (
- (minor is not None and minor > latest_minor_versions[major]["minor"])
- and (patch is not None and patch > latest_minor_versions[major]["patch"])
- ):
- latest_minor_versions[major] = {
- "index": len(
- release_data_minor
- if release_type == "minor"
- else release_data_major
- )
- - 1,
- "minor": minor,
- "patch": patch,
- }
-
- return (
- release_data_major,
- release_data_minor,
- latest_minor_versions,
- latest_patch_versions,
- )
-
-
-def create_initial_nightly_releases(major_releases):
- """Generate initial nightly releases from the earliest major release."""
- release_data = []
- release_type = "nightly"
-
- # Set the default start date to 2020-06-09 06:00 UTC
- start_date_default = datetime(2020, 6, 9, 6, 0, 0, tzinfo=pytz.UTC)
-
- if major_releases:
- # Get the earliest major release timestamp
- first_major_release = min(
- major_releases,
- key=lambda r: r["lifecycle"]["released"]["timestamp"],
+ phase = LifecyclePhase(
+ isodate=entry.get("isodate"),
+ timestamp=entry.get("timestamp"),
)
- # Convert the timestamp to a datetime object and set the time to 06:00 UTC
- start_date = datetime.utcfromtimestamp(
- first_major_release["lifecycle"]["released"]["timestamp"]
- ).replace(hour=7, minute=0, second=0, tzinfo=pytz.UTC)
- else:
- logging.info(
- "No major releases found in the generated data. Using default start date."
- )
- # Use the default start date if no major releases are available
- start_date = start_date_default
-
- # Ensure current_date is set to 06:00 UTC as well
- tz = pytz.timezone("UTC")
- current_date = datetime.now(tz).replace(hour=6, minute=0, second=0, patchsecond=0)
-
- date = start_date
-
- while date <= current_date:
- major, minor, patch = get_garden_version_for_date(release_type, date, [])
- commit, commit_short = get_git_commit_at_time(date.strftime("%Y-%m-%d"))
- nightly_name = f"nightly-{major}.{minor}.{patch}"
- release_info = {
- "name": nightly_name,
- "type": "nightly",
- "version": {"major": major, "minor": minor, "patch": patch},
- "lifecycle": {
- "released": {
- "isodate": date.strftime("%Y-%m-%d"),
- "timestamp": int(date.timestamp()),
- }
- },
- "git": {"commit": commit, "commit_short": commit_short},
- }
- release_data.append(release_info)
- logging.debug(f"Initial nightly release '{release_info['name']}' created.")
- date += timedelta(days=1)
-
- return release_data
+ phase.ensure_complete()
+ if entry.get("isodate") and not entry.get("timestamp"):
+ entry["timestamp"] = phase.timestamp
+ elif entry.get("timestamp") and not entry.get("isodate"):
+ entry["isodate"] = phase.isodate
def create_single_release(release_type, args, existing_releases):
@@ -580,15 +180,20 @@ def create_single_release(release_type, args, existing_releases):
lifecycle_eol_isodate = None
lifecycle_eol_timestamp = None
- # Check if a manual commit is provided, otherwise use git commit at the given time
- if args.commit:
- commit = args.commit
- if len(commit) != 40:
- logging.error("Error: Invalid commit hash. Must be 40 characters.")
- sys.exit(ERROR_CODES["validation_error"])
- commit_short = commit[:8]
- else:
- commit, commit_short = get_git_commit_at_time(lifecycle_released_isodate)
+ # Resolve the git commit only for release types that store it
+ # (minor, nightly, dev). 'next' and 'major' releases do not carry a commit,
+ # so we avoid the network lookup (git clone) entirely for them.
+ commit = None
+ commit_short = None
+ if release_type in ("minor", "nightly", "dev"):
+ if args.commit:
+ commit = args.commit
+ if len(commit) != 40:
+ logging.error("Error: Invalid commit hash. Must be 40 characters.")
+ sys.exit(ERROR_CODES["validation_error"])
+ commit_short = commit[:8]
+ else:
+ commit, commit_short = get_git_commit_at_time(lifecycle_released_isodate)
# Check if a manual version is provided, otherwise use garden version for the date
if args.create == "next":
@@ -643,111 +248,76 @@ def create_single_release(release_type, args, existing_releases):
# Create version object
version = {"major": major, "minor": minor, "patch": patch}
- # First try to get flavors from flavors.yaml
- flavors = parse_flavors_commit(
- commit, version=version, query_s3=False, logger=logging.getLogger()
- )
-
- # Only if no flavors found in flavors.yaml, try S3
- if not flavors:
- logging.info("No flavors found in flavors.yaml, checking S3 artifacts...")
- # Get artifacts data from S3 with caching
- artifacts_data = get_s3_artifacts(
- DEFAULTS["ARTIFACTS_S3_BUCKET_NAME"],
- DEFAULTS["ARTIFACTS_S3_PREFIX"],
- logger=logging.getLogger(),
+ # Flavors only apply to release types that carry a git commit.
+ flavors = []
+ if release_type in ("minor", "nightly", "dev"):
+ flavors = resolve_flavors(
+ commit, version, skip=getattr(args, "no_flavors", False)
)
-
- if artifacts_data:
- flavors = parse_flavors_commit(
- commit,
- version=version,
- query_s3=True,
- s3_objects=artifacts_data,
- logger=logging.getLogger(),
+ if not flavors:
+ logging.info(
+ f"No flavors found anywhere for version {version} "
+ f"(commit {commit_short})"
)
- else:
- logging.warning("No artifacts data available from S3")
- if not flavors:
- logging.info(
- f"No flavors found anywhere for version {version} (commit {commit_short})"
- )
+ # Build the release using the domain model (single source of truth for
+ # name generation, version shape, and serialization).
+ rtype = ReleaseType(release_type)
+ version_obj = Version(major, minor, patch)
- # Create release data
- release = {}
- release["type"] = f"{release_type}"
- release["version"] = {}
- release["version"]["major"] = major
- release["lifecycle"] = {}
- release["lifecycle"]["released"] = {}
- release["lifecycle"]["released"]["isodate"] = lifecycle_released_isodate
- release["lifecycle"]["released"]["timestamp"] = lifecycle_released_timestamp
-
- if release_type in ["dev", "nightly"]:
- # Create name and version based on schema version
- if major >= 2017:
- # v2 schema: include patch version
- release["name"] = f"{release_type}-{major}.{minor}.{patch}"
- release["version"]["minor"] = minor
- release["version"]["patch"] = patch
- else:
- # v1 schema: exclude patch version
- release["name"] = f"{release_type}-{major}.{minor}"
- release["version"]["minor"] = minor
- # Don't set patch for v1 schema releases
+ released_phase = LifecyclePhase(
+ isodate=lifecycle_released_isodate,
+ timestamp=lifecycle_released_timestamp,
+ )
- release["git"] = {}
- release["git"]["commit"] = commit
- release["git"]["commit_short"] = commit_short
- release["flavors"] = flavors
- release["attributes"] = {}
- release["attributes"]["source_repo"] = True
- elif release_type == "next":
- release["name"] = f"{release_type}"
- release["lifecycle"]["extended"] = {}
- release["lifecycle"]["extended"]["isodate"] = lifecycle_extended_isodate
- release["lifecycle"]["extended"]["timestamp"] = lifecycle_extended_timestamp
- release["lifecycle"]["eol"] = {}
- release["lifecycle"]["eol"]["isodate"] = lifecycle_eol_isodate
- release["lifecycle"]["eol"]["timestamp"] = lifecycle_eol_timestamp
- elif release_type == "major":
- release["name"] = f"{release_type}-{major}"
- release["lifecycle"]["extended"] = {}
- release["lifecycle"]["extended"]["isodate"] = lifecycle_extended_isodate
- release["lifecycle"]["extended"]["timestamp"] = lifecycle_extended_timestamp
- release["lifecycle"]["eol"] = {}
- release["lifecycle"]["eol"]["isodate"] = lifecycle_eol_isodate
- release["lifecycle"]["eol"]["timestamp"] = lifecycle_eol_timestamp
- elif release_type == "minor":
- # Create name and version based on schema version
- if major >= 2017:
- # v2 schema: include patch version
- release["name"] = f"{release_type}-{major}.{minor}.{patch}"
- release["version"]["minor"] = minor
- release["version"]["patch"] = patch
- github_version = f"{major}.{minor}.{patch}"
- else:
- # v1 schema: exclude patch version
- release["name"] = f"{release_type}-{major}.{minor}"
- release["version"]["minor"] = minor
- # Don't set patch for v1 schema releases
- github_version = f"{major}.{minor}"
-
- release["lifecycle"]["eol"] = {}
- release["lifecycle"]["eol"]["isodate"] = lifecycle_eol_isodate
- release["lifecycle"]["eol"]["timestamp"] = lifecycle_eol_timestamp
- release["git"] = {}
- release["git"]["commit"] = commit
- release["git"]["commit_short"] = commit_short
- release["github"] = {}
- release["github"][
- "release"
- ] = f"https://github.com/gardenlinux/gardenlinux/releases/tag/{github_version}"
- release["flavors"] = flavors
- release["attributes"] = {}
- release["attributes"]["source_repo"] = True
+ if rtype in (ReleaseType.NEXT, ReleaseType.MAJOR):
+ lifecycle = Lifecycle(
+ released=released_phase,
+ extended=LifecyclePhase(
+ isodate=lifecycle_extended_isodate,
+ timestamp=lifecycle_extended_timestamp,
+ ),
+ eol=LifecyclePhase(
+ isodate=lifecycle_eol_isodate,
+ timestamp=lifecycle_eol_timestamp,
+ ),
+ )
+ release_obj = Release(
+ name=Release.default_name(rtype, version_obj),
+ type=rtype,
+ version=version_obj,
+ lifecycle=lifecycle,
+ )
+ elif rtype == ReleaseType.MINOR:
+ lifecycle = Lifecycle(
+ released=released_phase,
+ eol=LifecyclePhase(
+ isodate=lifecycle_eol_isodate,
+ timestamp=lifecycle_eol_timestamp,
+ ),
+ )
+ release_obj = Release(
+ name=Release.default_name(rtype, version_obj),
+ type=rtype,
+ version=version_obj,
+ lifecycle=lifecycle,
+ git=GitInfo(commit=commit, commit_short=commit_short),
+ github={"release": Release.github_release_url(version_obj, rtype)},
+ flavors=flavors,
+ attributes={"source_repo": True},
+ )
+ else: # dev, nightly
+ release_obj = Release(
+ name=Release.default_name(rtype, version_obj),
+ type=rtype,
+ version=version_obj,
+ lifecycle=Lifecycle(released=released_phase),
+ git=GitInfo(commit=commit, commit_short=commit_short),
+ flavors=flavors,
+ attributes={"source_repo": True},
+ )
+ release = release_obj.to_dict()
logging.debug(f"Release '{release['name']}' created.")
return release
@@ -933,19 +503,6 @@ def update_release(
logging.debug(f"Release '{args.update}' will be updated.")
-def merge_input_data(existing_releases, new_releases):
- """Merge two lists of releases, updating existing releases with new releases."""
- # Create a dictionary of releases by name from existing_releases
- releases_by_name = {release["name"]: release for release in existing_releases}
-
- # Update or add releases from new_data
- for new_release in new_releases:
- releases_by_name[new_release["name"]] = new_release
-
- # Return the merged list of releases
- return list(releases_by_name.values())
-
-
def set_latest_minor_eol_to_major(major_releases, minor_releases):
"""Set the EOL of each minor version to the next higher minor version,
and the EOL of the latest minor version to match the major release."""
@@ -989,22 +546,17 @@ def load_input(filename):
"""Load manual input from a file if it exists."""
try:
input_data = yaml.safe_load(open(filename, "r"))
-
merged_releases = input_data.get("releases", [])
if len(merged_releases) == 0:
logging.error("Error, no releases found in JSON from file")
sys.exit(ERROR_CODES["input_parameter_missing"])
- next_releases = [r for r in merged_releases if r["type"] == "next"]
- major_releases = [r for r in merged_releases if r["type"] == "major"]
- minor_releases = [r for r in merged_releases if r["type"] == "minor"]
- nightly_releases = [r for r in merged_releases if r["type"] == "nightly"]
- dev_releases = [r for r in merged_releases if r["type"] == "dev"]
+ by_type = split_releases_by_type(merged_releases)
return (
- next_releases,
- major_releases,
- minor_releases,
- nightly_releases,
- dev_releases,
+ by_type["next"],
+ by_type["major"],
+ by_type["minor"],
+ by_type["nightly"],
+ by_type["dev"],
)
except json.JSONDecodeError as e:
logging.error(f"Error parsing JSON from file: {str(e)}")
@@ -1019,32 +571,24 @@ def load_input_stdin():
try:
stdin_data = sys.stdin.read()
input_data = json.loads(stdin_data)
-
logging.debug(f"Input data from stdin: {input_data}")
-
merged_releases = input_data.get("releases", [])
if len(merged_releases) == 0:
logging.error("Error, no releases found in JSON from stdin")
sys.exit(ERROR_CODES["input_parameter_missing"])
- next_releases = [r for r in merged_releases if r["type"] == "next"]
- major_releases = [r for r in merged_releases if r["type"] == "major"]
- minor_releases = [r for r in merged_releases if r["type"] == "minor"]
- nightly_releases = [r for r in merged_releases if r["type"] == "nightly"]
- dev_releases = [r for r in merged_releases if r["type"] == "dev"]
-
+ by_type = split_releases_by_type(merged_releases)
logging.debug(
f"Parsed releases from stdin - "
- f"next: {len(next_releases)}, major: {len(major_releases)}, "
- f"minor: {len(minor_releases)}, nightly: {len(nightly_releases)}, "
- f"dev: {len(dev_releases)}"
+ f"next: {len(by_type['next'])}, major: {len(by_type['major'])}, "
+ f"minor: {len(by_type['minor'])}, nightly: {len(by_type['nightly'])}, "
+ f"dev: {len(by_type['dev'])}"
)
-
return (
- next_releases,
- major_releases,
- minor_releases,
- nightly_releases,
- dev_releases,
+ by_type["next"],
+ by_type["major"],
+ by_type["minor"],
+ by_type["nightly"],
+ by_type["dev"],
)
except json.JSONDecodeError as e:
logging.error(f"Error parsing JSON from stdin: {str(e)}")
@@ -1055,78 +599,18 @@ def load_input_stdin():
def parse_release_name(release_name):
- """Parse the release name in the format 'type-major.minor.patch' or
- 'type-major.minor' or 'type-major'."""
- valid_types = ["next", "major", "minor", "nightly", "dev"]
- type_and_version = release_name.split("-", 1)
- if len(type_and_version) != 2:
- logging.error(
- "Error: Invalid release name format. Expected "
- "'type-major.minor.patch' or 'type-major.minor' or 'type-major'"
- )
- sys.exit(ERROR_CODES["validation_error"])
- release_type = type_and_version[0]
- if release_type not in valid_types:
- logging.error(
- f"Error: Invalid release type '{release_type}'. "
- f"Must be one of {', '.join(valid_types)}."
- )
- sys.exit(ERROR_CODES["validation_error"])
- version = type_and_version[1]
- version_parts = version.split(".")
- try:
- if len(version_parts) == 3:
- major = int(version_parts[0])
- minor = int(version_parts[1])
- patch = int(version_parts[2])
- elif len(version_parts) == 2:
- major = int(version_parts[0])
- minor = int(version_parts[1])
- patch = None
- elif len(version_parts) == 1:
- major = int(version_parts[0])
- minor = None
- patch = None
- else:
- logging.error("Error: Invalid version format in release name.")
- sys.exit(ERROR_CODES["validation_error"])
- except ValueError:
- logging.error("Error: Major, minor and patch versions must be integers.")
- sys.exit(ERROR_CODES["validation_error"])
- return release_type, major, minor, patch
+ """Parse a release name into (type, major, minor, patch).
-
-def validate_release_data(release, errors):
- """Validate release data using the appropriate JSON schema."""
- schema = get_schema_for_release(release)
- if not schema:
- error_message = f"Unknown release type: {release['type']}"
- logging.error(error_message)
- errors.append(error_message)
- return False
+ Thin wrapper around :func:`glrd.release.parse_release_name` that converts
+ the model's ``ValueError`` into the CLI's error-exit behavior. The release
+ model is the single source of truth for name parsing.
+ """
try:
- validate(instance=release, schema=schema)
- return True
- except ValidationError as e:
- # Construct the field path that caused the validation error
- field_path = ".".join([str(p) for p in e.absolute_path])
- error_message = (
- f"Validation error for release '{release['name']}' "
- f"at '{field_path}': {e.message}"
- )
- logging.error(error_message)
- errors.append(error_message)
- return False
-
-
-def validate_all_releases(releases):
- """Validate all releases and exit if any validation errors are found."""
- errors = []
- for release in releases:
- validate_release_data(release, errors)
- if errors:
- logging.error(f"Validation failed for {len(errors)} release(s). Exiting.")
+ release_type, major, minor, patch = model_parse_release_name(release_name)
+ except ValueError as exc:
+ logging.error(f"Error: {exc}")
sys.exit(ERROR_CODES["validation_error"])
+ return release_type.value, major, minor, patch
def diff_releases(existing_merged_releases, merged_releases):
@@ -1179,251 +663,10 @@ def diff_releases(existing_merged_releases, merged_releases):
]:
changes_list = sorted(list(changes))
for change in changes_list:
- # formatted_change = change.replace("root", "") # unused
- pass
-
-
-def save_output_file(data, filename, format="yaml"):
- """Save the data to a file in the specified format."""
- with open(filename, "w") as file:
- if format == "yaml":
- yaml.dump(
- data,
- file,
- default_flow_style=False,
- sort_keys=False,
- Dumper=NoAliasDumper,
- )
- else:
- # Optimize JSON by removing unnecessary spaces
- json.dump(data, file, separators=(",", ":"), ensure_ascii=False)
-
-
-def create_s3_bucket(args, bucket_name=None, region=None):
- """Create an S3 bucket for storing releases data."""
- if not bucket_name:
- bucket_name = args.s3_bucket_name
- if not region:
- region = args.s3_bucket_region
- try:
- s3_client = boto3.client("s3", region_name=region)
- location = {"LocationConstraint": region}
- s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location)
- logging.info(f"Bucket '{bucket_name}' created successfully.")
- s3_client.put_bucket_tagging(
- Bucket=bucket_name,
- Tagging={
- "TagSet": [
- {
- "Key": "sec-by-def-public-storage-exception",
- "Value": "enabled",
- },
- {
- "Key": "sec-by-def-objectversioning-exception",
- "Value": "enabled",
- },
- {
- "Key": "sec-by-def-encrypt-storage-exception",
- "Value": "enabled",
- },
- ]
- },
- )
- logging.info(f"Tags added to bucket '{bucket_name}'.")
- s3_client.put_public_access_block(
- Bucket=bucket_name,
- PublicAccessBlockConfiguration={
- "BlockPublicAcls": False,
- "IgnorePublicAcls": False,
- "BlockPublicPolicy": False,
- "RestrictPublicBuckets": False,
- },
- )
- logging.info(
- f"Public access block settings disabled for bucket '{bucket_name}'."
- )
- bucket_policy = {
- "Version": "2012-10-17",
- "Statement": [
- # Allow public read access to all objects in the bucket
- {
- "Sid": "PublicReadGetObject",
- "Effect": "Allow",
- "Principal": "*",
- "Action": "s3:GetObject",
- "Resource": f"arn:aws:s3:::{bucket_name}/*",
- },
- # Deny non-SSL access
- {
- "Sid": "AllowSSLRequestsOnly",
- "Effect": "Deny",
- "Principal": "*",
- "Action": "s3:*",
- "Resource": [
- "arn:aws:s3:::gardenlinux-glrd",
- "arn:aws:s3:::gardenlinux-glrd/*",
- ],
- "Condition": {"Bool": {"aws:SecureTransport": "false"}},
- },
- ],
- }
- s3_client.put_bucket_policy(
- Bucket=bucket_name, Policy=json.dumps(bucket_policy)
- )
- logging.info(
- f"Bucket '{bucket_name}' made public and denied non-SSL access with a bucket policy."
- )
- except ClientError as e:
- logging.error(f"Error creating bucket: {e}")
- sys.exit(ERROR_CODES["s3_output_error"])
-
-
-def upload_to_s3(file_path, bucket_name, bucket_key):
- """Upload a file to an S3 bucket."""
- s3_client = boto3.client("s3")
- try:
- s3_client.upload_file(file_path, bucket_name, bucket_key)
- logging.debug(f"Uploaded '{file_path}' to 's3://{bucket_name}/{bucket_key}'.")
- except ClientError as e:
- logging.error(f"Error uploading {file_path} to S3: {e}")
- sys.exit(ERROR_CODES["s3_output_error"])
-
-
-def download_from_s3(bucket_name, bucket_key, local_file):
- """Download a file from an S3 bucket to a local file."""
- s3_client = boto3.client("s3")
- try:
- s3_client.download_file(bucket_name, bucket_key, local_file)
- logging.debug(
- f"Downloaded 's3://{bucket_name}/{bucket_key}' to '{local_file}'."
- )
- except ClientError as e:
- if e.response["Error"]["Code"] == "404":
- logging.warning(
- f"No existing file found at 's3://{bucket_name}/{bucket_key}', "
- f"starting with a fresh file."
- )
- return None # No existing file, so we return None
- logging.error(f"Error downloading from S3: {e}")
- sys.exit(ERROR_CODES["s3_output_error"])
-
-
-def merge_existing_s3_data(bucket_name, bucket_key, local_file, new_data):
- """Download, merge, and return the merged data using a temporary file."""
- # Use a temporary file that will be automatically deleted when closed
- with tempfile.NamedTemporaryFile(delete=True, mode="w+") as temp_file:
- # Download existing releases.json from S3 if it exists
- download_from_s3(bucket_name, bucket_key, temp_file.name)
-
- # Load existing data if the file was successfully downloaded
- try:
- temp_file.seek(0) # Go to the start of the file to read the contents
- with open(temp_file.name, "r") as f:
- file_contents = f.read() # Read file contents as a string
- existing_data = json.loads(file_contents) # Load JSON from string
- # Ensure we're working with a list
- existing_releases = (
- existing_data
- if isinstance(existing_data, list)
- else existing_data.get("releases", [])
- )
- except (json.JSONDecodeError, FileNotFoundError):
- logging.warning(
- "Could not decode the existing JSON from S3 or no file "
- "exists. Starting with a fresh file."
- )
- existing_releases = []
-
- # Ensure new_data is treated as a list
- new_releases = (
- new_data if isinstance(new_data, list) else new_data.get("releases", [])
- )
-
- # Use the merge function to merge new and existing releases
- merged_releases = merge_input_data(existing_releases, new_releases)
-
- # Return the merged data as a list
- return merged_releases
-
-
-def download_all_s3_files(bucket_name, bucket_prefix):
- """Download all release files from S3 bucket."""
- s3_client = get_s3_client()
-
- try:
- # List all objects in the bucket with the given prefix
- paginator = s3_client.get_paginator("list_objects_v2")
- found_files = False
-
- logging.info(f"Looking for files in s3://{bucket_name}/{bucket_prefix}")
-
- for page in paginator.paginate(Bucket=bucket_name, Prefix=bucket_prefix):
- if "Contents" in page:
- found_files = True
- for obj in page["Contents"]:
- key = obj["Key"]
- if key.endswith(".json"):
- local_file = os.path.basename(key)
- download_from_s3(bucket_name, key, local_file)
-
- if not found_files:
- logging.warning(
- f"No release files found in s3://{bucket_name}/{bucket_prefix}"
- )
- # Create empty files for each release type
- for release_type in DEFAULTS["RELEASE_TYPES"]:
- filename = f"releases-{release_type}.json"
- save_output_file({"releases": []}, filename, "json")
-
- except Exception as e:
- logging.error(f"Error downloading files from S3: {e}")
-
-
-def upload_all_local_files(bucket_name, bucket_prefix):
- """Upload all local release files to S3."""
- s3_client = boto3.client("s3")
-
- try:
- # First find all matching local files
- matching_files = []
- for file in os.listdir("."):
- for release_type in DEFAULTS["RELEASE_TYPES"]:
- filename = f"releases-{release_type}.json"
- if fnmatch.fnmatch(file, filename):
- matching_files.append(file)
-
- if not matching_files:
- logging.warning("No release files found to upload")
- return
-
- # Show what will be uploaded and ask for confirmation
- print("\nThe following files will be uploaded to S3:")
- for file in matching_files:
- bucket_key = f"{bucket_prefix}{file}"
- print(f" {file} -> s3://{bucket_name}/{bucket_key}")
-
- response = input(
- "\nDo you really want to upload these files to S3? [y/N] "
- ).lower()
- if response != "y":
- print("Upload cancelled.")
- return
-
- # Proceed with upload
- files_uploaded = 0
- for file in matching_files:
- bucket_key = f"{bucket_prefix}{file}"
- try:
- s3_client.upload_file(file, bucket_name, bucket_key)
- files_uploaded += 1
- except Exception as e:
- logging.error(f"Error uploading {file}: {e}")
-
- logging.info(f"Successfully uploaded {files_uploaded} release files")
-
- except Exception as e:
- logging.error(f"Error accessing S3: {e}")
- sys.exit(ERROR_CODES["s3_error"])
+ formatted_change = str(change).replace("root", "")
+ logging.info(
+ f"{release_name} - {change_type}: {formatted_change}"
+ )
def handle_releases(args):
@@ -1436,6 +679,10 @@ def handle_releases(args):
download_all_s3_files(args.s3_bucket_name, args.s3_bucket_prefix)
return
+ if args.s3_create_bucket:
+ create_s3_bucket(args)
+ return
+
if not args.s3_update:
logging.warning("'--s3-update' was not passed, skipping S3 update.")
@@ -1757,6 +1004,39 @@ def parse_arguments():
help="The name of the input file (default: releases-input.yaml).",
)
+ parser.add_argument(
+ "--input-type",
+ choices=["file", "url"],
+ default=DEFAULTS["QUERY_INPUT_TYPE"],
+ help="Where existing releases are queried from when not using "
+ "--no-query: 'file' (local) or 'url' (default). Use 'file' to run "
+ "fully offline.",
+ )
+
+ parser.add_argument(
+ "--input-url",
+ type=str,
+ default=DEFAULTS["QUERY_INPUT_URL"],
+ help="Base URL used to query existing releases (default: "
+ "gardenlinux-glrd S3 URL). Only used with '--input-type url'.",
+ )
+
+ parser.add_argument(
+ "--input-file-prefix",
+ type=str,
+ default=DEFAULTS["QUERY_INPUT_FILE_PREFIX"],
+ help="Prefix used to query existing releases from local files "
+ "(default: releases). Only used with '--input-type file'.",
+ )
+
+ parser.add_argument(
+ "--query-input-format",
+ choices=["yaml", "json"],
+ default=DEFAULTS["QUERY_INPUT_FORMAT"],
+ help="Format of the existing releases queried when not using "
+ "--no-query (default: json).",
+ )
+
parser.add_argument(
"--output-format",
type=str,
@@ -1830,6 +1110,13 @@ def parse_arguments():
type=str,
help="Manually specify the git commit hash (40 characters).",
)
+ parser.add_argument(
+ "--no-flavors",
+ action="store_true",
+ help="Do not resolve flavors (skips the Git clone and S3 lookup). "
+ "Useful for offline use and tests. Can also be enabled via the "
+ "GLRD_SKIP_FLAVORS environment variable.",
+ )
parser.add_argument(
"--lifecycle-released-isodatetime",
type=str,
diff --git a/glrd/query.py b/glrd/query.py
index f6ef080..5a6d169 100755
--- a/glrd/query.py
+++ b/glrd/query.py
@@ -9,6 +9,7 @@
import tabulate
import yaml
+from glrd.release import Release, ReleaseCollection, ReleaseType
from glrd.util import (
DEFAULTS,
ERROR_CODES,
@@ -75,8 +76,7 @@ def get_version_string(version, release_type=None):
def is_active_release(release, current_timestamp):
"""Check if the release is still active based on its EOL timestamp."""
- eol_timestamp = release.get("lifecycle", {}).get("eol", {}).get("timestamp")
- return eol_timestamp and eol_timestamp > current_timestamp
+ return Release.from_dict(release).lifecycle.is_active(current_timestamp)
def filter_active_releases(releases):
@@ -89,8 +89,7 @@ def filter_active_releases(releases):
def is_archived_release(release, current_timestamp):
"""Check if the release archived based on its EOL timestamp."""
- eol_timestamp = release.get("lifecycle", {}).get("eol", {}).get("timestamp")
- return eol_timestamp and eol_timestamp < current_timestamp
+ return Release.from_dict(release).lifecycle.is_archived(current_timestamp)
def filter_archived_releases(releases):
@@ -108,32 +107,31 @@ def filter_releases(releases, release_types=None, version=None):
Filter releases by type and/or version, respecting versioned schemas.
Args:
- releases: List of release objects to filter
+ releases: List of release objects (dicts) to filter
release_types: Comma-separated string of release types to include
version: Version string in format major.minor.patch (patch is optional)
Returns:
- Filtered list of releases
+ Filtered list of releases (dicts)
"""
+ collection = ReleaseCollection([Release.from_dict(r) for r in releases])
+
if version:
version_parts = version.split(".")
major = int(version_parts[0])
minor = int(version_parts[1]) if len(version_parts) > 1 else None
patch = int(version_parts[2]) if len(version_parts) > 2 else None
-
- releases = [
- r
- for r in releases
- if r["version"]["major"] == major
- and (minor is None or r["version"].get("minor", 0) == minor)
- and (patch is None or r["version"].get("patch", 0) == patch)
- ]
+ collection = collection.filter_version(major, minor, patch)
if release_types:
- release_types = release_types.split(",")
- releases = [r for r in releases if r.get("type") in release_types]
+ valid = {rt.value for rt in ReleaseType}
+ # Unknown types are ignored (they simply match nothing), preserving the
+ # historical behavior where an invalid --type produced an empty result
+ # rather than an error.
+ types = [ReleaseType(t) for t in release_types.split(",") if t in valid]
+ collection = collection.by_types(types)
- return releases
+ return collection.to_list()
def find_latest_release(releases):
@@ -141,27 +139,14 @@ def find_latest_release(releases):
Find the latest release by version, respecting versioned schemas.
Args:
- releases: List of release objects to search
+ releases: List of release objects (dicts) to search
Returns:
- The latest release object or None if no releases found
+ The latest release object (dict) or None if no releases found
"""
-
- def get_version_key(release):
- """Get version key for comparison, respecting versioned schemas."""
- version = release["version"]
- major = version["major"]
- minor = version.get("minor", 0)
- patch = version.get("patch", 0)
-
- # For versions >= 2017.0.0, include patch in comparison
- if major >= 2017:
- return (major, minor, patch)
- else:
- # For versions < 2017.0.0, exclude patch from comparison
- return (major, minor, 0)
-
- return max(releases, key=get_version_key, default=None)
+ collection = ReleaseCollection([Release.from_dict(r) for r in releases])
+ latest = collection.latest()
+ return latest.to_dict() if latest else None
def get_extended_maintenance(release):
@@ -364,41 +349,13 @@ def sort_releases(releases):
Sort releases by version, respecting versioned schemas.
Args:
- releases: List of release objects to sort
+ releases: List of release objects (dicts) to sort
Returns:
- Sorted list of releases
+ Sorted list of releases (dicts)
"""
-
- def parse_version_part(part):
- if isinstance(part, int):
- return part
- elif isinstance(part, str):
- if part.lower() == "next":
- # Assign a special value to place 'next' releases appropriately
- return float("inf") # Place at the end
- else:
- # Handle other string versions if necessary
- return -1
- else:
- return -1 # Default value for unexpected types
-
- def sort_key(r):
- """Get sort key for a release, respecting versioned schemas."""
- version = r["version"]
- major = parse_version_part(version.get("major"))
- minor = parse_version_part(version.get("minor", -1))
- patch = parse_version_part(version.get("patch", -1))
-
- # For versions >= 2017.0.0, include patch in sorting
- if isinstance(major, int) and major >= 2017:
- return (major, minor, patch)
- else:
- # For versions < 2017.0.0 or non-integer majors (like 'next'),
- # exclude patch from sorting
- return (major, minor, 0)
-
- return sorted(releases, key=sort_key)
+ collection = ReleaseCollection([Release.from_dict(r) for r in releases])
+ return [r.to_dict() for r in collection.sorted()]
def load_releases(input_source, is_url=False):
diff --git a/glrd/release.py b/glrd/release.py
new file mode 100644
index 0000000..46905ab
--- /dev/null
+++ b/glrd/release.py
@@ -0,0 +1,519 @@
+"""
+Typed domain models for GLRD releases.
+
+This module provides dataclasses for all domain objects, replacing the previous
+dict-based approach with immutable, typed objects that are self-documenting
+and easier to test.
+"""
+
+from dataclasses import dataclass
+from enum import Enum
+from typing import Any, Dict, List, Optional, Tuple
+
+from glrd.util import (
+ V2_SCHEMA_THRESHOLD,
+ isodate_to_timestamp,
+ timestamp_to_isodate,
+ get_current_timestamp,
+)
+
+
+class ReleaseType(str, Enum):
+ """Enum for release types."""
+
+ NEXT = "next"
+ MAJOR = "major"
+ MINOR = "minor"
+ NIGHTLY = "nightly"
+ DEV = "dev"
+
+
+@dataclass(frozen=True)
+class Version:
+ """
+ Immutable version object representing a release version.
+
+ Handles both v1 schema (< 2017) and v2 schema (>= 2017) versions,
+ centralizing the version threshold logic. The ``major`` component may be
+ the string ``"next"`` for the special ``next`` release; such versions never
+ use a patch field and always sort after all numeric versions.
+ """
+
+ major: Any # int, or the string "next"
+ minor: Optional[int] = None
+ patch: Optional[int] = None
+
+ @property
+ def is_next(self) -> bool:
+ """True if this is the symbolic 'next' version."""
+ return not isinstance(self.major, int)
+
+ @property
+ def uses_patch(self) -> bool:
+ """Check if this version requires the patch field (v2 schema)."""
+ if self.is_next:
+ return False
+ return self.major >= V2_SCHEMA_THRESHOLD
+
+ def to_string(self, release_type: "ReleaseType") -> str:
+ """
+ Return a version string appropriate for the release type.
+
+ Args:
+ release_type: The type of release (affects string format)
+
+ Returns:
+ Formatted version string
+ """
+ if release_type in (ReleaseType.NEXT, ReleaseType.MAJOR):
+ return str(self.major)
+ if self.uses_patch:
+ return f"{self.major}.{self.minor}.{self.patch}"
+ return f"{self.major}.{self.minor}"
+
+ @classmethod
+ def from_string(cls, version_string: str) -> "Version":
+ """
+ Parse a version string into a Version object.
+
+ Args:
+ version_string: Version string like "2017", "2017.0", "2017.0.1",
+ or "next".
+
+ Returns:
+ Version object
+ """
+ if version_string == "next":
+ return cls("next")
+ parts = version_string.split(".")
+ major = int(parts[0])
+ minor = int(parts[1]) if len(parts) > 1 else None
+ patch = int(parts[2]) if len(parts) > 2 else None
+ return cls(major, minor, patch)
+
+ def to_sort_key(self) -> Tuple[float, float, float]:
+ """
+ Get a sortable tuple for version comparison.
+
+ For v1 schema versions (< 2017), patch is forced to 0 for comparison.
+ The symbolic 'next' version sorts after all numeric versions. A missing
+ minor or patch is treated as 0, so e.g. major ``2020`` and minor
+ ``2020.0`` sort adjacently (this is also why equality/hashing use the
+ same normalized key).
+
+ Returns:
+ Tuple of (major, minor, patch) suitable for sorting
+ """
+ if self.is_next:
+ return (float("inf"), float("inf"), float("inf"))
+ if self.uses_patch:
+ return (self.major, self.minor or 0, self.patch or 0)
+ return (self.major, self.minor or 0, 0)
+
+ def __lt__(self, other: "Version") -> bool:
+ """Compare versions."""
+ return self.to_sort_key() < other.to_sort_key()
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Version):
+ return NotImplemented
+ return self.to_sort_key() == other.to_sort_key()
+
+ def __hash__(self) -> int:
+ return hash(self.to_sort_key())
+
+
+@dataclass
+class LifecyclePhase:
+ """
+ A single lifecycle phase (released, extended, or eol).
+ """
+
+ isodate: Optional[str] = None # "YYYY-MM-DD"
+ timestamp: Optional[int] = None # Unix epoch
+
+ @classmethod
+ def from_isodate(cls, isodate: str) -> "LifecyclePhase":
+ """Create a LifecyclePhase from an isodate string."""
+ ts = isodate_to_timestamp(isodate)
+ return cls(isodate=isodate, timestamp=ts)
+
+ @classmethod
+ def from_timestamp(cls, timestamp: int) -> "LifecyclePhase":
+ """Create a LifecyclePhase from a timestamp."""
+ iso = timestamp_to_isodate(timestamp)
+ return cls(isodate=iso, timestamp=timestamp)
+
+ def ensure_complete(self) -> None:
+ """
+ Ensure both isodate and timestamp are populated.
+
+ Mutates in-place: fills in missing timestamp from isodate or vice versa.
+ """
+ if self.isodate and not self.timestamp:
+ self.timestamp = isodate_to_timestamp(self.isodate)
+ elif self.timestamp and not self.isodate:
+ self.isodate = timestamp_to_isodate(self.timestamp)
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert to dictionary for serialization."""
+ return {"isodate": self.isodate, "timestamp": self.timestamp}
+
+
+@dataclass
+class Lifecycle:
+ """
+ Lifecycle information for a release (released, extended, eol dates).
+ """
+
+ released: LifecyclePhase
+ extended: Optional[LifecyclePhase] = None
+ eol: Optional[LifecyclePhase] = None
+
+ def is_active(self, current_timestamp: Optional[int] = None) -> bool:
+ """
+ Check if the release is still active based on its EOL timestamp.
+
+ Args:
+ current_timestamp: Optional timestamp to check against.
+ If None, uses get_current_timestamp().
+
+ Returns:
+ True if the release is active (EOL in the future)
+ """
+ ts = (
+ current_timestamp
+ if current_timestamp is not None
+ else get_current_timestamp()
+ )
+ if self.eol and self.eol.timestamp:
+ return self.eol.timestamp > ts
+ return False
+
+ def is_archived(self, current_timestamp: Optional[int] = None) -> bool:
+ """
+ Check if the release is archived based on its EOL timestamp.
+
+ Args:
+ current_timestamp: Optional timestamp to check against.
+ If None, uses get_current_timestamp().
+
+ Returns:
+ True if the release is archived (EOL in the past)
+ """
+ ts = (
+ current_timestamp
+ if current_timestamp is not None
+ else get_current_timestamp()
+ )
+ if self.eol and self.eol.timestamp:
+ return self.eol.timestamp < ts
+ return False
+
+ def ensure_complete(self) -> None:
+ """
+ Ensure timestamps/isodates are complete for all phases.
+
+ Mutates in-place.
+ """
+ self.released.ensure_complete()
+ if self.extended:
+ self.extended.ensure_complete()
+ if self.eol:
+ self.eol.ensure_complete()
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert to dictionary for serialization."""
+ result = {"released": self.released.to_dict()}
+ if self.extended:
+ result["extended"] = self.extended.to_dict()
+ if self.eol:
+ result["eol"] = self.eol.to_dict()
+ return result
+
+
+@dataclass(frozen=True)
+class GitInfo:
+ """
+ Git commit information for a release.
+ """
+
+ commit: str # 40-char SHA
+ commit_short: str # 8-char prefix
+
+ @classmethod
+ def from_commit(cls, commit: str) -> "GitInfo":
+ """Create GitInfo from a full commit SHA."""
+ return cls(commit=commit, commit_short=commit[:8])
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert to dictionary."""
+ return {"commit": self.commit, "commit_short": self.commit_short}
+
+
+@dataclass
+class Release:
+ """
+ A complete release object representing a Garden Linux release.
+
+ This is the main domain object, replacing the previous dict-based approach.
+ """
+
+ name: str # e.g., "minor-2017.0.0"
+ type: ReleaseType
+ version: Version
+ lifecycle: Lifecycle
+ git: Optional[GitInfo] = None
+ github: Optional[Dict[str, str]] = None # {"release": "url"}
+ flavors: Optional[List[str]] = None
+ attributes: Optional[Dict[str, Any]] = None # {"source_repo": bool}
+
+ def to_dict(self) -> Dict[str, Any]:
+ """
+ Convert to dictionary for JSON/YAML serialization.
+
+ Returns:
+ Dictionary representation suitable for storage
+ """
+ result: Dict[str, Any] = {
+ "name": self.name,
+ "type": self.type.value,
+ "version": {},
+ "lifecycle": self.lifecycle.to_dict(),
+ }
+
+ # Version
+ result["version"]["major"] = self.version.major
+ if self.version.minor is not None:
+ result["version"]["minor"] = self.version.minor
+ if self.version.patch is not None and self.version.uses_patch:
+ result["version"]["patch"] = self.version.patch
+
+ # Git info
+ if self.git:
+ result["git"] = self.git.to_dict()
+
+ # GitHub info
+ if self.github:
+ result["github"] = self.github
+
+ # Flavors: preserved when set (including an empty list), so the
+ # serialized shape matches what glrd-manage produces on creation.
+ if self.flavors is not None:
+ result["flavors"] = self.flavors
+
+ # Attributes
+ if self.attributes is not None:
+ result["attributes"] = self.attributes
+
+ return result
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> "Release":
+ """
+ Create a Release from a dictionary (deserialization).
+
+ Handles both v1 and v2 dict shapes.
+
+ Args:
+ data: Dictionary with release data
+
+ Returns:
+ Release object
+ """
+ version_data = data["version"]
+ version = Version(
+ version_data["major"],
+ version_data.get("minor"),
+ version_data.get("patch"),
+ )
+
+ lifecycle_data = data["lifecycle"]
+ lifecycle = Lifecycle(
+ released=LifecyclePhase(**lifecycle_data["released"]),
+ extended=(
+ LifecyclePhase(**lifecycle_data["extended"])
+ if "extended" in lifecycle_data
+ else None
+ ),
+ eol=(
+ LifecyclePhase(**lifecycle_data["eol"])
+ if "eol" in lifecycle_data
+ else None
+ ),
+ )
+
+ git = None
+ if "git" in data:
+ git = GitInfo(
+ data["git"]["commit"],
+ data["git"]["commit_short"],
+ )
+
+ return cls(
+ name=data["name"],
+ type=ReleaseType(data["type"]),
+ version=version,
+ lifecycle=lifecycle,
+ git=git,
+ github=data.get("github"),
+ flavors=data.get("flavors"),
+ attributes=data.get("attributes"),
+ )
+
+ def is_active(self) -> bool:
+ """Check if the release is active."""
+ return self.lifecycle.is_active()
+
+ def is_archived(self) -> bool:
+ """Check if the release is archived."""
+ return self.lifecycle.is_archived()
+
+ @staticmethod
+ def default_name(release_type: "ReleaseType", version: "Version") -> str:
+ """
+ Generate the canonical release name for a type and version.
+
+ Examples: ``next``, ``major-27``, ``minor-2017.0.0``, ``nightly-1990.0``.
+ This is the single source of truth for release-name construction.
+ """
+ if release_type == ReleaseType.NEXT:
+ return "next"
+ if release_type == ReleaseType.MAJOR:
+ return f"major-{version.major}"
+ return f"{release_type.value}-{version.to_string(release_type)}"
+
+ @staticmethod
+ def github_release_url(version: "Version", release_type: "ReleaseType") -> str:
+ """Build the GitHub release URL for a minor release."""
+ return (
+ "https://github.com/gardenlinux/gardenlinux/releases/tag/"
+ f"{version.to_string(release_type)}"
+ )
+
+
+class ReleaseCollection:
+ """
+ A collection of releases with query and transformation methods.
+
+ Provides a fluent interface for filtering, sorting, and transforming
+ releases.
+ """
+
+ def __init__(self, releases: List[Release]):
+ self._releases = releases
+
+ def __iter__(self):
+ return iter(self._releases)
+
+ def __len__(self):
+ return len(self._releases)
+
+ def by_type(self, release_type: ReleaseType) -> "ReleaseCollection":
+ """Filter releases by type."""
+ return ReleaseCollection([r for r in self._releases if r.type == release_type])
+
+ def by_types(self, release_types: List[ReleaseType]) -> "ReleaseCollection":
+ """Filter releases by multiple types."""
+ return ReleaseCollection([r for r in self._releases if r.type in release_types])
+
+ def filter_active(self) -> "ReleaseCollection":
+ """Filter to only active releases."""
+ return ReleaseCollection([r for r in self._releases if r.is_active()])
+
+ def filter_archived(self) -> "ReleaseCollection":
+ """Filter to only archived releases."""
+ return ReleaseCollection([r for r in self._releases if r.is_archived()])
+
+ def filter_version(
+ self, major: int, minor: Optional[int] = None, patch: Optional[int] = None
+ ) -> "ReleaseCollection":
+ """
+ Filter releases by version components.
+
+ Missing minor/patch on a release are treated as 0 when a filter value
+ is supplied, matching the query tool's historical semantics.
+ """
+ return ReleaseCollection(
+ [
+ r
+ for r in self._releases
+ if r.version.major == major
+ and (minor is None or (r.version.minor or 0) == minor)
+ and (patch is None or (r.version.patch or 0) == patch)
+ ]
+ )
+
+ def latest(self) -> Optional[Release]:
+ """Find the latest release by version."""
+ if not self._releases:
+ return None
+ return max(self._releases, key=lambda r: r.version.to_sort_key())
+
+ def sorted(self) -> List[Release]:
+ """Return releases sorted by version."""
+ return sorted(self._releases, key=lambda r: r.version.to_sort_key())
+
+ def to_list(self) -> List[Dict[str, Any]]:
+ """Convert all releases to dictionaries."""
+ return [r.to_dict() for r in self._releases]
+
+ def to_dict_list(self) -> List[Release]:
+ """Return list of Release objects."""
+ return self._releases
+
+
+def parse_release_name(
+ release_name: str,
+) -> Tuple[ReleaseType, int, Optional[int], Optional[int]]:
+ """
+ Parse a release name in the format 'type-major.minor.patch' or similar.
+
+ Args:
+ release_name: Release name like "minor-2017.0.0" or "major-1234"
+
+ Returns:
+ Tuple of (release_type, major, minor, patch)
+
+ Raises:
+ ValueError: If the release name format is invalid
+ """
+ valid_types = [rt.value for rt in ReleaseType]
+ type_and_version = release_name.split("-", 1)
+
+ if len(type_and_version) != 2:
+ raise ValueError(
+ "Invalid release name format. Expected "
+ "'type-major.minor.patch' or 'type-major.minor' or 'type-major'"
+ )
+
+ release_type_str = type_and_version[0]
+ if release_type_str not in valid_types:
+ raise ValueError(
+ f"Invalid release type '{release_type_str}'. "
+ f"Must be one of {', '.join(valid_types)}."
+ )
+
+ release_type = ReleaseType(release_type_str)
+ version = type_and_version[1]
+ version_parts = version.split(".")
+
+ try:
+ if len(version_parts) == 3:
+ major = int(version_parts[0])
+ minor = int(version_parts[1])
+ patch = int(version_parts[2])
+ elif len(version_parts) == 2:
+ major = int(version_parts[0])
+ minor = int(version_parts[1])
+ patch = None
+ elif len(version_parts) == 1:
+ major = int(version_parts[0])
+ minor = None
+ patch = None
+ else:
+ raise ValueError("Invalid version format in release name.")
+ except ValueError:
+ raise ValueError("Major, minor and patch versions must be integers.")
+
+ return release_type, major, minor, patch
diff --git a/glrd/s3.py b/glrd/s3.py
new file mode 100644
index 0000000..8fe0df5
--- /dev/null
+++ b/glrd/s3.py
@@ -0,0 +1,268 @@
+"""
+S3 operations for GLRD.
+
+This module provides functions for interacting with AWS S3 buckets
+for storing and retrieving release data.
+"""
+
+import fnmatch
+import json
+import logging
+import os
+import sys
+import tempfile
+from typing import Any, Dict, List, Optional
+
+import boto3
+from botocore.exceptions import ClientError
+
+from glrd.util import DEFAULTS, ERROR_CODES, merge_input_data, NoAliasDumper
+
+
+def save_output_file(data: Dict[str, Any], filename: str, format: str = "yaml") -> None:
+ """Save the data to a file in the specified format."""
+ import yaml
+
+ with open(filename, "w") as file:
+ if format == "yaml":
+ yaml.dump(
+ data,
+ file,
+ default_flow_style=False,
+ sort_keys=False,
+ Dumper=NoAliasDumper,
+ )
+ else:
+ # Optimize JSON by removing unnecessary spaces
+ json.dump(data, file, separators=(",", ":"), ensure_ascii=False)
+
+
+def create_s3_bucket(
+ args, bucket_name: Optional[str] = None, region: Optional[str] = None
+):
+ """Create an S3 bucket for storing releases data."""
+ if not bucket_name:
+ bucket_name = args.s3_bucket_name
+ if not region:
+ region = args.s3_bucket_region
+ try:
+ s3_client = boto3.client("s3", region_name=region)
+ location = {"LocationConstraint": region}
+ s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location)
+ logging.info(f"Bucket '{bucket_name}' created successfully.")
+ s3_client.put_bucket_tagging(
+ Bucket=bucket_name,
+ Tagging={
+ "TagSet": [
+ {
+ "Key": "sec-by-def-public-storage-exception",
+ "Value": "enabled",
+ },
+ {
+ "Key": "sec-by-def-objectversioning-exception",
+ "Value": "enabled",
+ },
+ {
+ "Key": "sec-by-def-encrypt-storage-exception",
+ "Value": "enabled",
+ },
+ ]
+ },
+ )
+ logging.info(f"Tags added to bucket '{bucket_name}'.")
+ s3_client.put_public_access_block(
+ Bucket=bucket_name,
+ PublicAccessBlockConfiguration={
+ "BlockPublicAcls": False,
+ "IgnorePublicAcls": False,
+ "BlockPublicPolicy": False,
+ "RestrictPublicBuckets": False,
+ },
+ )
+ logging.info(
+ f"Public access block settings disabled for bucket '{bucket_name}'."
+ )
+ bucket_policy = {
+ "Version": "2012-10-17",
+ "Statement": [
+ # Allow public read access to all objects in the bucket
+ {
+ "Sid": "PublicReadGetObject",
+ "Effect": "Allow",
+ "Principal": "*",
+ "Action": "s3:GetObject",
+ "Resource": f"arn:aws:s3:::{bucket_name}/*",
+ },
+ # Deny non-SSL access
+ {
+ "Sid": "AllowSSLRequestsOnly",
+ "Effect": "Deny",
+ "Principal": "*",
+ "Action": "s3:*",
+ "Resource": [
+ "arn:aws:s3:::gardenlinux-glrd",
+ "arn:aws:s3:::gardenlinux-glrd/*",
+ ],
+ "Condition": {"Bool": {"aws:SecureTransport": "false"}},
+ },
+ ],
+ }
+ s3_client.put_bucket_policy(
+ Bucket=bucket_name, Policy=json.dumps(bucket_policy)
+ )
+ logging.info(
+ f"Bucket '{bucket_name}' made public and denied non-SSL access with a bucket policy."
+ )
+ except ClientError as e:
+ logging.error(f"Error creating bucket: {e}")
+ sys.exit(ERROR_CODES["s3_output_error"])
+
+
+def upload_to_s3(file_path: str, bucket_name: str, bucket_key: str) -> None:
+ """Upload a file to an S3 bucket."""
+ s3_client = boto3.client("s3")
+ try:
+ s3_client.upload_file(file_path, bucket_name, bucket_key)
+ logging.debug(f"Uploaded '{file_path}' to 's3://{bucket_name}/{bucket_key}'.")
+ except ClientError as e:
+ logging.error(f"Error uploading {file_path} to S3: {e}")
+ sys.exit(ERROR_CODES["s3_output_error"])
+
+
+def download_from_s3(bucket_name: str, bucket_key: str, local_file: str) -> None:
+ """Download a file from an S3 bucket to a local file."""
+ s3_client = boto3.client("s3")
+ try:
+ s3_client.download_file(bucket_name, bucket_key, local_file)
+ logging.debug(
+ f"Downloaded 's3://{bucket_name}/{bucket_key}' to '{local_file}'."
+ )
+ except ClientError as e:
+ if e.response["Error"]["Code"] == "404":
+ logging.warning(
+ f"No existing file found at 's3://{bucket_name}/{bucket_key}', "
+ f"starting with a fresh file."
+ )
+ return None # No existing file, so we return None
+ logging.error(f"Error downloading from S3: {e}")
+ sys.exit(ERROR_CODES["s3_output_error"])
+
+
+def merge_existing_s3_data(
+ bucket_name: str, bucket_key: str, local_file: str, new_data: Any
+) -> List[Dict[str, Any]]:
+ """Download, merge, and return the merged data using a temporary file."""
+ # Use a temporary file that will be automatically deleted when closed
+ with tempfile.NamedTemporaryFile(delete=True, mode="w+") as temp_file:
+ # Download existing releases.json from S3 if it exists
+ download_from_s3(bucket_name, bucket_key, temp_file.name)
+
+ # Load existing data if the file was successfully downloaded
+ try:
+ temp_file.seek(0) # Go to the start of the file to read the contents
+ with open(temp_file.name, "r") as f:
+ file_contents = f.read() # Read file contents as a string
+ existing_data = json.loads(file_contents) # Load JSON from string
+ # Ensure we're working with a list
+ existing_releases = (
+ existing_data
+ if isinstance(existing_data, list)
+ else existing_data.get("releases", [])
+ )
+ except (json.JSONDecodeError, FileNotFoundError):
+ logging.warning(
+ "Could not decode the existing JSON from S3 or no file "
+ "exists. Starting with a fresh file."
+ )
+ existing_releases = []
+
+ # Ensure new_data is treated as a list
+ new_releases = (
+ new_data if isinstance(new_data, list) else new_data.get("releases", [])
+ )
+
+ # Use the merge function to merge new and existing releases
+ merged_releases = merge_input_data(existing_releases, new_releases)
+
+ # Return the merged data as a list
+ return merged_releases
+
+
+def download_all_s3_files(bucket_name: str, bucket_prefix: str) -> None:
+ """Download all release files from S3 bucket."""
+ s3_client = boto3.client("s3")
+
+ try:
+ # List all objects in the bucket with the given prefix
+ paginator = s3_client.get_paginator("list_objects_v2")
+ found_files = False
+
+ logging.info(f"Looking for files in s3://{bucket_name}/{bucket_prefix}")
+
+ for page in paginator.paginate(Bucket=bucket_name, Prefix=bucket_prefix):
+ if "Contents" in page:
+ found_files = True
+ for obj in page["Contents"]:
+ key = obj["Key"]
+ if key.endswith(".json"):
+ local_file = os.path.basename(key)
+ download_from_s3(bucket_name, key, local_file)
+
+ if not found_files:
+ logging.warning(
+ f"No release files found in s3://{bucket_name}/{bucket_prefix}"
+ )
+ # Create empty files for each release type
+ for release_type in DEFAULTS["RELEASE_TYPES"]:
+ filename = f"releases-{release_type}.json"
+ save_output_file({"releases": []}, filename, "json")
+
+ except Exception as e:
+ logging.error(f"Error downloading files from S3: {e}")
+
+
+def upload_all_local_files(bucket_name: str, bucket_prefix: str) -> None:
+ """Upload all local release files to S3."""
+ s3_client = boto3.client("s3")
+
+ try:
+ # First find all matching local files
+ matching_files = []
+ for file in os.listdir("."):
+ for release_type in DEFAULTS["RELEASE_TYPES"]:
+ filename = f"releases-{release_type}.json"
+ if fnmatch.fnmatch(file, filename):
+ matching_files.append(file)
+
+ if not matching_files:
+ logging.warning("No release files found to upload")
+ return
+
+ # Show what will be uploaded and ask for confirmation
+ print("\nThe following files will be uploaded to S3:")
+ for file in matching_files:
+ bucket_key = f"{bucket_prefix}{file}"
+ print(f" {file} -> s3://{bucket_name}/{bucket_key}")
+
+ response = input(
+ "\nDo you really want to upload these files to S3? [y/N] "
+ ).lower()
+ if response != "y":
+ print("Upload cancelled.")
+ return
+
+ # Proceed with upload
+ files_uploaded = 0
+ for file in matching_files:
+ bucket_key = f"{bucket_prefix}{file}"
+ try:
+ s3_client.upload_file(file, bucket_name, bucket_key)
+ files_uploaded += 1
+ except Exception as e:
+ logging.error(f"Error uploading {file}: {e}")
+
+ logging.info(f"Successfully uploaded {files_uploaded} release files")
+
+ except Exception as e:
+ logging.error(f"Error accessing S3: {e}")
+ sys.exit(ERROR_CODES["s3_error"])
diff --git a/glrd/schema.py b/glrd/schema.py
new file mode 100644
index 0000000..649e181
--- /dev/null
+++ b/glrd/schema.py
@@ -0,0 +1,185 @@
+"""
+Unified schema definitions for Garden Linux releases.
+
+Schema v1: versions < 2017.0.0 (no patch field required)
+Schema v2: versions >= 2017.0.0 (patch field required for minor, nightly, dev)
+
+This module generates both schemas from shared building blocks to eliminate
+code duplication between the previous schema_v1.py and schema_v2.py files.
+"""
+
+
+def _build_lifecycle_required_fields(release_type: str) -> list:
+ if release_type in ("next", "major"):
+ return ["released", "extended", "eol"]
+ elif release_type == "minor":
+ return ["released", "eol"]
+ else:
+ return ["released"]
+
+
+def _build_version_properties(include_patch: bool) -> dict:
+ props = {"major": {"type": "integer"}, "minor": {"type": "integer"}}
+ if include_patch:
+ props["patch"] = {"type": "integer"}
+ return props
+
+
+def _build_version_required(include_patch: bool) -> list:
+ if include_patch:
+ return ["major", "minor", "patch"]
+ else:
+ return ["major", "minor"]
+
+
+def _build_release_schema(
+ release_type: str,
+ include_patch: bool,
+ include_github: bool = False,
+) -> dict:
+ lifecycle_props = {
+ "released": {
+ "type": "object",
+ "properties": {
+ "isodate": {"type": "string", "format": "date"},
+ "timestamp": {"type": "integer"},
+ },
+ "required": ["isodate", "timestamp"],
+ }
+ }
+
+ if release_type in ("next", "major"):
+ lifecycle_props["extended"] = {
+ "type": "object",
+ "properties": {
+ "isodate": {"type": ["string"], "format": "date"},
+ "timestamp": {"type": ["integer"]},
+ },
+ }
+ lifecycle_props["eol"] = {
+ "type": "object",
+ "properties": {
+ "isodate": {"type": ["string"], "format": "date"},
+ "timestamp": {"type": ["integer"]},
+ },
+ }
+ elif release_type == "minor":
+ lifecycle_props["eol"] = {
+ "type": "object",
+ "properties": {
+ "isodate": {"type": ["string"], "format": "date"},
+ "timestamp": {"type": ["integer"]},
+ },
+ }
+
+ schema = {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "type": {"enum": [release_type]},
+ "version": {
+ "type": "object",
+ "properties": _build_version_properties(include_patch),
+ "required": _build_version_required(include_patch),
+ },
+ "lifecycle": {
+ "type": "object",
+ "properties": lifecycle_props,
+ "required": _build_lifecycle_required_fields(release_type),
+ },
+ },
+ "required": ["name", "type", "version", "lifecycle"],
+ }
+
+ if release_type in ("minor", "nightly", "dev"):
+ git_props = {
+ "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
+ "commit_short": {"type": "string", "pattern": "^[0-9a-f]{7,8}$"},
+ }
+ schema["properties"]["git"] = {
+ "type": "object",
+ "properties": git_props,
+ "required": ["commit", "commit_short"],
+ }
+ schema["required"].append("git")
+
+ schema["properties"]["flavors"] = {"type": "array", "items": {"type": "string"}}
+ schema["properties"]["attributes"] = {
+ "type": "object",
+ "properties": {"source_repo": {"type": "boolean", "default": True}},
+ "required": ["source_repo"],
+ }
+
+ if include_github:
+ schema["properties"]["github"] = {
+ "type": "object",
+ "properties": {"release": {"type": "string", "format": "uri"}},
+ "required": ["release"],
+ }
+ schema["required"].append("github")
+
+ return schema
+
+
+def _build_next_major_schema(release_type: str) -> dict:
+ lifecycle_props = {
+ "released": {
+ "type": "object",
+ "properties": {
+ "isodate": {"type": "string", "format": "date"},
+ "timestamp": {"type": "integer"},
+ },
+ "required": ["isodate", "timestamp"],
+ },
+ "extended": {
+ "type": "object",
+ "properties": {
+ "isodate": {"type": ["string"], "format": "date"},
+ "timestamp": {"type": ["integer"]},
+ },
+ },
+ "eol": {
+ "type": "object",
+ "properties": {
+ "isodate": {"type": ["string"], "format": "date"},
+ "timestamp": {"type": ["integer"]},
+ },
+ },
+ }
+
+ return {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "type": {"enum": [release_type]},
+ "version": {
+ "type": "object",
+ "properties": (
+ {"major": {"enum": ["next"]}}
+ if release_type == "next"
+ else {"major": {"type": "integer"}}
+ ),
+ "required": ["major"],
+ },
+ "lifecycle": {
+ "type": "object",
+ "properties": lifecycle_props,
+ "required": ["released", "extended", "eol"],
+ },
+ },
+ "required": ["name", "type", "version", "lifecycle"],
+ }
+
+
+def _generate_schema(include_patch: bool) -> dict:
+ schema = {}
+ schema["next"] = _build_next_major_schema("next")
+ schema["major"] = _build_next_major_schema("major")
+ schema["minor"] = _build_release_schema("minor", include_patch, include_github=True)
+ schema["nightly"] = _build_release_schema("nightly", include_patch)
+ schema["dev"] = _build_release_schema("dev", include_patch)
+ return schema
+
+
+SCHEMA_V1 = _generate_schema(include_patch=False)
+SCHEMA_V2 = _generate_schema(include_patch=True)
diff --git a/glrd/schema_v1.py b/glrd/schema_v1.py
deleted file mode 100644
index d486adb..0000000
--- a/glrd/schema_v1.py
+++ /dev/null
@@ -1,289 +0,0 @@
-"""
-Schema v1 for Garden Linux releases with versions < 2017.0.0.
-This schema version does not require the patch field in version objects.
-"""
-
-# Schema v1 for releases that don't have patch field
-SCHEMA_V1 = {
- "next": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["next"]},
- "version": {
- "type": "object",
- "properties": {"major": {"enum": ["next"]}},
- "required": ["major"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- },
- "extended": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- "eol": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- },
- "required": ["released", "extended", "eol"],
- },
- },
- "required": ["name", "type", "version", "lifecycle"],
- },
- "major": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["major"]},
- "version": {
- "type": "object",
- "properties": {"major": {"type": "integer"}},
- "required": ["major"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- },
- "extended": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- "eol": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- },
- "required": ["released", "extended", "eol"],
- },
- },
- "required": ["name", "type", "version", "lifecycle"],
- },
- "minor": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["minor"]},
- "version": {
- "type": "object",
- "properties": {
- "major": {"type": "integer"},
- "minor": {"type": "integer"},
- },
- "required": ["major", "minor"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- },
- "eol": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- },
- "required": ["released", "eol"],
- },
- "git": {
- "type": "object",
- "properties": {
- "commit": {
- "type": "string",
- "pattern": "^[0-9a-f]{40}$",
- },
- "commit_short": {
- "type": "string",
- "pattern": "^[0-9a-f]{7,8}$",
- },
- },
- "required": ["commit", "commit_short"],
- },
- "github": {
- "type": "object",
- "properties": {"release": {"type": "string", "format": "uri"}},
- "required": ["release"],
- },
- "flavors": {"type": "array", "items": {"type": "string"}},
- "attributes": {
- "type": "object",
- "properties": {"source_repo": {"type": "boolean", "default": True}},
- "required": ["source_repo"],
- },
- },
- "required": [
- "name",
- "type",
- "version",
- "lifecycle",
- "git",
- "github",
- ],
- },
- "nightly": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["nightly"]},
- "version": {
- "type": "object",
- "properties": {
- "major": {"type": "integer"},
- "minor": {"type": "integer"},
- },
- "required": ["major", "minor"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- }
- },
- "required": ["released"],
- },
- "git": {
- "type": "object",
- "properties": {
- "commit": {
- "type": "string",
- "pattern": "^[0-9a-f]{40}$",
- },
- "commit_short": {
- "type": "string",
- "pattern": "^[0-9a-f]{7,8}$",
- },
- },
- "required": ["commit", "commit_short"],
- },
- "flavors": {"type": "array", "items": {"type": "string"}},
- "attributes": {
- "type": "object",
- "properties": {"source_repo": {"type": "boolean", "default": True}},
- "required": ["source_repo"],
- },
- },
- "required": ["name", "type", "version", "lifecycle", "git"],
- },
- "dev": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["dev"]},
- "version": {
- "type": "object",
- "properties": {
- "major": {"type": "integer"},
- "minor": {"type": "integer"},
- },
- "required": ["major", "minor"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- }
- },
- "required": ["released"],
- },
- "git": {
- "type": "object",
- "properties": {
- "commit": {
- "type": "string",
- "pattern": "^[0-9a-f]{40}$",
- },
- "commit_short": {
- "type": "string",
- "pattern": "^[0-9a-f]{7,8}$",
- },
- },
- "required": ["commit", "commit_short"],
- },
- "flavors": {"type": "array", "items": {"type": "string"}},
- "attributes": {
- "type": "object",
- "properties": {"source_repo": {"type": "boolean", "default": True}},
- "required": ["source_repo"],
- },
- },
- "required": ["name", "type", "version", "lifecycle", "git"],
- },
-}
diff --git a/glrd/schema_v2.py b/glrd/schema_v2.py
deleted file mode 100644
index 8949a64..0000000
--- a/glrd/schema_v2.py
+++ /dev/null
@@ -1,292 +0,0 @@
-"""
-Schema v2 for Garden Linux releases with versions >= 2017.0.0.
-This schema version requires the patch field in version objects.
-"""
-
-# Schema v2 for releases that have patch field
-SCHEMA_V2 = {
- "next": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["next"]},
- "version": {
- "type": "object",
- "properties": {"major": {"enum": ["next"]}},
- "required": ["major"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- },
- "extended": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- "eol": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- },
- "required": ["released", "extended", "eol"],
- },
- },
- "required": ["name", "type", "version", "lifecycle"],
- },
- "major": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["major"]},
- "version": {
- "type": "object",
- "properties": {"major": {"type": "integer"}},
- "required": ["major"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- },
- "extended": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- "eol": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- },
- "required": ["released", "extended", "eol"],
- },
- },
- "required": ["name", "type", "version", "lifecycle"],
- },
- "minor": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["minor"]},
- "version": {
- "type": "object",
- "properties": {
- "major": {"type": "integer"},
- "minor": {"type": "integer"},
- "patch": {"type": "integer"},
- },
- "required": ["major", "minor", "patch"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- },
- "eol": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": ["string"],
- "format": "date",
- },
- "timestamp": {"type": ["integer"]},
- },
- },
- },
- "required": ["released", "eol"],
- },
- "git": {
- "type": "object",
- "properties": {
- "commit": {
- "type": "string",
- "pattern": "^[0-9a-f]{40}$",
- },
- "commit_short": {
- "type": "string",
- "pattern": "^[0-9a-f]{7,8}$",
- },
- },
- "required": ["commit", "commit_short"],
- },
- "github": {
- "type": "object",
- "properties": {"release": {"type": "string", "format": "uri"}},
- "required": ["release"],
- },
- "flavors": {"type": "array", "items": {"type": "string"}},
- "attributes": {
- "type": "object",
- "properties": {"source_repo": {"type": "boolean", "default": True}},
- "required": ["source_repo"],
- },
- },
- "required": [
- "name",
- "type",
- "version",
- "lifecycle",
- "git",
- "github",
- ],
- },
- "nightly": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["nightly"]},
- "version": {
- "type": "object",
- "properties": {
- "major": {"type": "integer"},
- "minor": {"type": "integer"},
- "patch": {"type": "integer"},
- },
- "required": ["major", "minor", "patch"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- }
- },
- "required": ["released"],
- },
- "git": {
- "type": "object",
- "properties": {
- "commit": {
- "type": "string",
- "pattern": "^[0-9a-f]{40}$",
- },
- "commit_short": {
- "type": "string",
- "pattern": "^[0-9a-f]{7,8}$",
- },
- },
- "required": ["commit", "commit_short"],
- },
- "flavors": {"type": "array", "items": {"type": "string"}},
- "attributes": {
- "type": "object",
- "properties": {"source_repo": {"type": "boolean", "default": True}},
- "required": ["source_repo"],
- },
- },
- "required": ["name", "type", "version", "lifecycle", "git"],
- },
- "dev": {
- "type": "object",
- "properties": {
- "name": {"type": "string"},
- "type": {"enum": ["dev"]},
- "version": {
- "type": "object",
- "properties": {
- "major": {"type": "integer"},
- "minor": {"type": "integer"},
- "patch": {"type": "integer"},
- },
- "required": ["major", "minor", "patch"],
- },
- "lifecycle": {
- "type": "object",
- "properties": {
- "released": {
- "type": "object",
- "properties": {
- "isodate": {
- "type": "string",
- "format": "date",
- },
- "timestamp": {"type": "integer"},
- },
- "required": ["isodate", "timestamp"],
- }
- },
- "required": ["released"],
- },
- "git": {
- "type": "object",
- "properties": {
- "commit": {
- "type": "string",
- "pattern": "^[0-9a-f]{40}$",
- },
- "commit_short": {
- "type": "string",
- "pattern": "^[0-9a-f]{7,8}$",
- },
- },
- "required": ["commit", "commit_short"],
- },
- "flavors": {"type": "array", "items": {"type": "string"}},
- "attributes": {
- "type": "object",
- "properties": {"source_repo": {"type": "boolean", "default": True}},
- "required": ["source_repo"],
- },
- },
- "required": ["name", "type", "version", "lifecycle", "git"],
- },
-}
diff --git a/glrd/update.py b/glrd/update.py
index 55bfbc7..231d75d 100755
--- a/glrd/update.py
+++ b/glrd/update.py
@@ -6,11 +6,15 @@
import boto3
-from glrd.manage import download_all_s3_files, upload_all_local_files
-from glrd.util import DEFAULTS, ERROR_CODES, get_version
-
-from python_gardenlinux_lib.flavors.parse_flavors import parse_flavors_commit
-from python_gardenlinux_lib.s3.s3 import get_s3_artifacts
+from glrd.s3 import download_all_s3_files, upload_all_local_files
+from glrd.util import (
+ DEFAULTS,
+ ERROR_CODES,
+ get_version,
+ resolve_flavors,
+ get_s3_artifacts_data,
+ get_flavors_from_s3_artifacts,
+)
logger = logging.getLogger(__name__)
@@ -168,32 +172,7 @@ def update_flavors(release):
commit = release["git"]["commit"]
version = release["version"]
- # First try flavors.yaml
- flavors = parse_flavors_commit(
- commit, version=version, query_s3=False, logger=logger
- )
-
- # If no flavors found, try S3
- if not flavors:
- logger.info(
- f"No flavors found in flavors.yaml for {release['name']}, checking S3..."
- )
- artifacts_data = get_s3_artifacts(
- DEFAULTS["ARTIFACTS_S3_BUCKET_NAME"],
- DEFAULTS["ARTIFACTS_S3_PREFIX"],
- logger=logger,
- )
-
- if artifacts_data:
- flavors = parse_flavors_commit(
- commit,
- version=version,
- query_s3=True,
- s3_objects=artifacts_data,
- logger=logger,
- )
- else:
- logger.warning(f"No artifacts data available from S3 for {release['name']}")
+ flavors = resolve_flavors(commit, version)
if flavors:
release["flavors"] = flavors
@@ -291,8 +270,11 @@ def process_releases(args):
logging.info(
f"Fetching artifacts data from S3 bucket {DEFAULTS['ARTIFACTS_S3_BUCKET_NAME']}"
)
- artifacts_data = get_s3_artifacts(
- DEFAULTS["ARTIFACTS_S3_BUCKET_NAME"], DEFAULTS["ARTIFACTS_S3_PREFIX"]
+ # Get S3 artifacts using gardenlinux library
+ artifacts_data = get_s3_artifacts_data(
+ DEFAULTS["ARTIFACTS_S3_BUCKET_NAME"],
+ DEFAULTS["ARTIFACTS_S3_PREFIX"],
+ DEFAULTS["ARTIFACTS_S3_CACHE_FILE"],
)
if not artifacts_data or not artifacts_data.get("artifacts"):
@@ -400,12 +382,7 @@ def process_releases(args):
update_source_repo_attribute([release])
# Get flavors for this commit using artifacts data
- flavors = parse_flavors_commit(
- commit,
- version=version,
- query_s3=True,
- s3_objects=artifacts_data,
- )
+ flavors = get_flavors_from_s3_artifacts(artifacts_data, version, commit)
if flavors:
release["flavors"] = flavors
modified = True
diff --git a/glrd/util.py b/glrd/util.py
index 6b775d8..8c4e6c5 100644
--- a/glrd/util.py
+++ b/glrd/util.py
@@ -1,28 +1,102 @@
+import importlib.metadata
import logging
import os
import re
import signal
+import subprocess
import sys
-from datetime import datetime
+from datetime import datetime, timezone
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import Any, Dict, List, Optional
-import importlib.metadata
import pytz
import yaml
+from gardenlinux.constants import GL_REPOSITORY_URL
+from gardenlinux.flavors import Parser
+from gardenlinux.git import Repository
+from gardenlinux.s3 import Bucket
+
+
ERROR_CODES = {
"validation_error": 1,
"subprocess_output_error": 2,
+ "subprocess_output_missing": 12,
"no_releases": 3,
"s3_error": 4,
+ "s3_output_error": 13,
"query_error": 5,
"parameter_missing": 6,
+ "input_parameter_missing": 14,
+ "input_parameter_error": 15,
"invalid_field": 7,
"http_error": 8,
"file_not_found": 9,
"format_error": 10,
"input_error": 11,
+ "output_error": 16,
}
+
+V2_SCHEMA_THRESHOLD = 2017
+
+
+def uses_patch_version(major: int) -> bool:
+ """Check if a major version requires the patch field (v2 schema)."""
+ return major >= V2_SCHEMA_THRESHOLD
+
+
+def fatal_error(message: str, code: str) -> None:
+ """Log an error message and exit with the corresponding error code."""
+ logging.error(message)
+ sys.exit(ERROR_CODES[code])
+
+
+def split_releases_by_type(
+ releases: List[Dict[str, Any]],
+) -> Dict[str, List[Dict[str, Any]]]:
+ """Split a list of releases into a dict keyed by release type."""
+ result = {
+ "next": [],
+ "major": [],
+ "minor": [],
+ "nightly": [],
+ "dev": [],
+ }
+ for release in releases:
+ release_type = release.get("type")
+ if release_type in result:
+ result[release_type].append(release)
+ return result
+
+
+def merge_input_data(
+ existing_releases: List[Dict[str, Any]],
+ new_releases: List[Dict[str, Any]],
+) -> List[Dict[str, Any]]:
+ """
+ Merge two lists of releases, updating existing releases with new ones.
+
+ Releases are keyed by their ``name``; a release in ``new_releases``
+ replaces any existing release with the same name.
+ """
+ releases_by_name = {release["name"]: release for release in existing_releases}
+ for new_release in new_releases:
+ releases_by_name[new_release["name"]] = new_release
+ return list(releases_by_name.values())
+
+
+def run_subprocess(command: List[str], error_msg: str) -> str:
+ """Run a subprocess command and return stdout, or exit with error."""
+ result = subprocess.run(
+ command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
+ )
+ if result.returncode != 0:
+ fatal_error(f"{error_msg}: {result.stderr}", "subprocess_output_error")
+ return result.stdout
+
+
DEFAULTS = {
# Release types
"RELEASE_TYPES": ["next", "major", "minor", "nightly", "dev"],
@@ -50,6 +124,7 @@
"ARTIFACTS_S3_BUCKET_NAME": "gardenlinux-github-releases",
"ARTIFACTS_S3_PREFIX": "objects/",
"ARTIFACTS_S3_BASE_URL": ("https://gardenlinux-github-releases.s3.amazonaws.com"),
+ "ARTIFACTS_S3_CACHE_FILE": "artifacts-cache.json",
# Garden Linux repository
"GL_REPO_NAME": "gardenlinux",
"GL_REPO_OWNER": "gardenlinux",
@@ -126,10 +201,24 @@ def isodate_to_timestamp(isodate):
def timestamp_to_isodate(timestamp):
"""Convert timestamp to ISO date."""
- dt = datetime.utcfromtimestamp(timestamp)
+ dt = datetime.fromtimestamp(timestamp, timezone.utc)
return dt.strftime("%Y-%m-%d")
+def parse_isodatetime(value: str, field_name: str) -> Optional[datetime]:
+ """
+ Parse an ISO datetime string and return a timezone-aware datetime.
+ Exits with error if parsing fails.
+ """
+ try:
+ return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=pytz.UTC)
+ except ValueError:
+ fatal_error(
+ f"Invalid --{field_name} format. Use ISO format: YYYY-MM-DDTHH:MM:SS",
+ "validation_error",
+ )
+
+
# Handle SIGPIPE and BrokenPipeError
def handle_broken_pipe_error(signum, frame):
try:
@@ -145,3 +234,205 @@ def ignore_aliases(self, data):
signal.signal(signal.SIGPIPE, handle_broken_pipe_error)
+
+
+def get_flavors_from_git(commit: str) -> List[str]:
+ """
+ Get flavors from Git repository using gardenlinux library.
+
+ Args:
+ commit: Git commit hash (or 'latest')
+
+ Returns:
+ List of flavor strings
+ """
+
+ try:
+ with TemporaryDirectory() as git_directory:
+ # Use gardenlinux Repository class for sparse checkout with pygit2
+ Repository.checkout_repo_sparse(
+ git_directory=git_directory,
+ repo_url=GL_REPOSITORY_URL,
+ commit=commit if commit != "latest" else None,
+ pathspecs=["flavors.yaml"], # Only checkout the flavors.yaml file
+ )
+
+ flavors_file = Path(git_directory, "flavors.yaml")
+ if flavors_file.exists():
+ with flavors_file.open("r") as fp:
+ flavors_data = fp.read()
+ flavors_yaml = yaml.safe_load(flavors_data)
+ parser = Parser(flavors_yaml)
+ combinations = parser.filter()
+ all_flavors = set(combination for _, combination in combinations)
+ flavors = sorted(all_flavors)
+ logging.info(f"Found {len(flavors)} flavors in Git")
+ return flavors
+ else:
+ logging.warning("flavors.yaml not found in repository")
+ return []
+ except Exception as exc:
+ logging.debug(f"Could not get flavors from Git: {exc}")
+ return []
+
+
+def skip_flavors_lookup() -> bool:
+ """
+ Return True if flavor resolution (Git clone / S3 lookup) should be skipped.
+
+ Flavor resolution requires network access (a Git clone of the Garden Linux
+ repository and/or an S3 listing). Skipping is useful for offline usage and
+ for tests, which must never touch the network or S3.
+
+ Skipping is enabled when the ``GLRD_SKIP_FLAVORS`` environment variable is
+ set to a truthy value (``1``, ``true``, ``yes``, ``on``; case-insensitive).
+ """
+ return os.environ.get("GLRD_SKIP_FLAVORS", "").strip().lower() in (
+ "1",
+ "true",
+ "yes",
+ "on",
+ )
+
+
+def resolve_flavors(
+ commit: str,
+ version: Dict[str, Any],
+ skip: bool = False,
+) -> List[str]:
+ """
+ Resolve the flavors for a release, trying Git first and S3 as a fallback.
+
+ This centralizes the previously duplicated git-then-S3 fallback logic used
+ by both ``glrd-manage`` (create) and ``glrd-update``.
+
+ Args:
+ commit: Git commit hash of the release.
+ version: Version dict with at least ``major`` (and optionally ``minor``).
+ skip: If True (or if ``skip_flavors_lookup()`` is True), no network
+ access is performed and an empty list is returned. This keeps the
+ operation fully offline.
+
+ Returns:
+ Sorted list of flavor strings (possibly empty).
+ """
+ if skip or skip_flavors_lookup():
+ logging.info("Skipping flavor lookup (offline mode / GLRD_SKIP_FLAVORS set).")
+ return []
+
+ # First try flavors.yaml from the Git repository.
+ flavors = get_flavors_from_git(commit)
+ if flavors:
+ return flavors
+
+ # Fall back to S3 artifacts.
+ logging.info("No flavors found in flavors.yaml, checking S3 artifacts...")
+ artifacts_data = get_s3_artifacts_data(
+ DEFAULTS["ARTIFACTS_S3_BUCKET_NAME"],
+ DEFAULTS["ARTIFACTS_S3_PREFIX"],
+ DEFAULTS["ARTIFACTS_S3_CACHE_FILE"],
+ )
+ if artifacts_data:
+ return get_flavors_from_s3_artifacts(artifacts_data, version, commit)
+
+ logging.warning("No artifacts data available from S3")
+ return []
+
+
+def get_s3_artifacts_data(
+ bucket_name: str,
+ prefix: str,
+ cache_file: Optional[str] = None,
+ cache_ttl: int = 3600,
+) -> Optional[Dict]:
+ """
+ Get S3 artifacts data using gardenlinux library with caching support.
+
+ Args:
+ bucket_name: S3 bucket name
+ prefix: S3 prefix
+ cache_file: Optional cache file path for S3 object keys
+ cache_ttl: Cache time-to-live in seconds (default: 1 hour)
+
+ Returns:
+ Dictionary containing S3 artifacts data with 'index' and 'artifacts' keys
+ """
+
+ try:
+ bucket = Bucket(bucket_name)
+
+ artifacts = bucket.read_cache_file_or_filter(
+ cache_file, cache_ttl=cache_ttl, Prefix=prefix
+ )
+
+ index = {}
+ for key in artifacts:
+ try:
+ parts = key.split("/")
+ if len(parts) >= 3:
+ version_commit = parts[1]
+ if "-" in version_commit:
+ version_part, commit_part = version_commit.split("-", 1)
+ if version_part not in index:
+ index[version_part] = []
+ index[version_part].append(key)
+ except Exception as e:
+ logging.debug(f"Could not parse version from key {key}: {e}")
+
+ result = {"index": index, "artifacts": artifacts}
+ logging.info(f"Found {len(artifacts)} artifacts and {len(index)} index entries")
+ return result
+ except Exception as e:
+ logging.error(f"Error getting S3 artifacts: {e}")
+ return None
+
+
+def get_flavors_from_s3_artifacts(
+ artifacts_data: Dict, version: Dict[str, Any], commit: str
+) -> List[str]:
+ """
+ Extract flavors from S3 artifacts data.
+
+ Args:
+ artifacts_data: S3 artifacts data dictionary
+ version: Version dictionary with major, minor, micro
+ commit: Git commit hash
+
+ Returns:
+ List of flavor strings
+ """
+
+ try:
+ version_info = f"{version['major']}.{version.get('minor', 0)}"
+ commit_short = commit[:8]
+
+ # Try index lookup first
+ search_key = f"{version_info}-{commit_short}"
+ if search_key in artifacts_data.get("index", {}):
+ flavors = artifacts_data["index"][search_key]
+ logging.debug(f"Found flavors in S3 index for {search_key}")
+ return flavors
+ else:
+ # Search through artifacts
+ found_flavors = set()
+ for key in artifacts_data.get("artifacts", []):
+ if version_info in key and commit_short in key:
+ try:
+ parts = key.split("/")
+ if len(parts) >= 2:
+ flavor_with_version = parts[1]
+ flavor = flavor_with_version.rsplit(f"-{version_info}", 1)[
+ 0
+ ]
+ if flavor:
+ found_flavors.add(flavor)
+ except Exception as e:
+ logging.debug(f"Error parsing artifact key {key}: {e}")
+ continue
+ flavors = sorted(found_flavors)
+ if flavors:
+ logging.info(f"Found {len(flavors)} flavors in S3 artifacts")
+ return flavors
+ except Exception as e:
+ logging.error(f"Error processing S3 artifacts: {e}")
+ return []
diff --git a/glrd/validation.py b/glrd/validation.py
new file mode 100644
index 0000000..3cf4ea4
--- /dev/null
+++ b/glrd/validation.py
@@ -0,0 +1,140 @@
+"""
+Validation functions for GLRD releases.
+
+This module provides functions for validating release data against
+JSON schemas and checking version format requirements.
+"""
+
+import logging
+import sys
+from typing import Dict, List, Optional, Tuple
+
+from jsonschema import ValidationError, validate
+
+from glrd.schema import SCHEMA_V1, SCHEMA_V2
+from glrd.util import ERROR_CODES, V2_SCHEMA_THRESHOLD
+
+
+def validate_input_version_format(
+ version: str, release_type: str
+) -> Tuple[bool, Optional[str]]:
+ """
+ Validate that the input version format matches schema requirements.
+
+ Args:
+ version: Version string from user input (e.g., "2017.0.1")
+ release_type: Type of release (major, minor, etc.)
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ version_parts = version.split(".")
+
+ if release_type in ["major", "next"]:
+ # major and next don't use major.minor.patch
+ return True, None
+
+ if len(version_parts) == 2:
+ major = int(version_parts[0])
+ # Check if this version requires v2 schema (with patch field)
+ if major >= V2_SCHEMA_THRESHOLD:
+ return (
+ False,
+ f"Version {'.'.join(version_parts)} requires v2 schema "
+ f"but missing patch version. Use format: major.minor.patch",
+ )
+ return True, None
+ elif len(version_parts) == 3:
+ major = int(version_parts[0])
+ # Check if this version should use v1 schema (without patch field)
+ if major < V2_SCHEMA_THRESHOLD:
+ return (
+ False,
+ f"Version {'.'.join(version_parts)} uses v1 schema but "
+ f"includes patch version. Use format: major.minor",
+ )
+ return True, None
+ else:
+ return (
+ False,
+ "Invalid version format. Expected major.minor or major.minor.patch",
+ )
+
+
+def get_schema_for_release(release: Dict) -> Optional[Dict]:
+ """
+ Get the appropriate schema version for a release based on its version number.
+
+ Args:
+ release: Release dictionary containing type and version information
+
+ Returns:
+ Schema dictionary appropriate for the release version, or None
+ """
+ release_type = release.get("type")
+ version = release.get("version", {})
+
+ # For major and next releases, always use v2 schema
+ # (they don't have major.minor.patch version numbers >= 2017)
+ if release_type in ["major", "next"]:
+ return SCHEMA_V2[release_type]
+
+ # For minor, nightly, and dev releases, determine schema version based
+ # on version number
+ if release_type in ["minor", "nightly", "dev"]:
+ major = version.get("major", 0)
+
+ # Use v2 schema (with patch field) for versions >= 2017.0.0
+ if major >= V2_SCHEMA_THRESHOLD:
+ return SCHEMA_V2[release_type]
+ else:
+ return SCHEMA_V1[release_type]
+
+ return None
+
+
+def validate_release_data(release: Dict, errors: List[str]) -> bool:
+ """
+ Validate release data using the appropriate JSON schema.
+
+ Args:
+ release: Release dictionary to validate
+ errors: List to append error messages to (mutated in-place)
+
+ Returns:
+ True if valid, False otherwise
+ """
+ schema = get_schema_for_release(release)
+ if not schema:
+ error_message = f"Unknown release type: {release['type']}"
+ logging.error(error_message)
+ errors.append(error_message)
+ return False
+ try:
+ validate(instance=release, schema=schema)
+ return True
+ except ValidationError as e:
+ # Construct the field path that caused the validation error
+ field_path = ".".join([str(p) for p in e.absolute_path])
+ error_message = (
+ f"Validation error for release '{release['name']}' "
+ f"at '{field_path}': {e.message}"
+ )
+ logging.error(error_message)
+ errors.append(error_message)
+ return False
+
+
+def validate_all_releases(releases: List[Dict]) -> None:
+ """
+ Validate all releases and exit if any validation errors are found.
+
+ Args:
+ releases: List of release dictionaries to validate
+ """
+ errors = []
+ for release in releases:
+ validate_release_data(release, errors)
+ if errors:
+ logging.error(f"Validation failed for {len(errors)} release(s). Exiting.")
+ sys.exit(ERROR_CODES["validation_error"])
diff --git a/poetry.lock b/poetry.lock
index 8dd2508..cbd1c3f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,16 +1,4 @@
-# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
-
-[[package]]
-name = "alabaster"
-version = "0.7.16"
-description = "A light, configurable Sphinx theme"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"},
- {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"},
-]
+# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
[[package]]
name = "apt-repo"
@@ -58,21 +46,6 @@ files = [
[package.dependencies]
pycodestyle = ">=2.12.0"
-[[package]]
-name = "babel"
-version = "2.17.0"
-description = "Internationalization utilities"
-optional = false
-python-versions = ">=3.8"
-groups = ["main"]
-files = [
- {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"},
- {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"},
-]
-
-[package.extras]
-dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""]
-
[[package]]
name = "black"
version = "25.1.0"
@@ -120,34 +93,34 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "boto3"
-version = "1.40.21"
+version = "1.42.86"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "boto3-1.40.21-py3-none-any.whl", hash = "sha256:3772fb828864d3b7046c8bdf2f4860aaca4a79f25b7b060206c6a5f4944ea7f9"},
- {file = "boto3-1.40.21.tar.gz", hash = "sha256:876ccc0b25517b992bd27976282510773a11ebc771aa5b836a238ea426c82187"},
+ {file = "boto3-1.42.86-py3-none-any.whl", hash = "sha256:492c3c7cbbe9842882680064902f50cf711b5ab770d26525549872339ed95d5b"},
+ {file = "boto3-1.42.86.tar.gz", hash = "sha256:c87d2a750b1a8cad0384d1a83d3bad6aedf924ae9a14aaba814bcb3297b39c01"},
]
[package.dependencies]
-botocore = ">=1.40.21,<1.41.0"
+botocore = ">=1.42.86,<1.43.0"
jmespath = ">=0.7.1,<2.0.0"
-s3transfer = ">=0.13.0,<0.14.0"
+s3transfer = ">=0.16.0,<0.17.0"
[package.extras]
crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
-version = "1.40.21"
+version = "1.42.86"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "botocore-1.40.21-py3-none-any.whl", hash = "sha256:574ecf9b68c1721650024a27e00e0080b6f141c281ebfce49e0d302969270ef4"},
- {file = "botocore-1.40.21.tar.gz", hash = "sha256:f77e9c199df0252b14ea739a9ac99723940f6bde90f4c2e7802701553a62827b"},
+ {file = "botocore-1.42.86-py3-none-any.whl", hash = "sha256:443387337864e069f7e4e885ccdc81592725b5598ca966514af3e9776bce0bfe"},
+ {file = "botocore-1.42.86.tar.gz", hash = "sha256:baa49e93b4c92d63e0c8288026ee1ef8de83f182743127cc9175504440a48e49"},
]
[package.dependencies]
@@ -156,7 +129,7 @@ python-dateutil = ">=2.1,<3.0.0"
urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
[package.extras]
-crt = ["awscrt (==0.27.6)"]
+crt = ["awscrt (==0.31.2)"]
[[package]]
name = "certifi"
@@ -172,84 +145,100 @@ files = [
[[package]]
name = "cffi"
-version = "1.17.1"
+version = "2.0.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
groups = ["main"]
-markers = "platform_python_implementation != \"PyPy\""
files = [
- {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
- {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
- {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
- {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
- {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
- {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
- {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
- {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
- {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
- {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
- {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
- {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
- {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
- {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
- {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
- {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
- {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
- {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
- {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
- {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
- {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
- {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
- {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
- {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
- {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
- {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
- {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
- {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
+ {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
+ {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
+ {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
+ {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
+ {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
+ {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
+ {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
+ {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
+ {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
+ {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
+ {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
+ {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
+ {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
+ {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
+ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
+ {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
]
[package.dependencies]
-pycparser = "*"
+pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
[[package]]
name = "charset-normalizer"
@@ -355,14 +344,14 @@ files = [
[[package]]
name = "click"
-version = "8.2.1"
+version = "8.3.2"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
-groups = ["dev"]
+groups = ["main", "dev"]
files = [
- {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"},
- {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"},
+ {file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"},
+ {file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"},
]
[package.dependencies]
@@ -379,7 +368,7 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
-markers = {main = "sys_platform == \"win32\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""}
+markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "coverage"
@@ -484,62 +473,74 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "cryptography"
-version = "44.0.3"
+version = "46.0.7"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
-python-versions = "!=3.9.0,!=3.9.1,>=3.7"
+python-versions = "!=3.9.0,!=3.9.1,>=3.8"
groups = ["main"]
files = [
- {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
- {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
- {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
- {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
- {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
- {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
- {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
- {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
- {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
- {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
- {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
- {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
- {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
- {file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
- {file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
+ {file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"},
+ {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"},
+ {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"},
+ {file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"},
+ {file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"},
+ {file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"},
+ {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"},
+ {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"},
+ {file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"},
+ {file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"},
+ {file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"},
+ {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"},
+ {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"},
+ {file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"},
+ {file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"},
+ {file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"},
]
[package.dependencies]
-cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
+cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
[package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
+docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
-nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
-pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
+nox = ["nox[uv] (>=2024.4.15)"]
+pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
@@ -561,18 +562,6 @@ orderly-set = ">=5.3.0,<6"
cli = ["click (==8.1.8)", "pyyaml (==6.0.2)"]
optimize = ["orjson"]
-[[package]]
-name = "docutils"
-version = "0.20.1"
-description = "Docutils -- Python Documentation Utilities"
-optional = false
-python-versions = ">=3.7"
-groups = ["main"]
-files = [
- {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"},
- {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"},
-]
-
[[package]]
name = "dunamai"
version = "1.23.0"
@@ -605,6 +594,36 @@ mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.14.0,<2.15.0"
pyflakes = ">=3.4.0,<3.5.0"
+[[package]]
+name = "gardenlinux"
+version = "0.10.21"
+description = "Contains tools to work with the features directory of gardenlinux, for example deducting dependencies from feature sets or validating cnames"
+optional = false
+python-versions = ">=3.13,!=3.14.1"
+groups = ["main"]
+files = []
+develop = false
+
+[package.dependencies]
+apt-repo = "^0.5"
+boto3 = "^1.42.30"
+click = "^8.3.1"
+cryptography = "^46.0.3"
+gitpython = "^3.1.45"
+jsonschema = "^4.26.0"
+networkx = "^3.6"
+oras = "^0.2.38"
+pygit2 = "^1.19.1"
+PyGithub = "^2.8.1"
+pygments = "^2.19.2"
+PyYAML = "^6.0.2"
+
+[package.source]
+type = "git"
+url = "https://github.com/gardenlinux/python-gardenlinux-lib.git"
+reference = "0.10.21"
+resolved_reference = "efdffb9a6b279cd7aada23b02baf4ec2534dc6d2"
+
[[package]]
name = "gitdb"
version = "4.0.12"
@@ -622,14 +641,14 @@ smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
-version = "3.1.45"
+version = "3.1.46"
description = "GitPython is a Python library used to interact with Git repositories"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
- {file = "gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77"},
- {file = "gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c"},
+ {file = "gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058"},
+ {file = "gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f"},
]
[package.dependencies]
@@ -637,7 +656,7 @@ gitdb = ">=4.0.1,<5"
[package.extras]
doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"]
-test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""]
+test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""]
[[package]]
name = "idna"
@@ -654,25 +673,13 @@ files = [
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
-[[package]]
-name = "imagesize"
-version = "1.4.1"
-description = "Getting image size from png/jpeg/jpeg2000/gif file"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-groups = ["main"]
-files = [
- {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
- {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
-]
-
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
-groups = ["main", "dev"]
+groups = ["dev"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@@ -710,21 +717,21 @@ files = [
[[package]]
name = "jsonschema"
-version = "4.25.1"
+version = "4.26.0"
description = "An implementation of JSON Schema validation for Python"
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
groups = ["main"]
files = [
- {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"},
- {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"},
+ {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"},
+ {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"},
]
[package.dependencies]
attrs = ">=22.2.0"
-jsonschema-specifications = ">=2023.03.6"
+jsonschema-specifications = ">=2023.3.6"
referencing = ">=0.28.4"
-rpds-py = ">=0.7.1"
+rpds-py = ">=0.25.0"
[package.extras]
format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
@@ -842,50 +849,49 @@ files = [
[[package]]
name = "networkx"
-version = "3.5"
+version = "3.6.1"
description = "Python package for creating and manipulating graphs and networks"
optional = false
-python-versions = ">=3.11"
+python-versions = "!=3.14.1,>=3.11"
groups = ["main"]
files = [
- {file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"},
- {file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"},
+ {file = "networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762"},
+ {file = "networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509"},
]
[package.extras]
+benchmarking = ["asv", "virtualenv"]
default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"]
developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"]
doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"]
-example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"]
+example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "iplotx (>=0.9.0)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"]
extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"]
+release = ["build (>=0.10)", "changelist (==0.5)", "twine (>=4.0)", "wheel (>=0.40)"]
test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"]
test-extras = ["pytest-mpl", "pytest-randomly"]
[[package]]
name = "oras"
-version = "0.2.0"
+version = "0.2.38"
description = "OCI Registry as Storage Python SDK"
optional = false
python-versions = "*"
groups = ["main"]
-files = []
-develop = false
+files = [
+ {file = "oras-0.2.38-py3-none-any.whl", hash = "sha256:a97658086cea1e1ebb412a5b504005480c70de5d2e81a160d33e60fc187d42f4"},
+ {file = "oras-0.2.38.tar.gz", hash = "sha256:58482f78275cb5cc9fdf135248ca878a19204428a51bd3a1ca24c32714d35341"},
+]
[package.dependencies]
jsonschema = "*"
requests = "*"
[package.extras]
-all = ["docker (==5.0.1)", "jsonschema", "pytest (>=4.6.2)", "requests"]
+all = ["boto3 (>=1.33.0)", "docker (==5.0.1)", "jsonschema", "pytest (>=4.6.2)", "requests"]
docker = ["docker (==5.0.1)"]
+ecr = ["boto3 (>=1.33.0)"]
tests = ["pytest (>=4.6.2)"]
-[package.source]
-type = "git"
-url = "https://github.com/oras-project/oras-py.git"
-reference = "caf8db5b279382335fbb1f6d7402ed9b73618d37"
-resolved_reference = "caf8db5b279382335fbb1f6d7402ed9b73618d37"
-
[[package]]
name = "orderly-set"
version = "5.3.0"
@@ -945,7 +951,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
-groups = ["main", "dev"]
+groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -994,7 +1000,7 @@ description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
-markers = "platform_python_implementation != \"PyPy\""
+markers = "implementation_name != \"PyPy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
@@ -1012,6 +1018,94 @@ files = [
{file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"},
]
+[[package]]
+name = "pygit2"
+version = "1.19.2"
+description = "Python bindings for libgit2."
+optional = false
+python-versions = ">=3.11"
+groups = ["main"]
+files = [
+ {file = "pygit2-1.19.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70c7efc426bdae6b67465a03729b79277e7757a29a7d6550b40c18ed36cb7232"},
+ {file = "pygit2-1.19.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b96d6ed7251eef70cfd4126269f1044fa47bc6da6367300027c5e5d74789f7f"},
+ {file = "pygit2-1.19.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f3235db6b553b8fb4d3c1dc86af9be1eab445f1d6c42f4ade5cf5f60efd333"},
+ {file = "pygit2-1.19.2-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02a35d56126f82a303668f4198c138627b3e9820f9f1eec38fff0409be274b9e"},
+ {file = "pygit2-1.19.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e59a2e9eddd59edf999403c266c891dfc171eb95939d229ed614bc21e0c95804"},
+ {file = "pygit2-1.19.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0d2437bd5f8dbd652e8a6c318cbcaa245c0528ee48f6d64f4aaef8fd9b36b93"},
+ {file = "pygit2-1.19.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60d011496e57436b0c8e3fbd4d12745777427b3f33a60710ec3d94d2f76304b7"},
+ {file = "pygit2-1.19.2-cp311-cp311-win32.whl", hash = "sha256:9b0d5a44ca6d77a8c0e2526f6556d9b37cc85d44983ff3549bf5adbf95d289c4"},
+ {file = "pygit2-1.19.2-cp311-cp311-win_amd64.whl", hash = "sha256:0d9c795155086c95ef890c87b50e02792146cfaede2c715698e6988a122373e7"},
+ {file = "pygit2-1.19.2-cp311-cp311-win_arm64.whl", hash = "sha256:837f0a9a0093cbb213176284d29f0ab754ded3e5af967e7ec6419d590a7da92a"},
+ {file = "pygit2-1.19.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cf479077d48a60b09569a5bb50866d8609f434f8982058594b0d2e2950bd6fce"},
+ {file = "pygit2-1.19.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e6e7eb5fb49203735627b8e1d410afe19e7d610c9a9733c11084fabd17f0920"},
+ {file = "pygit2-1.19.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a810da2d108d6bd16115c72a1c3d69fa1528ef927719bdfc94d2cdbc4198288"},
+ {file = "pygit2-1.19.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d0b8ae5a822afb2771cbacf7c75140e663bc801c44eaaf2e4017f850cb27227c"},
+ {file = "pygit2-1.19.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:330430b6c1a3e6d45d1f5f950734d37d849c07924b5b0475cd995a7e541e6ab1"},
+ {file = "pygit2-1.19.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b7f165d1ddfa1e0f205c1115ee10f5fea700fd3584c727b0d61a57192238449"},
+ {file = "pygit2-1.19.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e46ec6a97a5c43704473e42a926f7f20f9934ceef4f4891660313f573c4f0ab8"},
+ {file = "pygit2-1.19.2-cp312-cp312-win32.whl", hash = "sha256:6b4de5469e88e7b069143f7a5d6336a4b3e7d911de4633ef18c113e416feb948"},
+ {file = "pygit2-1.19.2-cp312-cp312-win_amd64.whl", hash = "sha256:f064748202928f4e882501521229e378e0b7b69b0e7c433cdb2626d007745973"},
+ {file = "pygit2-1.19.2-cp312-cp312-win_arm64.whl", hash = "sha256:222f439d751799dc74c3fa75f187abdbc415d12f9a091efa66f0c9ff51893d32"},
+ {file = "pygit2-1.19.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:df207f93a33851a110dec70108e3f2a1c69578932919fd356303eda83a5624db"},
+ {file = "pygit2-1.19.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae884cd53e29b3d831f5261f36048a8d5db5642dc98cd63530810e7fd9c9e60d"},
+ {file = "pygit2-1.19.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bd4059964531d20aaf4577b3761590df9cc7c9e2395df5d33f0552224331b76"},
+ {file = "pygit2-1.19.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3befcccc7b3b62e45da2cc1ce4095964f7606d3d15b43dc667c6ef2a2ada20d"},
+ {file = "pygit2-1.19.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cf08b54553f997f6f60a7918504e22e7baa4ba2fbb11d1e1cb6c0a45ac7e04b"},
+ {file = "pygit2-1.19.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7f630e5a763f01b4be6e2374c487086229c8f7392a2e5591d29095c5e481da4"},
+ {file = "pygit2-1.19.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6166845f41d4f6be3353997022d64035fe3df348c8e34d7d30c5f95817fbcab4"},
+ {file = "pygit2-1.19.2-cp313-cp313-win32.whl", hash = "sha256:5bebea045102e87dea142242298d4dd668d0227f76042f98efb1c5d5dd3db21e"},
+ {file = "pygit2-1.19.2-cp313-cp313-win_amd64.whl", hash = "sha256:7bbfeb680821001a5c1b6959da1eae906806c90c9992ae4564d3ea83a27bb19f"},
+ {file = "pygit2-1.19.2-cp313-cp313-win_arm64.whl", hash = "sha256:033d489186145cf67b2c60840d2a308f6b1e9d641de12417c447f9829dacde70"},
+ {file = "pygit2-1.19.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5effee3f4ad0d9c89b34ebecf1acee26f6b117ef3c51345ad022bd521fd8dca"},
+ {file = "pygit2-1.19.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ed09804dc6b6de0be07a71443122fd7b6458f8466d1134003c2dea55af886fc"},
+ {file = "pygit2-1.19.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d114aa066e718d5ef3401b366dcb0b37b549c3b3b139f5f0042bd7059a4b0f7"},
+ {file = "pygit2-1.19.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1becc06071acfdd5ae8523aaeab6d4b0930b2bcb08f5eb878e052e61275000b"},
+ {file = "pygit2-1.19.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06d2db3bdbf2906eb17112adb14a2fe6e34c1b2bce39c91819f59208d4e56665"},
+ {file = "pygit2-1.19.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8a7e99e5dfc8d3ed8f849b9688bc3fb1bdc86f34af28159140a8d1e18b703dd8"},
+ {file = "pygit2-1.19.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7659d59eba6c4a706978237d02e8d719f960843df749256f1656c938c1f4142b"},
+ {file = "pygit2-1.19.2-cp314-cp314-win32.whl", hash = "sha256:e551908dfd93d471c0b08cfcddbe4924417865aae6ac90d20f3815c9483b0a82"},
+ {file = "pygit2-1.19.2-cp314-cp314-win_amd64.whl", hash = "sha256:eb1fd8538372230f8a471a5f3629901bc2fc7df992853d97bedc8fa269a9caf3"},
+ {file = "pygit2-1.19.2-cp314-cp314-win_arm64.whl", hash = "sha256:3cc461245b70be45a936e925744e67a45f6b0ee970aeb8e7a385dd7fe9f40877"},
+ {file = "pygit2-1.19.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cb686bc81dfe5b13937047643fddb1dd253dae33b4a9ca62858c49ed294e05be"},
+ {file = "pygit2-1.19.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ec3538d81963bd05dd16c0de75938a9173966e1c853ad7848ebcb60bcfe21b0"},
+ {file = "pygit2-1.19.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d02ebb50ea082d9631bbfda12787eb5324b8880a72cb8e3b9f11e9b323ad5781"},
+ {file = "pygit2-1.19.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a3643e4dd569c2909e88586659f617f70315680ca3c619cd8ff9e9c28726c25"},
+ {file = "pygit2-1.19.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:697e3684cb4ef2bfc084623c3f680d5ae8b4c8afca31a35a731b7b70204d9f83"},
+ {file = "pygit2-1.19.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:173165b54a2affed918302193f12dd369bec981b1d77904cdcd76b966a824e15"},
+ {file = "pygit2-1.19.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ff32adce1a48d76b10e790b36784f6cb5ef40699b758c8b84f7f53f13b13d237"},
+ {file = "pygit2-1.19.2-cp314-cp314t-win32.whl", hash = "sha256:637d7c023f6623da35cf02cd1091f260c709730dd615367f4524ec8d771d0898"},
+ {file = "pygit2-1.19.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2805a8abd546e38298ce5daf33e444960e483acce68cbfb5d338e72ad5bc3503"},
+ {file = "pygit2-1.19.2-cp314-cp314t-win_arm64.whl", hash = "sha256:376a0d2c27c082f6bd8b97fd8ffc1939f16dfe8374ec846deee9b11151b37b8a"},
+ {file = "pygit2-1.19.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4c2d397c887ff5a26b48ebd1bb9c66d2195ad377f0a44e05b79c462fff4040cd"},
+ {file = "pygit2-1.19.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:69a0d377ee46110bbeea9e4191edee05132d1e7ac84b7cdebc640bc45868a2ec"},
+ {file = "pygit2-1.19.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57d113a3eb61621ce16ceaa4bae7a93ffe525fd69da905445a0cf798d3601815"},
+ {file = "pygit2-1.19.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e0bc207abbef4d3be3bd37e0711e6974a148d41806fdc932aef9bb244b157c4"},
+ {file = "pygit2-1.19.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:219c03bdbca59bd1df12b8bc7974b429872f4267aa2287ec0237c268593c0c5e"},
+ {file = "pygit2-1.19.2.tar.gz", hash = "sha256:cbeb3dbca9ca6ee3d5ea5d02f5e844c2d6084a2d5d6621e3e06aa2b11c645bfd"},
+]
+
+[package.dependencies]
+cffi = ">=2.0"
+
+[[package]]
+name = "pygithub"
+version = "2.9.0"
+description = "Use the full Github API v3"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pygithub-2.9.0-py3-none-any.whl", hash = "sha256:5e2b260ce327bffce9b00f447b65953ef7078ffe93e5a5425624a3075483927c"},
+ {file = "pygithub-2.9.0.tar.gz", hash = "sha256:a26abda1222febba31238682634cad11d8b966137ed6cc3c5e445b29a11cb0a4"},
+]
+
+[package.dependencies]
+pyjwt = {version = ">=2.4.0", extras = ["crypto"]}
+pynacl = ">=1.4.0"
+requests = ">=2.14.0"
+typing-extensions = ">=4.5.0"
+urllib3 = ">=1.26.0"
+
[[package]]
name = "pygments"
version = "2.19.2"
@@ -1027,13 +1121,76 @@ files = [
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
+[[package]]
+name = "pyjwt"
+version = "2.12.1"
+description = "JSON Web Token implementation in Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"},
+ {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"},
+]
+
+[package.dependencies]
+cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"]
+
+[[package]]
+name = "pynacl"
+version = "1.6.2"
+description = "Python binding to the Networking and Cryptography (NaCl) library"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14"},
+ {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444"},
+ {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2"},
+ {file = "pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6"},
+ {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e"},
+ {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577"},
+ {file = "pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa"},
+ {file = "pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0"},
+ {file = "pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c"},
+ {file = "pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c"},
+]
+
+[package.dependencies]
+cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.9\""}
+
+[package.extras]
+docs = ["sphinx (<7)", "sphinx_rtd_theme"]
+tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+
[[package]]
name = "pytest"
version = "8.4.1"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.9"
-groups = ["main", "dev"]
+groups = ["dev"]
files = [
{file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"},
{file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"},
@@ -1083,50 +1240,6 @@ files = [
[package.dependencies]
six = ">=1.5"
-[[package]]
-name = "python-dotenv"
-version = "1.1.1"
-description = "Read key-value pairs from a .env file and set them as environment variables"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"},
- {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"},
-]
-
-[package.extras]
-cli = ["click (>=5.0)"]
-
-[[package]]
-name = "python-gardenlinux-lib"
-version = "0.6.0"
-description = "Contains tools to work with the features directory of gardenlinux, for example deducting dependencies from feature sets or validating cnames"
-optional = false
-python-versions = "^3.10"
-groups = ["main"]
-files = []
-develop = false
-
-[package.dependencies]
-apt-repo = "^0.5"
-boto3 = "*"
-cryptography = "^44.0.0"
-gitpython = "^3.1.43"
-jsonschema = "^4.23.0"
-networkx = "^3.3"
-oras = {git = "https://github.com/oras-project/oras-py.git", rev = "caf8db5b279382335fbb1f6d7402ed9b73618d37"}
-pytest = "^8.3.2"
-python-dotenv = "^1.0.1"
-PyYAML = "^6.0.2"
-sphinx-rtd-theme = "^2.0.0"
-
-[package.source]
-type = "git"
-url = "https://github.com/gardenlinux/python-gardenlinux-lib.git"
-reference = "0.6.0"
-resolved_reference = "1bcdff00e5b43acbf6c48cb1ac93f22232db8000"
-
[[package]]
name = "pytz"
version = "2025.1"
@@ -1242,134 +1355,146 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rpds-py"
-version = "0.22.3"
+version = "0.30.0"
description = "Python bindings to Rust's persistent data structures (rpds)"
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
groups = ["main"]
files = [
- {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"},
- {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"},
- {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"},
- {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"},
- {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"},
- {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"},
- {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"},
- {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"},
- {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"},
- {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"},
- {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"},
- {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"},
- {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"},
- {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"},
- {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"},
- {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"},
- {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"},
- {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"},
- {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"},
- {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"},
- {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"},
- {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"},
- {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"},
- {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"},
- {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"},
- {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"},
- {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"},
- {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"},
- {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"},
- {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"},
- {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"},
- {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"},
- {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"},
- {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"},
- {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"},
- {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"},
- {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"},
- {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"},
- {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"},
- {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"},
- {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"},
- {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"},
- {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"},
- {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"},
- {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"},
- {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"},
- {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"},
- {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"},
- {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"},
- {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"},
- {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"},
- {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"},
- {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"},
- {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"},
- {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"},
- {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"},
- {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"},
- {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"},
- {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"},
- {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"},
- {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"},
- {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"},
- {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"},
- {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"},
- {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"},
- {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"},
- {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"},
- {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"},
- {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"},
- {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"},
- {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"},
- {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"},
- {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"},
- {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"},
- {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"},
- {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"},
- {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"},
- {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"},
- {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"},
- {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"},
- {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"},
+ {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"},
+ {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"},
+ {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"},
+ {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"},
+ {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"},
+ {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"},
+ {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"},
+ {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"},
+ {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"},
+ {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"},
+ {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"},
+ {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"},
+ {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"},
+ {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"},
+ {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"},
+ {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"},
+ {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"},
+ {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"},
+ {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"},
+ {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"},
+ {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"},
+ {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"},
+ {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"},
+ {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"},
+ {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"},
+ {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"},
+ {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"},
+ {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"},
+ {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"},
+ {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"},
+ {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"},
+ {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"},
+ {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"},
+ {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"},
+ {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"},
+ {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"},
+ {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"},
+ {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"},
+ {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"},
+ {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"},
+ {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"},
+ {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"},
+ {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"},
+ {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"},
+ {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"},
+ {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"},
+ {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"},
+ {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"},
+ {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"},
+ {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"},
+ {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"},
+ {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"},
+ {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"},
+ {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"},
+ {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"},
+ {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"},
+ {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"},
+ {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"},
+ {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"},
+ {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"},
+ {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"},
+ {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"},
+ {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"},
+ {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"},
+ {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"},
+ {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"},
+ {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"},
+ {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"},
+ {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"},
+ {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"},
+ {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"},
+ {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"},
+ {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"},
+ {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"},
+ {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"},
+ {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"},
+ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"},
+ {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"},
]
[[package]]
name = "s3transfer"
-version = "0.13.1"
+version = "0.16.0"
description = "An Amazon S3 Transfer Manager"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724"},
- {file = "s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf"},
+ {file = "s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe"},
+ {file = "s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920"},
]
[package.dependencies]
-botocore = ">=1.37.4,<2.0a.0"
+botocore = ">=1.37.4,<2.0a0"
[package.extras]
-crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
+crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
[[package]]
name = "six"
@@ -1385,198 +1510,16 @@ files = [
[[package]]
name = "smmap"
-version = "5.0.2"
+version = "5.0.3"
description = "A pure Python implementation of a sliding window memory map manager"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
- {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"},
- {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"},
-]
-
-[[package]]
-name = "snowballstemmer"
-version = "3.0.1"
-description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms."
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*"
-groups = ["main"]
-files = [
- {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"},
- {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"},
+ {file = "smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f"},
+ {file = "smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c"},
]
-[[package]]
-name = "sphinx"
-version = "7.4.7"
-description = "Python documentation generator"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"},
- {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"},
-]
-
-[package.dependencies]
-alabaster = ">=0.7.14,<0.8.0"
-babel = ">=2.13"
-colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
-docutils = ">=0.20,<0.22"
-imagesize = ">=1.3"
-Jinja2 = ">=3.1"
-packaging = ">=23.0"
-Pygments = ">=2.17"
-requests = ">=2.30.0"
-snowballstemmer = ">=2.2"
-sphinxcontrib-applehelp = "*"
-sphinxcontrib-devhelp = "*"
-sphinxcontrib-htmlhelp = ">=2.0.0"
-sphinxcontrib-jsmath = "*"
-sphinxcontrib-qthelp = "*"
-sphinxcontrib-serializinghtml = ">=1.1.9"
-
-[package.extras]
-docs = ["sphinxcontrib-websupport"]
-lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"]
-test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"]
-
-[[package]]
-name = "sphinx-rtd-theme"
-version = "2.0.0"
-description = "Read the Docs theme for Sphinx"
-optional = false
-python-versions = ">=3.6"
-groups = ["main"]
-files = [
- {file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"},
- {file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"},
-]
-
-[package.dependencies]
-docutils = "<0.21"
-sphinx = ">=5,<8"
-sphinxcontrib-jquery = ">=4,<5"
-
-[package.extras]
-dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"]
-
-[[package]]
-name = "sphinxcontrib-applehelp"
-version = "2.0.0"
-description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"},
- {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["pytest"]
-
-[[package]]
-name = "sphinxcontrib-devhelp"
-version = "2.0.0"
-description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"},
- {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["pytest"]
-
-[[package]]
-name = "sphinxcontrib-htmlhelp"
-version = "2.1.0"
-description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"},
- {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["html5lib", "pytest"]
-
-[[package]]
-name = "sphinxcontrib-jquery"
-version = "4.1"
-description = "Extension to include jQuery on newer Sphinx releases"
-optional = false
-python-versions = ">=2.7"
-groups = ["main"]
-files = [
- {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"},
- {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"},
-]
-
-[package.dependencies]
-Sphinx = ">=1.8"
-
-[[package]]
-name = "sphinxcontrib-jsmath"
-version = "1.0.1"
-description = "A sphinx extension which renders display math in HTML via JavaScript"
-optional = false
-python-versions = ">=3.5"
-groups = ["main"]
-files = [
- {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
- {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
-]
-
-[package.extras]
-test = ["flake8", "mypy", "pytest"]
-
-[[package]]
-name = "sphinxcontrib-qthelp"
-version = "2.0.0"
-description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"},
- {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["defusedxml (>=0.7.1)", "pytest"]
-
-[[package]]
-name = "sphinxcontrib-serializinghtml"
-version = "2.0.0"
-description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
- {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"},
- {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"},
-]
-
-[package.extras]
-lint = ["mypy", "ruff (==0.5.5)", "types-docutils"]
-standalone = ["Sphinx (>=5)"]
-test = ["pytest"]
-
[[package]]
name = "tabulate"
version = "0.9.0"
@@ -1604,6 +1547,18 @@ files = [
{file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"},
]
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+]
+
[[package]]
name = "urllib3"
version = "2.3.0"
@@ -1624,5 +1579,5 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.1"
-python-versions = "^3.13"
-content-hash = "87163a9342b29561bcc028f2f603b9155cba7303cc3aafd1ae661931125ff2a1"
+python-versions = ">=3.13,<3.14.1"
+content-hash = "b39fb0368db979f131fa28eaa84b538c5f95005f5f4d27e94db33a06f6dd41ba"
diff --git a/pyproject.toml b/pyproject.toml
index 376a16d..231dbf1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,14 +4,14 @@ dynamic = ["version"]
[tool.poetry]
name = "glrd"
-version = "0.0.0"
+version = "4.2.0.post12.dev0+d0979c4.dirty"
description = "Garden Linux Release Database"
authors = ["Garden Linux Team"]
readme = "README.md"
packages = [{include = "glrd"}]
[tool.poetry.dependencies]
-python = "^3.13"
+python = ">=3.13,<3.14.1"
poetry-dynamic-versioning = "*"
boto3 = "*"
deepdiff = "*"
@@ -19,9 +19,10 @@ jsonschema = "*"
PyYAML = "*"
pytz = "*"
python-dateutil = "*"
+pygit2 = "*"
requests = "*"
tabulate = "*"
-python-gardenlinux-lib = { git = "https://github.com/gardenlinux/python-gardenlinux-lib.git", branch = "0.6.0" }
+gardenlinux = { git = "https://github.com/gardenlinux/python-gardenlinux-lib.git", tag = "0.10.21" }
[tool.poetry.group.dev.dependencies]
pytest = "*"
@@ -31,7 +32,7 @@ flake8 = "*"
autopep8 = "*"
[tool.poetry-dynamic-versioning]
-enable = true
+enable = false
vcs = "git"
style = "pep440"
pattern = '''(?x)
diff --git a/tests/conftest.py b/tests/conftest.py
index 46cdff4..26064c1 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,6 +9,21 @@
from pathlib import Path
+@pytest.fixture(autouse=True)
+def offline_environment(monkeypatch):
+ """
+ Force GLRD into fully offline mode for every test.
+
+ The tests must never touch the network or S3. Setting GLRD_SKIP_FLAVORS
+ makes glrd-manage/glrd-update skip flavor resolution (the Git clone and S3
+ lookup), and disabling the EC2 metadata endpoint prevents boto3 from
+ hanging on credential discovery. Because these are set via the environment,
+ they are inherited by the subprocesses that the integration tests spawn.
+ """
+ monkeypatch.setenv("GLRD_SKIP_FLAVORS", "1")
+ monkeypatch.setenv("AWS_EC2_METADATA_DISABLED", "true")
+
+
@pytest.fixture
def test_dir():
"""Create a temporary directory for test files."""
diff --git a/tests/test_git.py b/tests/test_git.py
new file mode 100644
index 0000000..80b6481
--- /dev/null
+++ b/tests/test_git.py
@@ -0,0 +1,344 @@
+"""
+Unit tests for glrd/git.py.
+
+GitHub API calls are mocked at the gardenlinux.github.Client boundary.
+pygit2 repository walks use a local in-memory fixture via pygit2.
+Tests that require a live GITHUB_TOKEN are gated with pytest.mark.skipif.
+"""
+
+from datetime import datetime, timezone
+from unittest.mock import MagicMock, patch
+
+import pygit2
+import pytest
+import pytz
+
+import glrd.git as git_module
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+
+def _make_mock_release(tag_name: str, published_at: str, html_url: str):
+ """Return a MagicMock that mimics a PyGithub GitRelease object."""
+ r = MagicMock()
+ r.tag_name = tag_name
+ r.published_at = datetime.strptime(published_at, "%Y-%m-%dT%H:%M:%SZ").replace(
+ tzinfo=timezone.utc
+ )
+ r.html_url = html_url
+ return r
+
+
+def _make_mock_ref(sha: str, obj_type: str = "commit"):
+ """Return a MagicMock mimicking a PyGithub GitRef / GitRef.object."""
+ ref = MagicMock()
+ ref.object.sha = sha
+ ref.object.type = obj_type
+ return ref
+
+
+# ---------------------------------------------------------------------------
+# get_github_releases
+# ---------------------------------------------------------------------------
+
+
+class TestGetGithubReleases:
+ def test_returns_dict_list_with_expected_keys(self):
+ mock_releases = [
+ _make_mock_release(
+ "1592.1",
+ "2024-08-22T00:00:00Z",
+ "https://github.com/gardenlinux/gardenlinux/releases/tag/1592.1",
+ ),
+ _make_mock_release(
+ "1592",
+ "2024-08-01T00:00:00Z",
+ "https://github.com/gardenlinux/gardenlinux/releases/tag/1592",
+ ),
+ ]
+
+ mock_repo = MagicMock()
+ mock_repo.get_releases.return_value = mock_releases
+
+ mock_client_instance = MagicMock()
+ mock_client_instance.get_repo.return_value = mock_repo
+
+ with patch("glrd.git.Client", return_value=mock_client_instance):
+ result = git_module.get_github_releases()
+
+ assert len(result) == 2
+ for item in result:
+ assert "tag_name" in item
+ assert "published_at" in item
+ assert "html_url" in item
+
+ assert result[0]["tag_name"] == "1592.1"
+ assert result[0]["published_at"] == "2024-08-22T00:00:00Z"
+ assert result[1]["tag_name"] == "1592"
+
+ def test_missing_token_exits(self, monkeypatch):
+ monkeypatch.delenv("GITHUB_TOKEN", raising=False)
+ with patch("glrd.git.Client", side_effect=ValueError("GITHUB_TOKEN not set")):
+ with pytest.raises(SystemExit):
+ git_module.get_github_releases()
+
+ def test_api_error_exits(self):
+ mock_client_instance = MagicMock()
+ mock_client_instance.get_repo.side_effect = Exception("API error")
+
+ with patch("glrd.git.Client", return_value=mock_client_instance):
+ with pytest.raises(SystemExit):
+ git_module.get_github_releases()
+
+
+# ---------------------------------------------------------------------------
+# get_git_commit_from_tag
+# ---------------------------------------------------------------------------
+
+
+class TestGetGitCommitFromTag:
+ def test_lightweight_tag_returns_sha(self):
+ sha = "abc123def456" + "0" * 28
+ mock_ref = _make_mock_ref(sha, obj_type="commit")
+
+ mock_repo = MagicMock()
+ mock_repo.get_git_ref.return_value = mock_ref
+
+ mock_client_instance = MagicMock()
+ mock_client_instance.get_repo.return_value = mock_repo
+
+ with patch("glrd.git.Client", return_value=mock_client_instance):
+ full, short = git_module.get_git_commit_from_tag("1592.1")
+
+ assert full == sha
+ assert short == sha[:8]
+ mock_repo.get_git_ref.assert_called_once_with("tags/1592.1")
+
+ def test_annotated_tag_is_dereferenced(self):
+ tag_sha = "tagobj" + "0" * 34
+ commit_sha = "realcommit" + "0" * 30
+
+ mock_tag_obj = MagicMock()
+ mock_tag_obj.object.sha = commit_sha
+
+ mock_ref = _make_mock_ref(tag_sha, obj_type="tag")
+
+ mock_repo = MagicMock()
+ mock_repo.get_git_ref.return_value = mock_ref
+ mock_repo.get_git_tag.return_value = mock_tag_obj
+
+ mock_client_instance = MagicMock()
+ mock_client_instance.get_repo.return_value = mock_repo
+
+ with patch("glrd.git.Client", return_value=mock_client_instance):
+ full, short = git_module.get_git_commit_from_tag("1592")
+
+ assert full == commit_sha
+ assert short == commit_sha[:8]
+ mock_repo.get_git_tag.assert_called_once_with(tag_sha)
+
+ def test_missing_token_exits(self, monkeypatch):
+ monkeypatch.delenv("GITHUB_TOKEN", raising=False)
+ with patch("glrd.git.Client", side_effect=ValueError("GITHUB_TOKEN not set")):
+ with pytest.raises(SystemExit):
+ git_module.get_git_commit_from_tag("1592.1")
+
+ def test_api_error_exits(self):
+ mock_client_instance = MagicMock()
+ mock_client_instance.get_repo.side_effect = Exception("API error")
+
+ with patch("glrd.git.Client", return_value=mock_client_instance):
+ with pytest.raises(SystemExit):
+ git_module.get_git_commit_from_tag("1592.1")
+
+
+# ---------------------------------------------------------------------------
+# get_git_commit_at_time — uses a real in-memory pygit2 repo as fixture
+# ---------------------------------------------------------------------------
+
+
+@pytest.fixture()
+def local_git_repo(tmp_path):
+ """
+ Create a minimal bare+non-bare pair of pygit2 repos so that
+ get_git_commit_at_time can clone from a local file:// URL.
+
+ Returns a dict with keys:
+ repo_url – file:// URL to the *bare* origin
+ sha_old – SHA of the earlier commit (committed at epoch 1_000_000)
+ sha_new – SHA of the later commit (committed at epoch 2_000_000)
+ """
+ bare_path = tmp_path / "origin.git"
+ bare_path.mkdir()
+ bare_repo = pygit2.init_repository(str(bare_path), bare=True)
+
+ sig_old = pygit2.Signature("Test", "test@example.com", time=1_000_000, offset=0)
+ sig_new = pygit2.Signature("Test", "test@example.com", time=2_000_000, offset=0)
+
+ # Initial commit
+ tb = bare_repo.TreeBuilder()
+ tree_oid = tb.write()
+ old_commit_oid = bare_repo.create_commit(
+ "refs/heads/main",
+ sig_old,
+ sig_old,
+ "first commit",
+ tree_oid,
+ [],
+ )
+
+ # Second commit
+ tb2 = bare_repo.TreeBuilder()
+ tree_oid2 = tb2.write()
+ new_commit_oid = bare_repo.create_commit(
+ "refs/heads/main",
+ sig_new,
+ sig_new,
+ "second commit",
+ tree_oid2,
+ [old_commit_oid],
+ )
+
+ return {
+ "repo_url": bare_path.as_uri(),
+ "sha_old": str(old_commit_oid),
+ "sha_new": str(new_commit_oid),
+ }
+
+
+class TestGetGitCommitAtTime:
+ def setup_method(self):
+ # Reset module-level cache before each test.
+ git_module._repo_clone_path = None
+ git_module._repo_instance = None
+
+ def teardown_method(self):
+ # Clean up any clone created during the test.
+ git_module.cleanup_temp_repo()
+
+ def test_returns_commit_before_target_time(self, local_git_repo):
+ # epoch 1_500_000 is between old (1_000_000) and new (2_000_000)
+ # so the walk should return old_commit (the last one <= target).
+ target_dt = datetime.fromtimestamp(1_500_000, tz=pytz.UTC)
+ date_str = target_dt.strftime("%Y-%m-%d")
+ time_str = target_dt.strftime("%H:%M")
+
+ full, short = git_module.get_git_commit_at_time(
+ date=date_str,
+ time=time_str,
+ branch="main",
+ remote_repo=local_git_repo["repo_url"],
+ )
+
+ assert full == local_git_repo["sha_old"]
+ assert short == local_git_repo["sha_old"][:8]
+
+ def test_clone_is_cached(self, local_git_repo):
+ target_dt = datetime.fromtimestamp(1_500_000, tz=pytz.UTC)
+ date_str = target_dt.strftime("%Y-%m-%d")
+ time_str = target_dt.strftime("%H:%M")
+
+ with patch(
+ "glrd.git.Repository.checkout_repo",
+ wraps=git_module.Repository.checkout_repo,
+ ) as p:
+ git_module.get_git_commit_at_time(
+ date=date_str,
+ time=time_str,
+ branch="main",
+ remote_repo=local_git_repo["repo_url"],
+ )
+ first_path = git_module._repo_clone_path
+
+ # Second call must NOT clone again.
+ git_module.get_git_commit_at_time(
+ date=date_str,
+ time=time_str,
+ branch="main",
+ remote_repo=local_git_repo["repo_url"],
+ )
+ assert git_module._repo_clone_path == first_path
+ assert p.call_count == 1
+
+ def test_no_commit_before_very_early_date_exits(self, local_git_repo):
+ # epoch 100 is before all commits; should exit.
+ target_dt = datetime.fromtimestamp(100, tz=pytz.UTC)
+ date_str = target_dt.strftime("%Y-%m-%d")
+ time_str = target_dt.strftime("%H:%M")
+
+ with pytest.raises(SystemExit):
+ git_module.get_git_commit_at_time(
+ date=date_str,
+ time=time_str,
+ branch="main",
+ remote_repo=local_git_repo["repo_url"],
+ )
+
+
+# ---------------------------------------------------------------------------
+# cleanup_temp_repo
+# ---------------------------------------------------------------------------
+
+
+class TestCleanupTempRepo:
+ def test_cleanup_removes_directory(self, tmp_path):
+ clone_dir = tmp_path / "clone"
+ clone_dir.mkdir()
+
+ git_module._repo_clone_path = str(clone_dir)
+ git_module._repo_instance = MagicMock()
+
+ git_module.cleanup_temp_repo()
+
+ assert not clone_dir.exists()
+ assert git_module._repo_clone_path is None
+ assert git_module._repo_instance is None
+
+ def test_cleanup_is_idempotent(self):
+ git_module._repo_clone_path = None
+ git_module._repo_instance = None
+ # Should not raise.
+ git_module.cleanup_temp_repo()
+
+
+# ---------------------------------------------------------------------------
+# get_garden_version_for_date (pure logic — no mocking needed)
+# ---------------------------------------------------------------------------
+
+
+class TestGetGardenVersionForDate:
+ def test_major_version_is_days_since_base(self):
+ base_date = datetime(2020, 3, 31, tzinfo=pytz.UTC)
+ test_date = datetime(2024, 8, 1, tzinfo=pytz.UTC)
+ expected_major = (test_date - base_date).days
+
+ major, minor, patch = git_module.get_garden_version_for_date(
+ "major", test_date, []
+ )
+ assert major == expected_major
+ assert minor == 0
+ assert patch == 0
+
+ def test_nightly_minor_increments(self):
+ base_date = datetime(2020, 3, 31, tzinfo=pytz.UTC)
+ test_date = datetime(2024, 8, 1, tzinfo=pytz.UTC)
+ major = (test_date - base_date).days
+
+ existing = [
+ {
+ "type": "nightly",
+ "version": {"major": major, "minor": 0, "patch": 0},
+ }
+ ]
+
+ major2, minor2, patch2 = git_module.get_garden_version_for_date(
+ "nightly", test_date, existing
+ )
+ assert major2 == major
+ # minor increments from 0 to 1; patch restarts at 0+1=1 because the
+ # existing release (minor=0, patch=0) is counted before incrementing
+ assert minor2 == 1
+ assert patch2 == 1
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 8d8cc25..2bdca91 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -20,7 +20,20 @@ class TestGLRDIntegration:
"""Integration tests for GLRD manage and query commands."""
def run_manage_command(self, manage_script, args, expect_success=True):
- """Run glrd-manage command and return result."""
+ """Run glrd-manage command and return result.
+
+ When the command queries existing releases (i.e. it does not pass
+ ``--no-query`` and does not already choose an ``--input-type``), a
+ non-existent local input prefix is injected so the query runs fully
+ offline instead of reaching out to the production S3 URL.
+ """
+ if "--input-type" not in args and "--no-query" not in args:
+ args = args + [
+ "--input-type",
+ "file",
+ "--input-file-prefix",
+ os.path.join(os.path.dirname(manage_script), "does-not-exist-glrd"),
+ ]
cmd = [sys.executable, manage_script] + args
result = subprocess.run(
cmd,
@@ -46,7 +59,18 @@ def run_manage_command(self, manage_script, args, expect_success=True):
def run_manage_command_stdin(
self, manage_script, args, stdin_data, expect_success=True
):
- """Run glrd-manage command with data on stdin and return result."""
+ """Run glrd-manage command with data on stdin and return result.
+
+ Injects an offline local input prefix when the command queries
+ existing releases, so no network/S3 access is attempted.
+ """
+ if "--input-type" not in args and "--no-query" not in args:
+ args = args + [
+ "--input-type",
+ "file",
+ "--input-file-prefix",
+ os.path.join(os.path.dirname(manage_script), "does-not-exist-glrd"),
+ ]
cmd = [sys.executable, manage_script] + args
result = subprocess.run(
cmd,
@@ -120,6 +144,8 @@ def test_v1_schema_valid_versions(
"nightly",
"--version",
version,
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -167,6 +193,8 @@ def test_v1_schema_invalid_versions(self, test_dir, manage_script, version):
"nightly",
"--version",
version,
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -208,6 +236,8 @@ def test_v2_schema_valid_versions(
"nightly",
"--version",
version,
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -256,6 +286,8 @@ def test_v2_schema_invalid_versions(self, test_dir, manage_script, version):
"nightly",
"--version",
version,
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -288,6 +320,8 @@ def test_boundary_version_validation(self, test_dir, manage_script):
"nightly",
"--version",
"2017.0",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -308,6 +342,8 @@ def test_boundary_version_validation(self, test_dir, manage_script):
"nightly",
"--version",
"2017.0.0",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -331,6 +367,8 @@ def test_schema_error_message_clarity(self, test_dir, manage_script):
"nightly",
"--version",
"1990.0.1",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -352,6 +390,8 @@ def test_schema_error_message_clarity(self, test_dir, manage_script):
"nightly",
"--version",
"2222.0",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -504,6 +544,8 @@ def test_dev_release_validation(self, test_dir, manage_script):
"dev",
"--version",
"1990.0",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -529,6 +571,8 @@ def test_dev_release_validation(self, test_dir, manage_script):
"dev",
"--version",
"2017.0.0",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -559,6 +603,8 @@ def test_glrd_query_verification(self, test_dir, manage_script, query_script):
"nightly",
"--version",
"1990.0",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -652,6 +698,8 @@ def test_query_created_dev_release(self, test_dir, manage_script, query_script):
"dev",
"--version",
"1990.0",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -853,6 +901,8 @@ def test_multiple_releases_query(self, test_dir, manage_script, query_script):
"nightly",
"--version",
version,
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
"--output-format",
"json",
"--output-file-prefix",
@@ -1158,3 +1208,223 @@ def test_create_releases_with_custom_commit(self, test_dir, manage_script):
# Note: next releases don't use git commit info and require lifecycle dates,
# so we skip testing next releases with custom commit
+
+ # ============================================================================
+ # QUERY FILTER TESTS (offline: seed local files, query with filters)
+ # ============================================================================
+
+ def _seed_minor_releases_file(self, test_dir, manage_script, prefix):
+ """Create a local releases-minor.json with one archived and one active
+ release, plus a newer active patch, and return the file path.
+
+ Uses far-past and far-future EOL timestamps so the active/archived
+ classification is stable regardless of the current date.
+ """
+ releases_json = {
+ "releases": [
+ {
+ "name": "minor-2017.0.0",
+ "type": "minor",
+ "version": {"major": 2017, "minor": 0, "patch": 0},
+ "lifecycle": {
+ "released": {"isodate": "2020-01-01", "timestamp": 1577836800},
+ # EOL in the far past -> archived
+ "eol": {"isodate": "2020-06-01", "timestamp": 1590969600},
+ },
+ "git": {
+ "commit": "a" * 40,
+ "commit_short": "aaaaaaaa",
+ },
+ "github": {
+ "release": (
+ "https://github.com/gardenlinux/gardenlinux/"
+ "releases/tag/2017.0.0"
+ )
+ },
+ "flavors": ["container-amd64"],
+ "attributes": {"source_repo": True},
+ },
+ {
+ "name": "minor-2017.0.1",
+ "type": "minor",
+ "version": {"major": 2017, "minor": 0, "patch": 1},
+ "lifecycle": {
+ "released": {"isodate": "2020-06-01", "timestamp": 1590969600},
+ # EOL in the far future -> active
+ "eol": {"isodate": "2999-01-01", "timestamp": 32472144000},
+ },
+ "git": {
+ "commit": "b" * 40,
+ "commit_short": "bbbbbbbb",
+ },
+ "github": {
+ "release": (
+ "https://github.com/gardenlinux/gardenlinux/"
+ "releases/tag/2017.0.1"
+ )
+ },
+ "flavors": ["container-amd64"],
+ "attributes": {"source_repo": True},
+ },
+ ]
+ }
+ self.run_manage_command_stdin(
+ manage_script,
+ [
+ "--input-stdin",
+ "--output-format",
+ "json",
+ "--output-file-prefix",
+ prefix,
+ "--no-query",
+ ],
+ json.dumps(releases_json),
+ )
+ return f"{prefix}-minor.json"
+
+ def test_query_active_filter(self, test_dir, manage_script, query_script):
+ """--active returns only releases whose EOL is in the future."""
+ prefix = os.path.join(test_dir, "releases-active")
+ self._seed_minor_releases_file(test_dir, manage_script, prefix)
+
+ result = self.run_query_command(
+ query_script,
+ [
+ "--type",
+ "minor",
+ "--active",
+ "--input-type",
+ "file",
+ "--input-file-prefix",
+ prefix,
+ "--output-format",
+ "json",
+ ],
+ )
+ data = json.loads(result.stdout)
+ names = {r["name"] for r in data["releases"]}
+ assert names == {"minor-2017.0.1"}
+
+ def test_query_archived_filter(self, test_dir, manage_script, query_script):
+ """--archived returns only releases whose EOL is in the past."""
+ prefix = os.path.join(test_dir, "releases-archived")
+ self._seed_minor_releases_file(test_dir, manage_script, prefix)
+
+ result = self.run_query_command(
+ query_script,
+ [
+ "--type",
+ "minor",
+ "--archived",
+ "--input-type",
+ "file",
+ "--input-file-prefix",
+ prefix,
+ "--output-format",
+ "json",
+ ],
+ )
+ data = json.loads(result.stdout)
+ names = {r["name"] for r in data["releases"]}
+ assert names == {"minor-2017.0.0"}
+
+ def test_query_latest_filter(self, test_dir, manage_script, query_script):
+ """--latest returns the single highest-versioned release."""
+ prefix = os.path.join(test_dir, "releases-latest")
+ self._seed_minor_releases_file(test_dir, manage_script, prefix)
+
+ result = self.run_query_command(
+ query_script,
+ [
+ "--type",
+ "minor",
+ "--latest",
+ "--input-type",
+ "file",
+ "--input-file-prefix",
+ prefix,
+ "--output-format",
+ "json",
+ ],
+ )
+ data = json.loads(result.stdout)
+ assert len(data["releases"]) == 1
+ assert data["releases"][0]["name"] == "minor-2017.0.1"
+
+ def test_query_unknown_type_is_ignored(self, test_dir, manage_script, query_script):
+ """A mix of valid and unknown --type values must not crash.
+
+ Regression test: unknown types are ignored rather than raising, so
+ `--type minor,bogus` returns the same result as `--type minor`.
+ """
+ prefix = os.path.join(test_dir, "releases-unknown-type")
+ self._seed_minor_releases_file(test_dir, manage_script, prefix)
+
+ result = self.run_query_command(
+ query_script,
+ [
+ "--type",
+ "minor,bogus",
+ "--input-type",
+ "file",
+ "--input-file-prefix",
+ prefix,
+ "--output-format",
+ "json",
+ ],
+ )
+ data = json.loads(result.stdout)
+ names = {r["name"] for r in data["releases"]}
+ assert names == {"minor-2017.0.0", "minor-2017.0.1"}
+
+ def test_query_version_filter(self, test_dir, manage_script, query_script):
+ """--version filters to an exact major.minor.patch."""
+ prefix = os.path.join(test_dir, "releases-version")
+ self._seed_minor_releases_file(test_dir, manage_script, prefix)
+
+ result = self.run_query_command(
+ query_script,
+ [
+ "--type",
+ "minor",
+ "--version",
+ "2017.0.0",
+ "--input-type",
+ "file",
+ "--input-file-prefix",
+ prefix,
+ "--output-format",
+ "json",
+ ],
+ )
+ data = json.loads(result.stdout)
+ names = {r["name"] for r in data["releases"]}
+ assert names == {"minor-2017.0.0"}
+
+ def test_create_no_flavors_flag(self, test_dir, manage_script):
+ """--no-flavors creates a release offline with an empty flavors list."""
+ prefix = os.path.join(test_dir, "releases-noflavors")
+ output_file = f"{prefix}-nightly.json"
+
+ self.run_manage_command(
+ manage_script,
+ [
+ "--create",
+ "nightly",
+ "--version",
+ "2017.0.0",
+ "--commit",
+ "deadbeef1234567890abcdef1234567890abcdef",
+ "--no-flavors",
+ "--output-format",
+ "json",
+ "--output-file-prefix",
+ prefix,
+ "--no-query",
+ ],
+ )
+
+ data = self.load_json_output(output_file)
+ release = data["releases"][0]
+ assert release["name"] == "nightly-2017.0.0"
+ assert release["flavors"] == []
diff --git a/tests/test_release_model.py b/tests/test_release_model.py
new file mode 100644
index 0000000..a55d3e9
--- /dev/null
+++ b/tests/test_release_model.py
@@ -0,0 +1,275 @@
+"""
+Unit tests for the release domain model (glrd/release.py).
+
+The release model is the single source of truth for parsing, version handling,
+name generation, and serialization. These tests are pure and fully offline.
+"""
+
+import pytest
+
+from glrd.release import (
+ GitInfo,
+ Lifecycle,
+ LifecyclePhase,
+ Release,
+ ReleaseCollection,
+ ReleaseType,
+ Version,
+ parse_release_name,
+)
+
+
+@pytest.mark.unit
+class TestVersion:
+ """Tests for the Version value object."""
+
+ def test_uses_patch_threshold(self):
+ assert Version(2017, 0, 0).uses_patch is True
+ assert Version(2016, 9).uses_patch is False
+ # Exactly at the v2 threshold
+ assert Version(2017).uses_patch is True
+
+ def test_next_sentinel(self):
+ v = Version("next")
+ assert v.is_next is True
+ assert v.uses_patch is False
+ assert v.to_string(ReleaseType.NEXT) == "next"
+ # 'next' sorts after every numeric version
+ assert v.to_sort_key() > Version(9999, 9, 9).to_sort_key()
+
+ def test_to_string_respects_type_and_schema(self):
+ assert Version(2017, 0, 1).to_string(ReleaseType.MINOR) == "2017.0.1"
+ assert Version(1990, 0).to_string(ReleaseType.NIGHTLY) == "1990.0"
+ assert Version(27).to_string(ReleaseType.MAJOR) == "27"
+
+ def test_from_string_roundtrip(self):
+ assert Version.from_string("2017.0.1") == Version(2017, 0, 1)
+ assert Version.from_string("1990.0") == Version(1990, 0)
+ assert Version.from_string("next").is_next
+
+ def test_sorting_v1_ignores_patch(self):
+ # v1 versions (< 2017) compare without patch
+ assert Version(1990, 0).to_sort_key() == Version(1990, 0).to_sort_key()
+
+
+@pytest.mark.unit
+class TestParseReleaseName:
+ """Tests for release-name parsing."""
+
+ def test_valid_names(self):
+ assert parse_release_name("minor-2017.0.0") == (
+ ReleaseType.MINOR,
+ 2017,
+ 0,
+ 0,
+ )
+ assert parse_release_name("major-27") == (ReleaseType.MAJOR, 27, None, None)
+ assert parse_release_name("nightly-1990.0") == (
+ ReleaseType.NIGHTLY,
+ 1990,
+ 0,
+ None,
+ )
+
+ def test_invalid_type(self):
+ with pytest.raises(ValueError):
+ parse_release_name("bogus-1.2.3")
+
+ def test_missing_dash(self):
+ with pytest.raises(ValueError):
+ parse_release_name("next")
+
+ def test_non_integer_version(self):
+ with pytest.raises(ValueError):
+ parse_release_name("minor-abc.def")
+
+
+@pytest.mark.unit
+class TestReleaseSerialization:
+ """Tests for Release.from_dict/to_dict round-tripping."""
+
+ def _minor(self):
+ return {
+ "name": "minor-2017.0.0",
+ "type": "minor",
+ "version": {"major": 2017, "minor": 0, "patch": 0},
+ "lifecycle": {
+ "released": {"isodate": "2025-01-01", "timestamp": 1735689600},
+ "eol": {"isodate": "2025-10-01", "timestamp": 1759276800},
+ },
+ "git": {
+ "commit": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
+ "commit_short": "a94a8fe5",
+ },
+ "github": {
+ "release": (
+ "https://github.com/gardenlinux/gardenlinux/releases/tag/2017.0.0"
+ )
+ },
+ "flavors": ["container-amd64"],
+ "attributes": {"source_repo": True},
+ }
+
+ def test_minor_roundtrip(self):
+ data = self._minor()
+ assert Release.from_dict(data).to_dict() == data
+
+ def test_v1_minor_omits_patch(self):
+ data = {
+ "name": "minor-1990.0",
+ "type": "minor",
+ "version": {"major": 1990, "minor": 0},
+ "lifecycle": {
+ "released": {"isodate": "2025-01-01", "timestamp": 1735689600},
+ "eol": {"isodate": "2025-10-01", "timestamp": 1759276800},
+ },
+ "git": {
+ "commit": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
+ "commit_short": "a94a8fe5",
+ },
+ "github": {
+ "release": (
+ "https://github.com/gardenlinux/gardenlinux/releases/tag/1990.0"
+ )
+ },
+ "flavors": ["container-amd64"],
+ "attributes": {"source_repo": True},
+ }
+ out = Release.from_dict(data).to_dict()
+ assert "patch" not in out["version"]
+ assert out == data
+
+ def test_next_roundtrip(self):
+ data = {
+ "name": "next",
+ "type": "next",
+ "version": {"major": "next"},
+ "lifecycle": {
+ "released": {"isodate": "2025-01-01", "timestamp": 1735689600},
+ "extended": {"isodate": "2025-07-01", "timestamp": 1751328000},
+ "eol": {"isodate": "2025-10-01", "timestamp": 1759276800},
+ },
+ }
+ assert Release.from_dict(data).to_dict() == data
+
+ def test_empty_flavors_preserved(self):
+ # A freshly created release with no flavors keeps the empty list.
+ data = self._minor()
+ data["flavors"] = []
+ assert Release.from_dict(data).to_dict()["flavors"] == []
+
+
+@pytest.mark.unit
+class TestNameAndUrlGeneration:
+ """Tests that the model owns name and URL generation."""
+
+ def test_default_name(self):
+ assert Release.default_name(ReleaseType.NEXT, Version("next")) == "next"
+ assert Release.default_name(ReleaseType.MAJOR, Version(27)) == "major-27"
+ assert (
+ Release.default_name(ReleaseType.MINOR, Version(2017, 0, 0))
+ == "minor-2017.0.0"
+ )
+ assert (
+ Release.default_name(ReleaseType.NIGHTLY, Version(1990, 0))
+ == "nightly-1990.0"
+ )
+
+ def test_github_url(self):
+ assert Release.github_release_url(Version(2017, 0, 0), ReleaseType.MINOR) == (
+ "https://github.com/gardenlinux/gardenlinux/releases/tag/2017.0.0"
+ )
+ assert Release.github_release_url(Version(1990, 0), ReleaseType.MINOR) == (
+ "https://github.com/gardenlinux/gardenlinux/releases/tag/1990.0"
+ )
+
+
+@pytest.mark.unit
+class TestLifecycle:
+ """Tests for lifecycle active/archived logic."""
+
+ def test_active_when_eol_in_future(self):
+ lc = Lifecycle(
+ released=LifecyclePhase("2025-01-01", 1735689600),
+ eol=LifecyclePhase("2999-01-01", 32472144000),
+ )
+ assert lc.is_active(current_timestamp=1_800_000_000) is True
+ assert lc.is_archived(current_timestamp=1_800_000_000) is False
+
+ def test_archived_when_eol_in_past(self):
+ lc = Lifecycle(
+ released=LifecyclePhase("2020-01-01", 1577836800),
+ eol=LifecyclePhase("2021-01-01", 1609459200),
+ )
+ assert lc.is_active(current_timestamp=1_800_000_000) is False
+ assert lc.is_archived(current_timestamp=1_800_000_000) is True
+
+ def test_no_eol_is_neither_active_nor_archived(self):
+ lc = Lifecycle(released=LifecyclePhase("2020-01-01", 1577836800))
+ assert lc.is_active(current_timestamp=1_800_000_000) is False
+ assert lc.is_archived(current_timestamp=1_800_000_000) is False
+
+
+@pytest.mark.unit
+class TestReleaseCollection:
+ """Tests for filtering, sorting, and latest selection."""
+
+ def _make(self, name, rtype, major, minor=None, patch=None, eol_ts=None):
+ return Release(
+ name=name,
+ type=ReleaseType(rtype),
+ version=Version(major, minor, patch),
+ lifecycle=Lifecycle(
+ released=LifecyclePhase("2025-01-01", 1735689600),
+ eol=LifecyclePhase("2999-01-01", eol_ts) if eol_ts else None,
+ ),
+ git=(
+ GitInfo("a" * 40, "aaaaaaaa")
+ if rtype in ("minor", "nightly", "dev")
+ else None
+ ),
+ )
+
+ def test_by_type(self):
+ col = ReleaseCollection(
+ [
+ self._make("major-27", "major", 27),
+ self._make("minor-2017.0.0", "minor", 2017, 0, 0),
+ ]
+ )
+ assert len(col.by_type(ReleaseType.MAJOR)) == 1
+ assert len(col.by_types([ReleaseType.MAJOR, ReleaseType.MINOR])) == 2
+
+ def test_latest_and_sorted_put_next_last(self):
+ col = ReleaseCollection(
+ [
+ self._make("minor-2017.0.0", "minor", 2017, 0, 0),
+ self._make("next", "next", "next"),
+ self._make("minor-2017.0.5", "minor", 2017, 0, 5),
+ ]
+ )
+ latest = col.latest()
+ assert latest.name == "next" # next sorts to the end == "latest"
+ names = [r.name for r in col.sorted()]
+ assert names[-1] == "next"
+ assert names.index("minor-2017.0.0") < names.index("minor-2017.0.5")
+
+ def test_filter_version_treats_missing_minor_as_zero(self):
+ col = ReleaseCollection([self._make("major-27", "major", 27)])
+ # A major release (no minor) should match --version 27.0
+ assert len(col.filter_version(27, 0)) == 1
+
+ def test_sorted_v1_missing_minor_treated_as_zero(self):
+ # Characterization: a major-only v1 version and a x.0 version share the
+ # same normalized sort key (missing minor == 0), and both sort before
+ # x.1. This pins the intended ordering after the model refactor.
+ col = ReleaseCollection(
+ [
+ self._make("minor-1990.1", "minor", 1990, 1),
+ self._make("major-1990", "major", 1990),
+ self._make("minor-1990.0", "minor", 1990, 0),
+ ]
+ )
+ names = [r.name for r in col.sorted()]
+ assert names[-1] == "minor-1990.1"
+ assert Version(1990).to_sort_key() == Version(1990, 0).to_sort_key()
diff --git a/tests/test_schema_validation.py b/tests/test_schema_validation.py
index c8e7c48..8727d31 100644
--- a/tests/test_schema_validation.py
+++ b/tests/test_schema_validation.py
@@ -3,7 +3,7 @@
"""
import pytest
-from glrd.manage import validate_input_version_format
+from glrd.validation import validate_input_version_format
@pytest.mark.unit
diff --git a/tests/test_update.py b/tests/test_update.py
index 8a08693..c841372 100644
--- a/tests/test_update.py
+++ b/tests/test_update.py
@@ -89,7 +89,20 @@ class TestUpdateIntegration:
def run_manage_command_stdin(
self, manage_script, args, stdin_data, expect_success=True
):
- """Run glrd-manage command with data on stdin and return result."""
+ """Run glrd-manage command with data on stdin and return result.
+
+ A non-existent local input prefix is injected so the existing-release
+ query runs fully offline (it finds no files and falls back to an empty
+ list) instead of reaching out to the production S3 URL. Tests that
+ already specify their own ``--input-type`` are left untouched.
+ """
+ if "--input-type" not in args and "--no-query" not in args:
+ args = args + [
+ "--input-type",
+ "file",
+ "--input-file-prefix",
+ os.path.join(os.path.dirname(manage_script), "does-not-exist-glrd"),
+ ]
cmd = [sys.executable, manage_script] + args
result = subprocess.run(
cmd,