diff --git a/.githooks/pre-push b/.githooks/pre-push index 59dc517a..c9de9206 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -50,7 +50,7 @@ do # Check test examples set -e - doc-generation/check-examples.bash + data-model/tools/check-examples.bash set +e fi diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 605d8a66..ee0ca1b0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -60,7 +60,7 @@ jobs: - name: Run tests run: | - doc-generation/check-examples.bash + tools/check-examples.bash documents: needs: @@ -90,17 +90,18 @@ jobs: - name: Install dependencies run: poetry install --no-interaction --all-extras - - name: Generate MarkDown documentation + - name: Generate all artifacts run: | - doc-generation/generate-documentation.bash + tools/generate-all.bash - - name: Share generated documentation with later jobs + - name: Share generated artifacts with later jobs uses: actions/upload-artifact@v4 with: - name: generated-documentation + name: generated-artifacts path: | - system-design/specification/applications/application-description.md - system-design/specification/margo-management-interface/desired-state.md + docs/specification/applications/application-description.md + docs/specification/margo-management-interface/desired-state.md + system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml pages: needs: @@ -135,11 +136,15 @@ jobs: - name: Install the project dependencies run: poetry install - - name: Get generated documentation from previous jobs + - name: Get generated artifacts from previous jobs uses: actions/download-artifact@v4 with: - name: generated-documentation - path: system-design/specification + name: generated-artifacts + path: docs/specification + + - name: Generate merged documentation tree + run: | + tools/generate-docs.bash - name: Build Pages run: poetry run -- mkdocs build diff --git a/.gitignore b/.gitignore index d687d211..c509d16a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,7 @@ myenv .python-version # Ignore the generated files: -system-design/specification/margo-management-interface/desired-state.md -system-design/specification/applications/application-description.md -src/specification/applications/docs -src/specification/margo-management-interface/docs +build +docs/specification/margo-management-interface/desired-state.md +docs/specification/applications/application-description.md *.code-workspace diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..29991887 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,336 @@ +# AGENTS.md — Margo Specification Contributor Guide + +## Project Overview + +The Margo Specification defines open standards for workload fleet management on edge compute devices. This repository contains the normative specification documents, some authored manually in MarkDown and others generated from a [LinkML](https://linkml.io/) data model using Jinja2 templates. + +The final HTML documentation is built with [MkDocs](https://www.mkdocs.org/) using the [Material theme](https://squidfunk.github.io/mkdocs-material/). + +## Repository Layout + +``` +. +├── model/ # Aggregate data model (current source of truth) +│ ├── margo-data-model.linkml.yaml # Top-level schema aggregating all sub-schemas +│ ├── application-description.linkml.yaml +│ ├── application-deployment.linkml.yaml +│ ├── desired-state-manifest.linkml.yaml +│ ├── device-capabilities.linkml.yaml +│ ├── deployment-status.linkml.yaml +│ ├── margo-resources.linkml.yaml +│ ├── margo-deployments.linkml.yaml +│ ├── generation-gap.md # Differences between pre-draft and generated OpenAPI spec +│ ├── examples/{valid,invalid}/ # Valid and invalid example files +│ └── diagrams/ # Static PNG diagrams +├── system-design/ # Tracked specification content (generated + manually-authored) +│ └── specification/margo-management-interface/ +│ ├── specification-extensions.md # Manually-authored extensions +│ └── workload-management-api-1.0.0.yaml # Generated OpenAPI spec +├── tools/ # Generation & validation scripts +│ ├── generate-all.bash # Runs all generators in sequence +│ ├── generate-docs.bash # Generates MarkDown from LinkML, copies system-design/ → build/site/ +│ ├── generate-json-schemas.bash # Generates JSON-Schema artifacts +│ ├── generate-openapi.bash # Generates OpenAPI spec (to build/artifacts/ + system-design/) +│ ├── generate-class-diagram.bash # Generates PlantUML class diagrams +│ ├── check-examples.bash # Validates schemas and examples +│ ├── openapigen.py # Custom OpenAPI generator +│ ├── configurations/ # Per-spec JSON configs +│ │ ├── application-deployment.json +│ │ ├── application-description.json +│ │ ├── deployment-status.json +│ │ ├── desired-state-manifest.json +│ │ └── device-capabilities.json +│ └── templates/ +│ ├── model/ # Templates for the aggregate data-model docs +│ ├── main-classes/ # Templates for per-resource docs +│ └── openapi/ +│ └── workload-management-api-1.0.0.openapi.yaml # OpenAPI template +├── docs/ # Manually-authored MarkDown (copied into build/site/ by generate-docs.bash) +│ ├── index.md +│ ├── CNAME +│ ├── assets/ +│ ├── css/ +│ └── specification/ +│ ├── margo-management-interface/ # Manually-authored pages (api-requirements, certificate-api, etc.) +│ ├── applications/ # application-registry.md (manually-authored) +│ ├── margo-devices/ # device-requirements.md +│ └── observability/ # 3 pages (publishing/collecting/consuming) +├── build/ # Generated artifacts (tracked in git) +│ ├── artifacts/ # diagrams, OpenAPI, JSON-Schema, intermediate markdown +│ │ ├── diagrams/ +│ │ ├── json-schemas/ +│ │ ├── main-classes/ # Generated per-resource .md (intermediate) +│ │ ├── markdown/ # Generated aggregate data-model .md (intermediate) +│ │ └── openapi/ # Generated OpenAPI spec +│ └── site/ # Merged MarkDown tree used by mkdocs build +├── CONTRIBUTING.md # Contribution requirements and process +├── legacy/ # Archived superseded trees +│ ├── doc-generation/ # Legacy scripts (superseded by tools/) +│ ├── src-specification/ # Legacy per-resource schemas + templates + examples +│ └── validate-openapi.py # One-shot migration aid: compares generated spec vs pre-draft branch +├── mkdocs.yml # MkDocs configuration +└── pyproject.toml # Python dependencies (linkml@git, mkdocs, mkdocs-material, openapi-spec-validator) +``` + +## Setup + +### Option A: Development Container + +The repository includes a dev-container with all dependencies pre-installed. Open the repo in VS Code and accept the dev-container prompt. + +### Option B: Poetry (recommended for local development) + +```bash +poetry install +``` + +### Option C: pip + +```bash +pip install -e . +``` + +## Key Commands + +### Validate LinkML schemas and examples + +```bash +tools/check-examples.bash +``` + +This script: +- Reads each config from `tools/configurations/*.json` +- Validates the corresponding LinkML schema +- Validates valid examples in `model/examples/valid/` against the schema +- Validates that invalid examples in `model/examples/invalid/` are correctly rejected +- Exits non-zero if any check fails + +### Generate all artifacts + +```bash +tools/generate-all.bash +``` + +This runs all generators in sequence: class diagrams, JSON-Schemas, OpenAPI, and MarkDown docs. + +### Generate MarkDown from LinkML + +```bash +tools/generate-docs.bash +``` + +This script: +- Generates per-resource MarkDown using `tools/templates/main-classes/` +- Generates the full data model MarkDown using `tools/templates/model/` +- Copies generated diagrams and OpenAPI spec into the merged tree +- Copies everything from `docs/` and `system-design/` into `build/site/` (which is what `mkdocs build` reads) + +### Generate JSON-Schemas + +```bash +tools/generate-json-schemas.bash +``` + +Generates one `.schema.json` file per resource schema into `build/artifacts/json-schemas/`. + +### Generate OpenAPI spec + +```bash +tools/generate-openapi.bash +``` + +Generates `workload-management-api-1.0.0.openapi.yaml` into `build/artifacts/openapi/`. + +### Generate class diagrams + +```bash +tools/generate-class-diagram.bash +``` + +Generates SVG/PNG class diagrams via PlantUML into `build/artifacts/diagrams/`. + +### Build HTML documentation + +```bash +mkdocs build # one-time build +mkdocs serve # live-reloading local server +``` + +## Generation Pipeline + +Understanding how artifacts flow through the pipeline is essential when modifying schemas or templates. + +### Artifact flow + +``` +model/*.linkml.yaml (source of truth — LinkML schemas) + │ + ├──► tools/check-examples.bash + │ uses: tools/configurations/*.json + │ validates: model/examples/{valid,invalid}/ + │ + ├──► tools/generate-docs.bash (orchestrator) + │ │ + │ ├──► linkml generate doc (per-resource, using templates/main-classes/) + │ │ → build/artifacts/main-classes/.md + │ │ → moved into build/site/specification/{applications,margo-management-interface}/ + │ │ + │ ├──► linkml generate doc (aggregate, using templates/model/) + │ │ → build/artifacts/markdown/* + │ │ → moved into build/site/data-model/ + │ │ + │ ├──► generate-class-diagram.bash + │ │ → build/artifacts/diagrams/DataModel-ClassDiagram.{svg,png} + │ │ → copied into build/site/figures/ + │ │ + │ ├──► generate-openapi.bash + │ │ → build/artifacts/openapi/workload-management-api-1.0.0.openapi.yaml + │ │ → moved into build/site/specification/margo-management-interface/ + │ │ → also written to system-design/specification/margo-management-interface/ (tracked) + │ │ + │ └──► JSON schemas copied into build/site/json-schemas/ + │ + └──► tools/generate-json-schemas.bash (standalone) + → build/artifacts/json-schemas/*.schema.json + +mkdocs build reads from build/site/ → site/ +``` + +Key points: +- `mkdocs build` reads from `build/site/`, not directly from `docs/` or `build/artifacts/`. The `generate-docs.bash` script copies everything into `build/site/` first. +- The per-resource Markdown generation and the aggregate data-model generation use **different template directories** and produce output in **different locations**. +- `check-examples.bash` reads the list of schemas from `tools/configurations/*.json`, but `generate-docs.bash` and `generate-json-schemas.bash` have **hardcoded** schema lists. When adding a new resource, you must update all three. + +### OpenAPI generation + +The OpenAPI spec is generated by `tools/openapigen.py`, which: +- Reads an OpenAPI template (`tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml`) that defines endpoints, security schemes, and request/response structure. +- Fills in `components/schemas` from the LinkML model using the JSON-Schema generator. +- Only classes referenced by the endpoints in the template are included in the output. + +When adding a new resource to the API, you must update both the LinkML schema **and** the OpenAPI template (add new endpoints that reference the new class). The template follows standard OpenAPI 3.0.3 structure — add a new entry under `paths` with request/response schemas that use `$ref: "#/components/schemas/"`. + +## Legacy Code + +The `legacy/src-specification/` directory contains the original per-resource LinkML schemas, templates, and examples. These are **legacy** and no longer the source of truth. The current source of truth is `model/`. Do not modify files under `legacy/src-specification/` unless specifically instructed. + +The `legacy/doc-generation/` directory contains the original generation and validation scripts. These have been superseded by `tools/`. Do not use the old scripts. + +The `legacy/validate-openapi.py` script is a one-shot migration aid that compares the generated OpenAPI spec against the hand-written spec on the `pre-draft` branch. See `legacy/README.md` for usage details. It becomes obsolete once `pre-draft` is fully replaced by the generated spec, at which point it should be deleted. + +## How to Modify the LinkML Data Model + +### Step 1: Edit the schema + +The **source of truth** for LinkML-specified resources lives under `model/`. Each resource has its own `.linkml.yaml` file. The aggregate schema `model/margo-data-model.linkml.yaml` imports all sub-schemas. + +When making changes: + +1. Edit the relevant `.linkml.yaml` file to add/modify classes, attributes, enums, slots, or types. +2. Add or update valid examples in `model/examples/valid/` to cover the new or changed model elements. +3. Add invalid counter-examples in `model/examples/invalid/` if new validation rules are introduced. +4. If the change affects rendering, update the Jinja2 templates: + - `tools/templates/main-classes/` — for per-resource specification pages (attribute tables, examples, JSON-Schema links). + - `tools/templates/model/` — for the aggregate data-model documentation (class hierarchy, diagrams). +5. If the change adds new API endpoints, update the OpenAPI template `tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml`. + +### Step 2: Validate + +```bash +tools/check-examples.bash +``` + +Fix any validation errors before proceeding. + +### Step 3: Regenerate artifacts + +```bash +tools/generate-all.bash +``` + +This runs all generators in sequence. Alternatively, run individual generators if you only need to update one artifact type. + +The generated `.md` files land under `docs/specification/` and `docs/data-model/`. Generated JSON-Schemas, OpenAPI specs, and diagrams land under `build/artifacts/`. Verify the output looks correct. + +### Step 4: Preview the site + +```bash +mkdocs serve +``` + +Open http://127.0.0.1:8000 and navigate to the relevant specification page to visually verify. + +### Step 5: Commit + +Include the modified LinkML schema, updated examples, and all regenerated artifacts (`docs/`, `build/`) in your commit. These directories are tracked in git and must reflect the generated output. + +## Adding a New LinkML-Specified Resource + +1. Add the LinkML schema: `model/.linkml.yaml`. +2. Add valid examples: `model/examples/valid/-NNN.{yaml,json}`. +3. Add invalid counter-examples: `model/examples/invalid/-NNN.{yaml,json}`. +4. Add a configuration file in `tools/configurations/.json` with: + ```json + { + "root": "model", + "targetclass": "", + "schemafile": ".linkml.yaml", + "markdowndoc": ".md" + } + ``` +5. Add an import in `model/margo-data-model.linkml.yaml`. +6. Add the new MarkDown file to `mkdocs.yml` under the `nav` section. +7. Add the schema name to the hardcoded lists in `generate-docs.bash` (line 41) and `generate-json-schemas.bash` (line 24). +8. Run validation and generation as described above. + +## Jinja2 Templates + +The generation uses `linkml generate doc`, which provides the following variables and objects in the template context: + +- `schema` — the parsed LinkML schema object +- `schemaview` — a `SchemaView` instance for querying classes, slots, enums, etc. +- `gen` — the generator instance with helper methods like `all_class_objects()`, `get_direct_slots()`, `link()`, `mermaid_diagram()` + +Common patterns in existing templates: + +- Iterate over class slots: `{% for slot in schemaview.class_slots("ClassName")|sort(attribute='rank') %}` +- Get slot details: `schemaview.get_slot(slot_name).range`, `.required`, `.description` +- Include example files: `{% include 'examples/valid/FileName.yaml' %}` +- Format ranges with inline macros for multivalued/inlined slots (see `index.md.jinja2` in each template directory) + +There are two separate sets of templates that serve different purposes: + +| Template directory | Used by | Produces | Output location | +| --- | --- | --- | --- | +| `tools/templates/main-classes/` | `generate-docs.bash` (per-resource loop) | One `.md` per schema with attribute tables, examples, JSON-Schema links | `build/site/specification/{applications,margo-management-interface}/` | +| `tools/templates/model/` | `generate-docs.bash` (aggregate step) | Full data model overview with class hierarchy, diagrams, all-class listing | `build/site/data-model/` | + +Each directory contains an `index.md.jinja2` (the main page) and may contain `class.md.jinja2` (individual class detail pages). When modifying rendering, determine which template directory to edit based on the table above. + +**When do templates need updating?** Most schema changes (adding/removing attributes, changing types, adding enums) do **not** require template changes — the templates iterate dynamically over class slots. Templates need updating only when: +- Changing the **structure** of the rendered page (e.g., adding a new section, changing table columns) +- Changing how **multivalued/inlined ranges** are displayed (the `format_range` macro in `index.md.jinja2`) +- Adding support for a **new example format** (e.g., rendering `.json` examples alongside `.yaml`) + +## CI + +The GitHub Actions pipeline (`.github/workflows/pages.yml`) runs: + +1. **Quality checks** — validates `pyproject.toml` and `poetry.lock` consistency +2. **Validation** — runs `tools/check-examples.bash` +3. **Document generation** — runs `tools/generate-docs.bash` +4. **Pages build & deploy** — deploys to GitHub Pages on the `pre-draft` branch + +PR checks (`.github/workflows/pr-checks.yml`) verify that commits are signed off. + +## Conventions + +- Sign off all commits (`git commit -s`) +- All contributions require CLA compliance (EasyCLA) +- One logical change per commit; the tree must build and work after each commit +- Base PRs on the `pre-draft` branch +- Example files follow the naming convention `-NNN.{yaml,json}` (e.g., `ApplicationDescription-001.yaml`, `DeploymentStatusManifest-001.json`) +- LinkML schemas use the `.linkml.yaml` extension +- The YAML language server schema annotation `# yaml-language-server: $schema=...` should be kept at the top of each schema file for IDE support +- Do **not** add `default_range: string` to schemas that define slots using `any_of` — it causes the JSON-Schema generator to emit `type: string` at the top level, overriding the `anyOf` union. See [linkml/linkml#1483](https://github.com/linkml/linkml/issues/1483) +- Omit explicit `required: false` on optional attributes — the LinkML default is already `false` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b5bc1cdd..880991ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,13 +138,13 @@ Once the required tools have been installed as documented in the [Preparation](# As mentioned in a previous section, as of now only some of the resources are being specified in LinkML and are being used to generate the MarkDown documents. Steps 1. and 2. only apply to those documents. -Currently two Bash scripts are being provided the directory `doc-generation` to simplify steps 1. and 2. +Currently several Bash scripts are being provided in the directory `tools` to simplify steps 1. and 2. -The input for the generation of the MarkDown documents is provided in the directory `[src](./src/)`. +The input for the generation of the MarkDown documents is provided in the directory [model](./model/). #### Validate input for MarkDown Generation -The script [check-examples.bash](./doc-generation/check-examples.bash) checks: +The script [check-examples.bash](./tools/check-examples.bash) checks: - the validity of the LinkML resource definitions (AKA schemas), and - the validity of provided examples and counter-examples according the resource definitions @@ -153,9 +153,25 @@ The script [check-examples.bash](./doc-generation/check-examples.bash) checks: #### Generate MarkDown Documents -The script [generate-documentation.bash](./doc-generation/generate-documentation.bash) generates MarkDown documents for the resources specified in LinkML format. +The script [generate-docs.bash](./tools/generate-docs.bash) generates MarkDown documents for the resources specified in LinkML format. -The LinkML specification documents can be found in the directory [src](./src/) and the resulting MarkDown documents are integrated with the other MarkDown documents in the directory [system-design](./system-design/). +The LinkML specification documents can be found in the directory [model](./model/) and the resulting MarkDown documents are integrated with the other MarkDown documents in the directory [docs](./docs/). + +#### Generate OpenAPI YAML Documents + +The script [generate-openapi.bash](./tools/generate-openapi.bash) generates the OpenAPI v3.0.3 specification YAML file for the Workload Management API. + +It uses the custom generator [openapigen.py](./tools/openapigen.py) which composes a user-provided OpenAPI template (containing API header, paths/endpoints, and security schemes) with JSON Schema components generated from the LinkML data model. Only schemas referenced by the template's endpoints (and their transitive dependencies) are included. + +The generation: + +1. Reads the aggregate data model [`model/margo-data-model.linkml.yaml`](./model/margo-data-model.linkml.yaml) as the LinkML source. +2. Reads the OpenAPI template [`tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml`](./tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml) which defines the API endpoints, request/response structures, and security schemes. +3. Generates JSON Schema definitions for all referenced classes and injects them under `components/schemas` in the template. +4. Writes the output to `build/artifacts/openapi/workload-management-api-1.0.0.openapi.yaml`. +5. Copies the result into the tracked location `system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml` for version control. `generate-docs.bash` copies it from there into `build/site/` for MkDocs. + +When adding new API endpoints, edit the OpenAPI template to add the corresponding `paths` entries referencing the relevant `$ref: "#/components/schemas/"` schemas. #### Generate HTML Documents diff --git a/system-design/CNAME b/docs/CNAME similarity index 100% rename from system-design/CNAME rename to docs/CNAME diff --git a/system-design/assets/favicon.ico b/docs/assets/favicon.ico similarity index 100% rename from system-design/assets/favicon.ico rename to docs/assets/favicon.ico diff --git a/system-design/assets/margo_white.svg b/docs/assets/margo_white.svg similarity index 100% rename from system-design/assets/margo_white.svg rename to docs/assets/margo_white.svg diff --git a/docs/assets/svg-pan-zoom-init.js b/docs/assets/svg-pan-zoom-init.js new file mode 100644 index 00000000..86e5737b --- /dev/null +++ b/docs/assets/svg-pan-zoom-init.js @@ -0,0 +1,56 @@ +/** + * Initialize svg-pan-zoom on containers marked with data-svg-pan-zoom. + * Each container should have a data-svg-src attribute pointing to the SVG file. + * The SVG is fetched, inlined into the DOM, and then svg-pan-zoom is attached. + */ +document.addEventListener("DOMContentLoaded", function () { + var containers = document.querySelectorAll("[data-svg-pan-zoom]"); + containers.forEach(function (container) { + var svgSrc = container.getAttribute("data-svg-src"); + if (!svgSrc) return; + + fetch(svgSrc) + .then(function (response) { return response.text(); }) + .then(function (svgText) { + // Parse the SVG and insert it into the container + var parser = new DOMParser(); + var svgDoc = parser.parseFromString(svgText, "image/svg+xml"); + var svgEl = svgDoc.documentElement; + + // Make the SVG fill the container + svgEl.setAttribute("width", "100%"); + svgEl.setAttribute("height", "100%"); + svgEl.style.width = "100%"; + svgEl.style.height = "100%"; + + container.appendChild(svgEl); + + // Initialize svg-pan-zoom + var panZoomInstance = svgPanZoom(svgEl, { + zoomEnabled: true, + controlIconsEnabled: true, + fit: true, + center: true, + minZoom: 0.25, + maxZoom: 20, + zoomScaleSensitivity: 0.3 + }); + + // Handle resize + window.addEventListener("resize", function () { + panZoomInstance.resize(); + panZoomInstance.fit(); + panZoomInstance.center(); + }); + }) + .catch(function (err) { + console.error("Failed to load SVG for pan-zoom:", err); + // Fallback: show as a regular image + var img = document.createElement("img"); + img.src = svgSrc; + img.alt = container.getAttribute("data-svg-alt") || "SVG diagram"; + img.style.width = "100%"; + container.appendChild(img); + }); + }); +}); diff --git a/docs/assets/svg-pan-zoom.min.js b/docs/assets/svg-pan-zoom.min.js new file mode 100644 index 00000000..4904d12d --- /dev/null +++ b/docs/assets/svg-pan-zoom.min.js @@ -0,0 +1,3 @@ +// svg-pan-zoom v3.6.1 +// https://github.com/ariutta/svg-pan-zoom +!function s(r,a,l){function u(e,t){if(!a[e]){if(!r[e]){var o="function"==typeof require&&require;if(!t&&o)return o(e,!0);if(h)return h(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var i=a[e]={exports:{}};r[e][0].call(i.exports,function(t){return u(r[e][1][t]||t)},i,i.exports,s,r,a,l)}return a[e].exports}for(var h="function"==typeof require&&require,t=0;tthis.options.maxZoom*n.zoom&&(t=this.options.maxZoom*n.zoom/this.getZoom());var i=this.viewport.getCTM(),s=e.matrixTransform(i.inverse()),r=this.svg.createSVGMatrix().translate(s.x,s.y).scale(t).translate(-s.x,-s.y),a=i.multiply(r);a.a!==i.a&&this.viewport.setCTM(a)},i.prototype.zoom=function(t,e){this.zoomAtPoint(t,a.getSvgCenterPoint(this.svg,this.width,this.height),e)},i.prototype.publicZoom=function(t,e){e&&(t=this.computeFromRelativeZoom(t)),this.zoom(t,e)},i.prototype.publicZoomAtPoint=function(t,e,o){if(o&&(t=this.computeFromRelativeZoom(t)),"SVGPoint"!==r.getType(e)){if(!("x"in e&&"y"in e))throw new Error("Given point is invalid");e=a.createSVGPoint(this.svg,e.x,e.y)}this.zoomAtPoint(t,e,o)},i.prototype.getZoom=function(){return this.viewport.getZoom()},i.prototype.getRelativeZoom=function(){return this.viewport.getRelativeZoom()},i.prototype.computeFromRelativeZoom=function(t){return t*this.viewport.getOriginalState().zoom},i.prototype.resetZoom=function(){var t=this.viewport.getOriginalState();this.zoom(t.zoom,!0)},i.prototype.resetPan=function(){this.pan(this.viewport.getOriginalState())},i.prototype.reset=function(){this.resetZoom(),this.resetPan()},i.prototype.handleDblClick=function(t){var e;if((this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),this.options.controlIconsEnabled)&&-1<(t.target.getAttribute("class")||"").indexOf("svg-pan-zoom-control"))return!1;e=t.shiftKey?1/(2*(1+this.options.zoomScaleSensitivity)):2*(1+this.options.zoomScaleSensitivity);var o=a.getEventPoint(t,this.svg).matrixTransform(this.svg.getScreenCTM().inverse());this.zoomAtPoint(e,o)},i.prototype.handleMouseDown=function(t,e){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),r.mouseAndTouchNormalize(t,this.svg),this.options.dblClickZoomEnabled&&r.isDblClick(t,e)?this.handleDblClick(t):(this.state="pan",this.firstEventCTM=this.viewport.getCTM(),this.stateOrigin=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()))},i.prototype.handleMouseMove=function(t){if(this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&this.options.panEnabled){var e=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()),o=this.firstEventCTM.translate(e.x-this.stateOrigin.x,e.y-this.stateOrigin.y);this.viewport.setCTM(o)}},i.prototype.handleMouseUp=function(t){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&(this.state="none")},i.prototype.fit=function(){var t=this.viewport.getViewBox(),e=Math.min(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.contain=function(){var t=this.viewport.getViewBox(),e=Math.max(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.center=function(){var t=this.viewport.getViewBox(),e=.5*(this.width-(t.width+2*t.x)*this.getZoom()),o=.5*(this.height-(t.height+2*t.y)*this.getZoom());this.getPublicInstance().pan({x:e,y:o})},i.prototype.updateBBox=function(){this.viewport.simpleViewBoxCache()},i.prototype.pan=function(t){var e=this.viewport.getCTM();e.e=t.x,e.f=t.y,this.viewport.setCTM(e)},i.prototype.panBy=function(t){var e=this.viewport.getCTM();e.e+=t.x,e.f+=t.y,this.viewport.setCTM(e)},i.prototype.getPan=function(){var t=this.viewport.getState();return{x:t.x,y:t.y}},i.prototype.resize=function(){var t=a.getBoundingClientRectNormalized(this.svg);this.width=t.width,this.height=t.height;var e=this.viewport;e.options.width=this.width,e.options.height=this.height,e.processCTM(),this.options.controlIconsEnabled&&(this.getPublicInstance().disableControlIcons(),this.getPublicInstance().enableControlIcons())},i.prototype.destroy=function(){var e=this;for(var t in this.beforeZoom=null,this.onZoom=null,this.beforePan=null,this.onPan=null,(this.onUpdatedCTM=null)!=this.options.customEventsHandler&&this.options.customEventsHandler.destroy({svgElement:this.svg,eventsListenerElement:this.options.eventsListenerElement,instance:this.getPublicInstance()}),this.eventListeners)(this.options.eventsListenerElement||this.svg).removeEventListener(t,this.eventListeners[t],!this.options.preventMouseEventsDefault&&h);this.disableMouseWheelZoom(),this.getPublicInstance().disableControlIcons(),this.reset(),c=c.filter(function(t){return t.svg!==e.svg}),delete this.options,delete this.viewport,delete this.publicInstance,delete this.pi,this.getPublicInstance=function(){return null}},i.prototype.getPublicInstance=function(){var o=this;return this.publicInstance||(this.publicInstance=this.pi={enablePan:function(){return o.options.panEnabled=!0,o.pi},disablePan:function(){return o.options.panEnabled=!1,o.pi},isPanEnabled:function(){return!!o.options.panEnabled},pan:function(t){return o.pan(t),o.pi},panBy:function(t){return o.panBy(t),o.pi},getPan:function(){return o.getPan()},setBeforePan:function(t){return o.options.beforePan=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnPan:function(t){return o.options.onPan=null===t?null:r.proxy(t,o.publicInstance),o.pi},enableZoom:function(){return o.options.zoomEnabled=!0,o.pi},disableZoom:function(){return o.options.zoomEnabled=!1,o.pi},isZoomEnabled:function(){return!!o.options.zoomEnabled},enableControlIcons:function(){return o.options.controlIconsEnabled||(o.options.controlIconsEnabled=!0,s.enable(o)),o.pi},disableControlIcons:function(){return o.options.controlIconsEnabled&&(o.options.controlIconsEnabled=!1,s.disable(o)),o.pi},isControlIconsEnabled:function(){return!!o.options.controlIconsEnabled},enableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!0,o.pi},disableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!1,o.pi},isDblClickZoomEnabled:function(){return!!o.options.dblClickZoomEnabled},enableMouseWheelZoom:function(){return o.enableMouseWheelZoom(),o.pi},disableMouseWheelZoom:function(){return o.disableMouseWheelZoom(),o.pi},isMouseWheelZoomEnabled:function(){return!!o.options.mouseWheelZoomEnabled},setZoomScaleSensitivity:function(t){return o.options.zoomScaleSensitivity=t,o.pi},setMinZoom:function(t){return o.options.minZoom=t,o.pi},setMaxZoom:function(t){return o.options.maxZoom=t,o.pi},setBeforeZoom:function(t){return o.options.beforeZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnZoom:function(t){return o.options.onZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},zoom:function(t){return o.publicZoom(t,!0),o.pi},zoomBy:function(t){return o.publicZoom(t,!1),o.pi},zoomAtPoint:function(t,e){return o.publicZoomAtPoint(t,e,!0),o.pi},zoomAtPointBy:function(t,e){return o.publicZoomAtPoint(t,e,!1),o.pi},zoomIn:function(){return this.zoomBy(1+o.options.zoomScaleSensitivity),o.pi},zoomOut:function(){return this.zoomBy(1/(1+o.options.zoomScaleSensitivity)),o.pi},getZoom:function(){return o.getRelativeZoom()},setOnUpdatedCTM:function(t){return o.options.onUpdatedCTM=null===t?null:r.proxy(t,o.publicInstance),o.pi},resetZoom:function(){return o.resetZoom(),o.pi},resetPan:function(){return o.resetPan(),o.pi},reset:function(){return o.reset(),o.pi},fit:function(){return o.fit(),o.pi},contain:function(){return o.contain(),o.pi},center:function(){return o.center(),o.pi},updateBBox:function(){return o.updateBBox(),o.pi},resize:function(){return o.resize(),o.pi},getSizes:function(){return{width:o.width,height:o.height,realZoom:o.getZoom(),viewBox:o.viewport.getViewBox()}},destroy:function(){return o.destroy(),o.pi}}),this.publicInstance};var c=[];e.exports=function(t,e){var o=r.getSvg(t);if(null===o)return null;for(var n=c.length-1;0<=n;n--)if(c[n].svg===o)return c[n].instance.getPublicInstance();return c.push({svg:o,instance:new i(o,e)}),c[c.length-1].instance.getPublicInstance()}},{"./control-icons":1,"./shadow-viewport":2,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(t,e,o){var l=t("./utilities"),s="unknown";document.documentMode&&(s="ie"),e.exports={svgNS:"http://www.w3.org/2000/svg",xmlNS:"http://www.w3.org/XML/1998/namespace",xmlnsNS:"http://www.w3.org/2000/xmlns/",xlinkNS:"http://www.w3.org/1999/xlink",evNS:"http://www.w3.org/2001/xml-events",getBoundingClientRectNormalized:function(t){if(t.clientWidth&&t.clientHeight)return{width:t.clientWidth,height:t.clientHeight};if(t.getBoundingClientRect())return t.getBoundingClientRect();throw new Error("Cannot get BoundingClientRect for SVG.")},getOrCreateViewport:function(t,e){var o=null;if(!(o=l.isElement(e)?e:t.querySelector(e))){var n=Array.prototype.slice.call(t.childNodes||t.children).filter(function(t){return"defs"!==t.nodeName&&"#text"!==t.nodeName});1===n.length&&"g"===n[0].nodeName&&null===n[0].getAttribute("transform")&&(o=n[0])}if(!o){var i="viewport-"+(new Date).toISOString().replace(/\D/g,"");(o=document.createElementNS(this.svgNS,"g")).setAttribute("id",i);var s=t.childNodes||t.children;if(s&&0 Note: This assumes consistent connection to the WFM, we will address intermittent or extended disconnection scenarios in the future. + +## Route and HTTP Methods + +```https +POST /api/v1/clients/{clientId}/deployments/{deploymentId}/status +``` + +### Route Parameters + +|Parameter | Type | Required? | Description| +|----------|------|-----------|------------| +| {clientId} | string | Y | The unique identifier of the (device) client registered with the WFM during onboarding. | +| {deploymentId} | string | Y | The UUID of the `ApplicationDeployment` YAML being reported. + +### Response Codes + +| Code | Description | +|------|-------------| +| 200 OK | The deployment status was added, or updated, successfully. | +| 400 Bad Request | Missing or invalid content-digest header. Ensure the SHA256 hash of the base64-encoded payload is included. | +| 401 Unauthorized | Signature verification failed. Ensure you are signing with the correct X.509 private key. | +| 403 Forbidden | Client certificate is not trusted or has been revoked. | +| 422 Unprocessable Content | Request body includes a semantic error. | + + +## Request Body + +[DeploymentStatus](./deployment status.md) document describing the status of an application deployment. + diff --git a/system-design/specification/margo-management-interface/deployment-status.md b/docs/specification/margo-management-interface/deployment-status.md similarity index 100% rename from system-design/specification/margo-management-interface/deployment-status.md rename to docs/specification/margo-management-interface/deployment-status.md diff --git a/docs/specification/margo-management-interface/desired-state.md b/docs/specification/margo-management-interface/desired-state.md new file mode 100644 index 00000000..5e3781ef --- /dev/null +++ b/docs/specification/margo-management-interface/desired-state.md @@ -0,0 +1,534 @@ +# Desired State + +In order for the Workload Fleet Manager (WFM) to manage workloads on an Edge Compute Device, the device's Workload Fleet Management Client must periodically retrieve its desired workload configuration - referred to as the Desired State - from the WFM. + +The Desired State defines *what* workloads (applications) should run on the device and *how* they should be configured. +It is distributed using a lightweight, pull-based HTTP API that allows devices to stay synchronized with the WFM. + +At the center of this process is the State Manifest, a JSON document that lists all workloads assigned to the device. Each workload is represented by an [`ApplicationDeployment`](#applicationdeployment-yaml-definition) YAML - a self-contained object defining configuration, components, and parameters for that workload. + +The manifest includes two complementary ways for the client to obtain the same `ApplicationDeployment` YAMLs: + +- Individual YAMLs - each `ApplicationDeployment` YAML fetched separately using its own URL. +- A bundle archive - a single compressed archive containing multiple `ApplicationDeployment` YAMLs. + +Both references describe the same content. The bundle is simply a packaging optimization. +This design allows the Workload Fleet Management Client to choose the optimal retrieval strategy depending on network conditions or update size. + +| Retrieval Method | Typical Use | Advantages | +| ---------------- | ----------- | ---------- | +| Bundle | Initial onboarding, large updates, high-latency or high-round-trip networks | Single request with minimal overhead | +| Individual YAMLs | Incremental updates, bandwidth-limited or metered links | Only changed workloads are downloaded | + +The Workload Fleet Management Client compares the manifest with its current state and reconciles any differences by deploying, updating, or removing workloads. +For every change in deployment state - including installation, updates, removals, and failures - the client MUST report the corresponding status to the WFM using the [Deployment Status API](../../specification/margo-management-interface/deployment-status.md). + +## Endpoints - State Manifest + +This section defines the API endpoint used by a client to retrieve the State Manifest from the Workload Fleet Manager, representing the complete desired workload configuration assigned to the device. + +### Route and HTTP Methods + +```https +GET /api/v1/clients/{clientId}/deployments +``` + +### Route Parameters + +| Parameter | Type | Required? | Description | +| --------- | ---- | --------- | ----------- | +| `{clientId}` | string | Y | The unique identifier of the (device) client registered with the WFM during onboarding. | + +### Request Headers + +| Header | Description | +| ------ | ------------| +| `If-None-Match` *(optional)* | The `ETag` value from the last successfully retrieved manifest. | +| `Accept` *(optional)* | The client SHOULD request the manifest in the `application/vnd.margo.manifest.v1+json` format. If the `Accept` header lists only unsupported types, the server MUST return `406 Not Acceptable`. If omitted, the server MUST return this format by default. | + +### Response Codes + +| Code | Description | +| ----- | ---------- | +| 200 OK | The response body contains the manifest. The server MUST include a valid `ETag` and `Content-Type: application/vnd.margo.manifest.v1+json`. | +| 304 Not Modified | The response body is empty. Returned if the `If-None-Match` `ETag` matches, i.e. the cached response body has not changed since the last retrieved version. | +| 406 Not Acceptable | The server cannot return a representation matching the `Accept` header. | + +### Example State Manifest Response + +```json +{ + "manifestVersion": 101, + "bundle": { + "mediaType": "application/vnd.margo.bundle.v1+tar+gzip", + "digest": "sha256:b5c6d7e8f9...", + "url": "/api/v1/clients/1234/bundles/sha256:b5c6d7e8f9..." + }, + "deployments": [ + { + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "digest": "sha256:a4e01b2c3d...", + "url": "/api/v1/clients/1234/deployments/a3e2f5dc-912e-494f-8395-52cf3769bc06/sha256:a4e01b2c3d..." + } + ] +} +``` + +### Response Body Attributes + +| Field | Type | Required? | Description | +| ----- | ---- | --------- | ----------- | +| `manifestVersion` | number | Y | Monotonically increasing unsigned 64-bit integer in the inclusive range `[1, 2^64-1]`. Each new manifest for the same (device) client MUST have a strictly greater value than the previous. The first manifest for a given client MUST use the value 1. | +| `bundle` | object | Y | Describes an archive containing all referenced `ApplicationDeployment` YAMLs. If there are zero deployments (i.e., the `deployments` array is empty), this field MUST be present with the value `null`. An empty archive MUST NOT be served. | +| `bundle.mediaType` | string | Y | MUST be `application/vnd.margo.bundle.v1+tar+gzip`, which denotes a gzip-compressed tar archive (commonly delivered as a .tar.gz) whose root contains one or more `ApplicationDeployment` YAML files. Servers MUST set the HTTP `Content-Type` to this media type. The archive MUST contain exactly the set of YAML files referenced by `deployments`. | +| `bundle.digest` | string | Y | Digest of the bundle archive. MUST equal the digest computed over the exact sequence of bytes in the [bundle endpoint's](#endpoints-deployment-bundle) HTTP `200 OK` response body. See [Protocol - Digest](#protocol-digest) for further details. | +| `bundle.sizeBytes` | number | N | Optional unsigned 64-bit advisory estimate of the decoded payload length in bytes for the bundle archive. Provided for bandwidth estimation and update planning. MUST NOT be used for integrity verification. | +| `bundle.url` | string | Y | Content-addressable retrieval endpoint for the bundle of the form `/api/v1/clients/{clientId}/bundles/{digest}` where `{digest}` equals `bundle.digest`. | +| `deployments` | array | Y | List of deployment objects describing each workload. | +| `deployments[].deploymentId` | string | Y | The UUID of the deployment. MUST equal [`metadata.annotations.id`](#annotations-attributes) in the `ApplicationDeployment`. | +| `deployments[].digest` | string | Y | Digest of the corresponding `ApplicationDeployment` YAML file. MUST equal the digest computed over the exact sequence of bytes in the [individual deployment endpoint's](#endpoints-individual-deployment-yaml) HTTP `200 OK` response body. See [Protocol - Digest](#protocol-digest) for further details. | +| `deployments[].sizeBytes` | number | N | Optional unsigned 64-bit advisory estimate of the decoded payload length in bytes for the `ApplicationDeployment` YAML. Provided for bandwidth estimation and update planning. MUST NOT be used for integrity verification. | +| `deployments[].url` | string | Y | Content-addressable retrieval endpoint for the `ApplicationDeployment` YAML of the form `/api/v1/clients/{clientId}/deployments/{deploymentId}/{digest}` where `{digest}` equals `deployments[].digest`. | + +> **Note:** The `ETag` returned from this endpoint is a digest of the entire JSON response body (after serialization). It is independent of `bundle.digest` and individual deployment digests (`deployments[].digest`). See [ETag and Caching](#protocol-etag-and-caching) for details. + +### Client Validation Rules + +- The client MUST verify the digest of every fetched artifact before use. +- If any digest validation fails, the client MUST abort the update and retain the previous state. +- The client MUST persist both the last accepted `manifestVersion` and `ETag` to prevent rollback across restarts. + +## Endpoints - Individual Deployment YAML + +This section defines the API endpoint used by a client to retrieve a single `ApplicationDeployment` YAML for incremental synchronization and targeted updates. + +### Route and HTTP Methods + +```https +GET /api/v1/clients/{clientId}/deployments/{deploymentId}/{digest} +``` + +### Route Parameters + +| Parameter | Type | Required? | Description | +| --------- | ---- | --------- | ----------- | +| `{clientId}` | string | Y | The unique identifier of the (device) client registered with the WFM during onboarding. | +| `{deploymentId}` | string | Y | The UUID of the served `ApplicationDeployment` YAML. This MUST equal to `metadata.annotations.id`. | +| `{digest}` | string | Y | Content-addressable digest of the served `ApplicationDeployment` YAML. See [Protocol - Digest](#protocol-digest) for further details. | + +### Response Codes + +| Code | Description | +| ---- | ----------- | +| 200 OK | The response body contains the raw `ApplicationDeployment` YAML (`Content-Type: application/yaml`). Server MUST set `ETag` to the quoted digest and SHOULD return `Cache-Control: public, max-age=31536000, immutable`. | +| 404 Not Found | The referenced digest does not exist on the server. `404 Not Found` indicates only that this specific digest is unavailable. It MUST NOT be interpreted as a deletion signal by a client; deletion of workloads is determined solely by absence from the state manifest. | + +> **Note:** Servers MAY apply HTTP `Content-Encoding` (e.g., gzip, br). The client advertises support via `Accept-Encoding`. Digests and ETags always refer to the decoded representation (i.e., the exact bytes of the response body after decompressing any HTTP `Content-Encoding` such as gzip). Servers SHOULD include `Vary: Accept-Encoding` if compression is used. + +## Endpoints - Deployment Bundle + +This section defines the API endpoint used by a client to retrieve a compressed bundle containing all `ApplicationDeployment` YAMLs for efficient bulk synchronization. + +### Route and HTTP Methods + +```https +GET /api/v1/clients/{clientId}/bundles/{digest} +``` + +### Route Parameters + +| Parameter | Type | Required? | Description | +| --------- | ---- | --------- | ----------- | +| `{clientId}` | string | Y | The unique identifier of the (device) client registered with the WFM during onboarding. | +| `{digest}` | string | Y | Content-addressable digest of the served bundle archive. See [Protocol - Digest](#protocol-digest) for further details. | + +### Response Codes + +| Code | Description | +| ---- | ----------- | +| 200 OK | The bundle was successfully retrieved. The server MUST set `Content-Type` to the manifest-declared `bundle.mediaType`, `ETag` to the quoted digest, and SHOULD return `Cache-Control: public, max-age=31536000, immutable`. | +| 404 Not Found | The referenced digest does not exist on the server. `404 Not Found` indicates only that this specific digest is unavailable. It MUST NOT be interpreted as a deletion signal by a client; deletion of workloads is determined solely by absence from the state manifest. | + +> **Note:** Servers MAY apply `Content-Encoding` (e.g., gzip, br) and SHOULD include `Vary: Accept-Encoding` if they do. + +## Protocol - Digest + +All Desired State artifacts - including the manifest, bundle archives, and individual `ApplicationDeployment` YAMLs - use a canonical digest to ensure content integrity and consistency across client and server implementations. + +A digest has the form `algorithm:encoded`, where both parts are lowercase. Clients and servers MUST NOT add prefixes, suffixes, or whitespace. The required algorithm is `sha256`, and the encoded portion is a 64-character lowercase hexadecimal string. + +- The digest MUST be computed over the exact bytes of the decoded HTTP response body - that is, after decompressing any HTTP `Content-Encoding` (for example, gzip or br). +- No reformatting, re-serialization, or newline normalization is permitted during digest computation. + +The resulting digest value is used consistently across all API representations: + +- In JSON responses: `"digest": "sha256:a4e01b2c3d..."` +- In URLs: `/api/v1/clients/{clientId}/deployments/{deploymentId}/sha256:a4e01b2c3d...` +- In HTTP headers: `ETag: "sha256:a4e01b2c3d..."` + +Clients MUST verify that the digest they compute for every retrieved artifact matches the value provided in the manifest. Any mismatch MUST cause the client to abort the update and preserve the previous state. +If a manifest references a digest using an unsupported algorithm, the client MUST treat the manifest as invalid and abort processing. + +**Example:** + +```text +sha256:a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef +``` + +> **Note:** +> This API defines the digest format `sha256:` (lowercase) for JSON fields, URLs, and ETags. +> This digest represents content identity, ensuring that each artifact (manifest, bundle, or `ApplicationDeployment` YAML) can be uniquely verified and referenced within the Desired State API. +> +> By contrast, the mechanism described in the [API Requirements and Security Details](../../specification/margo-management-interface/api-requirements-and-security.md) document - which follows [RFC 9421](https://datatracker.ietf.org/doc/html/rfc9421) - provides HTTP message-level integrity through signed payloads. +> While both rely on the SHA-256 algorithm, they operate at different layers of the protocol: +> +> - The digest in this API defines immutable content identity for artifacts. +> - The RFC 9421 mechanism ensures end-to-end message integrity and authenticity during HTTP transport. + +## Protocol - ETag and Caching + +All Desired State endpoints implement standard HTTP caching semantics to optimize synchronization between the Workload Fleet Manager (WFM) and the Workload Fleet Management Client. ETags are used to detect content changes and avoid redundant data transfers. Two caching models are defined: one for the mutable State Manifest, and one for immutable, content-addressable resources such as individual deployments and bundles. + +### State Manifest Endpoint + +For the [State Manifest](#endpoints-state-manifest) endpoint, servers use strong ETags as defined in [RFC 9110 § 8.8.3](https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3). + +- The `ETag` MUST be a strong validator computed as a digest of the exact serialized JSON response body. + The format MUST follow the digest grammar defined in [Protocol – Digest](#protocol-digest): + `":"`, for example: + `"sha256:a4e01b2c3d..."`. +- Servers SHOULD serialize JSON deterministically (for example, per [RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785)) so that logically identical manifests yield identical bytes and therefore identical ETags. +- Manifest responses MUST NOT be marked immutable (e.g., by applying `Cache-Control: immutable` or excessively long `max-age` values). Freshness is controlled through periodic polling using `If-None-Match` revalidation requests. +- When a client presents an `ETag` that matches the current manifest, the server MUST respond with `304 Not Modified`, omitting the response body. + +### Content-Addressable Endpoints + +For [Individual Deployment YAML](#endpoints-individual-deployment-yaml) and [Deployment Bundle](#endpoints-deployment-bundle) endpoints, the resources are immutable and uniquely identified by their digest. + +- The `ETag` MUST equal the quoted digest embedded in the resource’s URL (e.g., `ETag: "sha256:a4e01b2c3d..."`). + This constitutes a strong validator per [RFC 9110 § 8.8.3](https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3). +- Servers SHOULD include `Cache-Control: public, max-age=31536000, immutable` to enable long-term caching of immutable artifacts. +- If compression is applied, servers SHOULD include `Vary: Accept-Encoding` to ensure cache correctness across encodings. +- Clients MAY send `If-None-Match` when revalidating cached resources; servers MAY return `304 Not Modified` if the artifact has not changed. + +## Deployment Workflow + +This section defines the end-to-end workflow followed by a client to retrieve, validate, and reconcile its Desired State with the Workload Fleet Manager. + +- The client polls the WFM for the latest manifest using the last known `ETag` (if any): + + ```https + GET /api/v1/clients/{clientId}/deployments + ``` + + - If the manifest is unchanged, the WFM responds with `304 Not Modified`. + - If the manifest has changed (`200 OK`), the client: + - Verifies the `manifestVersion` is strictly greater than the stored version. If not, the update MUST be rejected and SHOULD be logged as a security event. The specific requirements for logging security events are not currently defined and will be addressed in a future version of the specification. + - Parses the manifest and decides whether to fetch the bundle or individual deployments. + - Downloads and verifies digests for all referenced `ApplicationDeployment` YAMLs. + - The client reconciles its local workloads: + - Adds or updates workloads that appear in the new manifest. + - Removes workloads no longer present in the new manifest. + - For each change in workload state, the client reports progress and results to the WFM using the [Deployment Status API](../../specification/margo-management-interface/deployment-status.md). + - Once reconciliation succeeds, the client MUST durably persist the new `manifestVersion` and associated `ETag` for use in the next poll cycle. + +### Sequence Diagram + +```mermaid +sequenceDiagram + autonumber + participant Client as Workload Fleet
Management Client + participant WFM as Workload Fleet
Manager + + loop Poll for updates + Client->>+WFM: GET /api/v1/clients/{clientId}/deployments
Header: If-None-Match: "sha256:abc..." + alt State unchanged + WFM-->>-Client: 304 Not Modified + else State updated + WFM-->>Client: 200 OK
Header: ETag: "sha256:xyz..."
Body: Manifest JSON + Client->>Client: Compare manifestVersion and current state + + alt Initial sync or major update + Client->>WFM: GET (bundle URL) + WFM-->>Client: 200 OK
Body: Bundle archive + else Incremental update + Client->>WFM: GET (deployment URLs) + WFM-->>Client: 200 OK
Body: YAMLs + end + + Client->>Client: Verify digests and reconcile workloads + Client->>WFM: POST /api/v1/clients/{clientId}/deployment/{deploymentId}/status
Report progress + end + end +``` + +## ApplicationDeployment YAML Definition + +This section defines the structure and YAML schema of an `ApplicationDeployment`, providing a normative reference for Desired State configuration objects. + +Each workload is represented as an `ApplicationDeployment` YAML file that specifies its components, configuration, and parameters. This resource is delivered via the Desired State API and referenced by `id` in the Deployment Status API. + +```yaml +apiVersion: application.margo.org/v1alpha1 +kind: ApplicationDeployment +metadata: + annotations: + id: + applicationId: + name: + namespace: +spec: + deploymentProfile: + type: + components: + - name: + properties: + parameters: + param: + value: + targets: + - pointer: + components:[] +``` + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +| apiVersion | string | Y | Identifier of the version of the API the object definition follows.| +| kind | string | Y | Must be `ApplicationDeployment`.| +| metadata | Metadata | Y | Metadata element specifying characteristics about the application deployment. See the [Metadata Attributes](#metadata-attributes) section below.| +| spec | Spec | Y | Spec element that defines deployment profile and parameters associated with the application deployment. See the [Spec Attributes](#spec-attributes) section below.| + + +#### Metadata Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +| annotations | Annotations | Y | Defines the application ID and unique identifier associated to the deployment specification. Needs to be assigned by the Workload Orchestration Software. See the [Annotation Attributes](#annotations-attributes) section below.| +| name | string | Y | When deploying to Kubernetes, the manifests name. The name is chosen by the workload orchestration vendor and is not displayed anywhere.| +| namespace | string | Y | When deploying to Kubernetes, the namespace the manifest is added under. The namespace is chosen by the workload orchestration solution vendor.| + + +#### Annotations Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +| applicationId | string | Y | An identifier for the application. The id is used to help create unique identifiers where required, such as namespaces. The id must be lower case letters and numbers and MAY contain dashes. Uppercase letters, underscores and periods MUST NOT be used. The id MUST NOT be more than 200 characters. The applicationId MUST match the associated application package Metadata "id" attribute.| +| id | string | Y | The unique identifier UUID of the deployment specification. Needs to be assigned by the Workload Orchestration Software.| + + +#### Spec Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +| deploymentProfile | DeploymentProfile | Y | Section that defines deployment details including type and components.| +| parameters | map[string][Parameter] | Y | Describes the configured parameters applied via the end-user.| + + +#### DeploymentProfile Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +| type | string | Y | The type of deployment profile (e.g., helm.v3, compose).| +| components | Component | Y | Components of the application| + + +#### ComposeDeploymentProfile Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | + + +#### Component Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +| name | string | Y | The name of the component.| +| properties | map[string][string] | Y | Properties associated with the component.| + + +#### ComposeComponent Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | + + +#### Parameter Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +| name | string | Y | None| +| value | string | Y | The value of the parameter.| +| targets | Target | Y | The targets associated with the parameter.| + + +#### Target Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +| pointer | string | Y | The pointer indicating the location of the target.| +| components | string | Y | The components associated with the target.| + + +### Example: Cluster Enabled Application Deployment Specification + +```yaml +apiVersion: application.margo.org/v1alpha1 +kind: ApplicationDeployment +metadata: + annotations: + applicationId: com-northstartida-digitron-orchestrator + id: a3e2f5dc-912e-494f-8395-52cf3769bc06 + name: com-northstartida-digitron-orchestrator-deployment + namespace: margo-poc +spec: + deploymentProfile: + type: helm.v3 + components: + - name: database-services + properties: + repository: oci://quay.io/charts/realtime-database-services + revision: 2.3.7 + timeout: 8m30s + wait: "true" + - name: digitron-orchestrator + properties: + repository: oci://northstarida.azurecr.io/charts/northstarida-digitron-orchestrator + revision: 1.0.9 + wait: "true" + parameters: + adminName: + value: Some One + targets: + - pointer: administrator.name + components: + - digitron-orchestrator + adminPrincipalName: + value: someone@somewhere.com + targets: + - pointer: administrator.userPrincipalName + components: + - digitron-orchestrator + cpuLimit: + value: "4" + targets: + - pointer: settings.limits.cpu + components: + - digitron-orchestrator + idpClientId: + value: 123-ABC + targets: + - pointer: idp.clientId + components: + - digitron-orchestrator + idpName: + value: Azure AD + targets: + - pointer: idp.name + components: + - digitron-orchestrator + idpProvider: + value: aad + targets: + - pointer: idp.provider + components: + - digitron-orchestrator + idpUrl: + value: https://123-abc.com + targets: + - pointer: idp.providerUrl + components: + - digitron-orchestrator + - pointer: idp.providerMetadata + components: + - digitron-orchestrator + memoryLimit: + value: "16384" + targets: + - pointer: settings.limits.memory + components: + - digitron-orchestrator + pollFrequency: + value: "120" + targets: + - pointer: settings.pollFrequency + components: + - digitron-orchestrator + - database-services + siteId: + value: SID-123-ABC + targets: + - pointer: settings.siteId + components: + - digitron-orchestrator + - database-services +``` + +### Example: Standalone Device Application Deployment Specification + +```yaml +apiVersion: application.margo.org/v1alpha1 +kind: ApplicationDeployment +metadata: + annotations: + applicationId: com-northstartida-digitron-orchestrator + id: ad9b614e-8912-45f4-a523-372358765def + name: com-northstartida-digitron-orchestrator-deployment + namespace: margo-poc +spec: + deploymentProfile: + type: compose + components: + - name: digitron-orchestrator-docker + properties: + keyLocation: https://northsitarida.com/digitron/docker/public-key.asc + packageLocation: https://northsitarida.com/digitron/docker/digitron-orchestrator.tar.gz + parameters: + adminName: + value: Some One + targets: + - pointer: ENV.ADMIN_NAME + components: + - digitron-orchestrator-docker + adminPrincipalName: + value: someone@somewhere.com + targets: + - pointer: ENV.ADMIN_PRINCIPALNAME + components: + - digitron-orchestrator-docker + idpClientId: + value: 123-ABC + targets: + - pointer: ENV.IDP_CLIENT_ID + components: + - digitron-orchestrator-docker + idpName: + value: Azure AD + targets: + - pointer: ENV.IDP_NAME + components: + - digitron-orchestrator-docker + idpProvider: + value: aad + targets: + - pointer: ENV.IDP_PROVIDER + components: + - digitron-orchestrator-docker + idpUrl: + value: https://123-abc.com + targets: + - pointer: ENV.IDP_URL + components: + - digitron-orchestrator-docker + pollFrequency: + value: "120" + targets: + - pointer: ENV.POLL_FREQUENCY + components: + - digitron-orchestrator-docker + siteId: + value: SID-123-ABC + targets: + - pointer: ENV.SITE_ID + components: + - digitron-orchestrator-docker +``` \ No newline at end of file diff --git a/docs/specification/margo-management-interface/device-capabilities-api.md b/docs/specification/margo-management-interface/device-capabilities-api.md new file mode 100644 index 00000000..ff4f52e0 --- /dev/null +++ b/docs/specification/margo-management-interface/device-capabilities-api.md @@ -0,0 +1,35 @@ +# Device Capabilities API + +Devices MUST provide the Workload Fleet Management service with its capabilities and characteristics. This is done by calling the Device API's `device capabilities` endpoint. Reporting the device capabilities is the final step in the onboarding of the device's client. + +To ensure the WFM is kept up to date, the device's client MUST send updated capabilities information if any changes occur to the information originally provided (i.e., additional memory is added to the device). + +- Requests to this endpoint MUST be authenticated using the HTTP Message Signature method as defined in the [Payload Security](../margo-management-interface/api-requirements-and-security.md#payload-security-method) section. + +## Route and HTTP Methods + +```https +POST /api/v1/clients/{clientId}/capabilities +PUT /api/v1/clients/{clientId}/capabilities +``` + +### Route Parameters + +|Parameter | Type | Required? | Description| +|----------|------|-----------|------------| +| {clientId} | string | Y | The unique identifier of the (device) client registered with the WFM during onboarding. | + +### Response Codes + +| Code | Description | +|------|-------------| +| 201 OK | The device capabilities document was added, or updated, successfully | +| 400 Bad Request | Missing or invalid content-digest header. Ensure the SHA256 hash of the base64-encoded payload is included. | +| 401 Unauthorized | Signature verification failed. Ensure you are signing with the correct X.509 private key. | +| 403 Forbidden | Client certificate is not trusted or has been revoked. | +| 422 Unprocessable Content | Request body includes a semantic error. | + +## Request Body + +[DeviceCapabilitiesManifest](./device-capabilities.md) document describing a device's capabilities and characteristics. + diff --git a/system-design/specification/margo-management-interface/device-capabilities.md b/docs/specification/margo-management-interface/device-capabilities.md similarity index 100% rename from system-design/specification/margo-management-interface/device-capabilities.md rename to docs/specification/margo-management-interface/device-capabilities.md diff --git a/system-design/specification/margo-management-interface/device-client-onboarding.md b/docs/specification/margo-management-interface/device-client-onboarding.md similarity index 100% rename from system-design/specification/margo-management-interface/device-client-onboarding.md rename to docs/specification/margo-management-interface/device-client-onboarding.md diff --git a/system-design/specification/margo-management-interface/management-interface-swagger.md b/docs/specification/margo-management-interface/management-interface-swagger.md similarity index 100% rename from system-design/specification/margo-management-interface/management-interface-swagger.md rename to docs/specification/margo-management-interface/management-interface-swagger.md diff --git a/docs/specification/margo-management-interface/workload-management-api-1.0.0.yaml b/docs/specification/margo-management-interface/workload-management-api-1.0.0.yaml new file mode 100644 index 00000000..f47a1d29 --- /dev/null +++ b/docs/specification/margo-management-interface/workload-management-api-1.0.0.yaml @@ -0,0 +1,995 @@ +openapi: 3.0.3 +info: + title: Margo Workload Management API + version: 1.0.0 + description: + API for managing workloads on Margo-compliant edge devices. + Includes the APIs for exchanging desired state and current state. + Communication is secured using server-side TLS (TLS 1.3 preferred), + and payloads are signed using X.509 certificates. + +servers: + - url: https://wfm.margo.org/ + description: Workload Fleet Manager API + +security: + - PayloadSignature: [] + +paths: + /api/v1/onboarding/certificate: + get: + summary: Download Root CA certificate + security: [] + responses: + '200': + description: Root CA certificate + content: + application/json: + schema: + type: object + properties: + certificate: + type: string + description: Base64-encoded certificate text + /api/v1/onboarding: + post: + requestBody: + content: + application/json: + schema: + type: object + required: [apiVersion, kind, certificate] + properties: + apiVersion: + type: string + description: API version identifier + kind: + type: string + enum: [OnboardingRequest] + description: Resource kind + certificate: + description: Base64-encoded client certificate + type: string + required: true + responses: + '201': + content: + application/json: + schema: + properties: + clientId: + type: string + type: object + description: New client onboarded successfully. + '400': + content: + application/json: + schema: + properties: + error: + example: Invalid certificate + type: string + type: object + description: Invalid certificate format or structure. + '403': + content: + application/json: + schema: + properties: + error: + example: Client rejected + type: string + type: object + description: Client certificate not trusted or client rejected. + security: + - PayloadSignature: [] + summary: Complete onboarding with client certificate + + /api/v1/clients/{clientId}/capabilities/{deviceId}: + post: + summary: Report device capabilities + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + - name: deviceId + in: path + required: true + schema: + $ref: '#/components/schemas/DeviceId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceCapabilitiesManifest' + responses: + '201': + description: Capabilities reported successfully + '400': + description: Missing or invalid content-digest header. Ensure the SHA256 hash of the payload is included. + '401': + description: Signature verification failed. Ensure you are signing with the correct X.509 private key. + '403': + description: Client certificate is not trusted or has been revoked. + '404': + description: No client with the given `clientID` was found. + '422': + description: Request body includes a semantic error. + put: + summary: Update device capabilities (Update) + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + - name: deviceId + in: path + required: true + schema: + $ref: '#/components/schemas/DeviceId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceCapabilitiesManifest' + responses: + '201': + description: Capabilities reported successfully + '400': + description: Missing or invalid content-digest header. Ensure the SHA256 hash of the payload is included. + '401': + description: Signature verification failed. Ensure you are signing with the correct X.509 private key. + '403': + description: Client certificate is not trusted or has been revoked. + '404': + description: No client with the given `clientID` was found. + '422': + description: Request body includes a semantic error. + delete: + summary: Remove device (Unregister) + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + - name: deviceId + in: path + required: true + schema: + $ref: '#/components/schemas/DeviceId' + responses: + '204': + description: Device capabilities removed successfully + '400': + description: Missing or invalid content-digest header. Ensure the SHA256 hash of the payload is included. + '401': + description: Signature verification failed. Ensure you are signing with the correct X.509 private key. + '403': + description: Client certificate is not trusted or has been revoked. + '404': + description: Client or device not found. + /api/v1/clients/{clientId}/bundles/{digest}: + get: + summary: Retrieve bundle information for a specific device and digest + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + description: Unique identifier of the device-client + - name: digest + in: path + required: true + schema: + type: string + description: Content-addressable digest of the bundle archive. MUST conform to the 'digest' attribute in the Digest Specification and MUST equal the digest computed over the exact sequence of bytes (per Exact Bytes Rule) in the HTTP 200 OK response body. If the server cannot produce content whose digest matches this value it MUST return 404 Not Found. + - in: header + name: If-None-Match + required: false + schema: + type: string + description: Quoted ETag (same as digest) previously returned for this bundle. + responses: + '200': + description: Bundle archive (immutable) + headers: + ETag: + schema: + type: string + description: New ETag for the returned manifest + Cache-Control: + schema: + type: string + description: public, max-age=31536000, immutable + content: + application/vnd.margo.bundle.v1+tar+gzip: + schema: + type: string + format: binary + description: Gzip-compressed tar containing one YAML file per deployment. + '304': + description: Representation not modified + '404': + description: Bundle not found for the given digest + '400': + description: Invalid request. + # TBD + # '500': + # $ref: '#/components/responses/ErrorResponse' + + /api/v1/clients/{clientId}/deployments: + get: + summary: Retrieve the complete desired state for all workloads assigned to a device + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + description: The unique identifier of the Edge Compute Device making the request + - name: If-None-Match + in: header + required: false + schema: + type: string + description: > + ETag value of the last successfully synced manifest. The ETag is returned to the client from the /deployments endpoint, it is the digest of the state manifest. + - name: Accept + in: header + required: false + schema: + type: string + description: > + Indicates which manifest formats the client supports. + Supported values: application/vnd.margo.manifest.v1+json. + responses: + '200': + description: Manifest returned in the negotiated format + headers: + Content-Type: + schema: + type: string + description: Format of the returned manifest + ETag: + schema: + type: string + description: New ETag for the returned manifest + content: + application/vnd.margo.manifest.v1+json: + schema: + $ref: '#/components/schemas/UnsignedAppStateManifest' + '304': + description: Not Modified - Manifest has not changed + '406': + description: Not Acceptable - Server cannot generate a response matching the Accept header + + + /api/v1/clients/{clientId}/deployments/{deploymentId}/{digest}: + get: + summary: Retrieve an individual ApplicationDeployment YAML file + security: + - PayloadSignature: [] + description: > + This endpoint is used by the client to fetch the YAML for a single ApplicationDeployment after it has processed a new State Manifest and identified a small number of new or updated deployments. This allows for highly efficient, incremental updates without needing to download the full bundle. + To make individual workload retrievals race-free and cache-friendly, this endpoint is content-addressable: the digest of the expected YAML is part of the URL. This guarantees immutability of the fetched resource and prevents a time-of-check / time-of-use race where a deployment changes between manifest retrieval and content fetch. + parameters: + - name: clientId + in: path + required: true + schema: + type: string + description: Unique identifier of the Edge Compute Device + - name: deploymentId + in: path + required: true + schema: + type: string + description: Unique identifier for the application deployment + - name: digest + in: path + required: true + schema: + type: string + description: > + Content-addressable digest of the ApplicationDeployment YAML file. MUST conform to the Digest Specification and MUST equal the digest computed over the exact sequence of bytes (per Exact Bytes Rule) in the HTTP 200 OK response body. If the server cannot produce content whose digest matches this value it MUST return 404 Not Found. + - name: If-None-Match + in: header + required: false + schema: + type: string + description: > + Optional ETag for caching. The ETag is returned to the client from the /deployments endpoint, it is the digest of the state manifest. + - name: Accept-Encoding + in: header + required: false + schema: + type: string + description: Indicates supported compression formats (e.g., gzip, br) + responses: + '200': + description: > + The response body is the raw ApplicationDeployment YAML file (Content-Type: application/yaml). The content MUST match the {digest} path segment; the server MUST return 404 if it does not have the exact digest referenced. + headers: + Content-Type: + schema: + type: string + description: application/yaml + ETag: + schema: + type: string + description: > + The ETag is returned to the client from the /deployments endpoint, it is the digest of the state manifest. + Cache-Control: + schema: + type: string + description: public, max-age=31536000, immutable + Vary: + schema: + type: string + description: Accept-Encoding + content: + application/yaml: + schema: + type: string + description: Raw YAML content of the ApplicationDeployment + '404': + description: Deployment not found for the given digest + + /api/v1/clients/{clientId}/deployments/{deploymentId}/status: + post: + summary: Report deployment status + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + - name: deploymentId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeploymentStatusManifest' + responses: + '200': + description: The deployment status was added, or updated, successfully. + '400': + description: Missing or invalid content-digest header. Ensure the SHA256 hash of the base64-encoded payload is included. + '401': + description: Signature verification failed. Ensure you are signing with the correct X.509 private key. + '403': + description: Client certificate is not trusted or has been revoked. + '422': + description: Request body includes a semantic error. + +components: + securitySchemes: + # TODO: fix this as we are following RFC 9421, instead of a custom signature header field + PayloadSignature: + type: apiKey + in: header + name: X-Payload-Signature + description: > + Base64-encoded payload signature using SHA-256 and device certificate. + Format: public_key;digital_signature + + # Modifications compared to pre-data-model stage: + # - ManifestVersion INLINED into UnsignedAppStateManifest(OpenAPI)/DesiredStateManifest(LinkML) + # - appDeploymentParams INLINED into appDeploymentSpec(OpenAPI)/Spec(LinkML) + schemas: + appDeploymentProfile: + type: object + description: Represents a deployment configuration for the application. + properties: + type: + type: string + description: Defines the type of this deployment configuration for the application. The + allowed values are `helm.v3`, to indicate the deployment profile's format + is Helm version 3, and `compose` to indicate the deployment profile's format + is a Compose file. When installing the application on a device supporting + the Kubernetes platform, all `helm.v3` components, and only `helm.v3` components, + will be provided to the device in same order they are listed in the application + description file. When installing the application on a device supporting + Compose, all `compose` components, and only `compose` components, will be + provided to the device in the same order they are listed in the application + description file. The device will install the components in the same order + they are listed in the application description file. + pattern: ^(helm\.v3|compose)$ + components: + type: array + items: + $ref: '#/components/schemas/Component' + description: Component element indicating the components to deploy when installing + the application. See the [Component](#component-attributes) section below. + required: + - type + - components + Component: + type: object + description: A class representing a component of a deployment profile. + properties: + name: + type: string + description: A unique name used to identify the component package. For helm + installations the name will be used as the chart name. The name must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. + properties: + $ref: '#/components/schemas/ComponentProperties' + description: A dictionary element specifying the component packages's deployment + details. See the [Component Properties](#componentproperties-attributes) + section below. + required: + - name + - properties + helmApplicationDeploymentProfileComponent: + type: object + description: '' + properties: + name: + type: string + description: A unique name used to identify the component package. For helm + installations the name will be used as the chart name. The name must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. + properties: + $ref: '#/components/schemas/ComponentProperties' + description: A dictionary element specifying the component packages's deployment + details. See the [Component Properties](#componentproperties-attributes) + section below. + required: + - name + - properties + composeApplicationDeploymentProfileComponent: + type: object + description: '' + properties: + name: + type: string + description: A unique name used to identify the component package. For helm + installations the name will be used as the chart name. The name must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. + properties: + $ref: '#/components/schemas/ComponentProperties' + description: A dictionary element specifying the component packages's deployment + details. See the [Component Properties](#componentproperties-attributes) + section below. + required: + - name + - properties + ComponentProperties: + type: object + description: Properties dictionary for component deployment details. + properties: + repository: + type: string + description: Repository location for the component. + revision: + type: string + description: Revision version for the component. + wait: + type: boolean + description: If True, indicates the device waits for the component installation + to complete. + timeout: + type: string + description: Time to wait for component installation to complete, formatted + as "##m##s". + packageLocation: + type: string + description: URL indicating the Compose package's location. + keyLocation: + type: string + description: URL for the public key used to validate a digitally signed package. + appParameterTarget: + type: object + description: Specifies where the parameter applies in the deployment. + properties: + pointer: + type: string + description: The name of the parameter in the deployment configuration. For + Helm deployments, this is the dot notation for the matching element in the + `values.yaml` file. This follows the same naming convention you would use + with the `--set` command line argument with the `helm install` command. For + compose deployments, this is the name of the environment variable to set. + components: + type: array + items: + type: string + description: Indicates which deployment profile [component](#component-attributes + the parameter target applies to. The component name specified here MUST match + a component name in the [deployment profiles](#deploymentprofile-attributes) + section. + required: + - pointer + - components + appDeploymentManifest: + type: object + description: A class representing the desired state of an entity. + properties: + apiVersion: + type: string + description: Identifier of the version of the API the object definition follows. + kind: + type: string + description: Must be `ApplicationDeployment`. + enum: + - ApplicationDeployment + metadata: + $ref: '#/components/schemas/appDeploymentMetadata' + description: Metadata element specifying characteristics about the application + deployment. See the [Metadata Attributes](#metadata-attributes) section below. + spec: + $ref: '#/components/schemas/appDeploymentSpec' + description: Spec element that defines deployment profile and parameters associated + with the application deployment. See the [Spec Attributes](#spec-attributes) + section below. + required: + - apiVersion + - kind + - metadata + - spec + appDeploymentMetadata: + type: object + description: Metadata associated with the desired state. + properties: + annotations: + $ref: '#/components/schemas/DeploymentAnnotations' + description: Defines the application ID and unique identifier associated to + the deployment specification. Needs to be assigned by the Workload Orchestration + Software. See the [Annotation Attributes](#annotations-attributes) section + below. + name: + type: string + description: When deploying to Kubernetes, the manifests name. The name is chosen + by the workload orchestration vendor and is not displayed anywhere. + namespace: + type: string + description: When deploying to Kubernetes, the namespace the manifest is added + under. The namespace is chosen by the workload orchestration solution vendor. + deviceId: + type: string + pattern: ^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*(\/\*)?$ + required: + - annotations + - name + - namespace + - deviceId + DeploymentAnnotations: + type: object + description: A class representing annotations. + properties: + applicationId: + type: string + description: An identifier for the application. The id is used to help create + unique identifiers where required, such as namespaces. The id must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. The id MUST NOT be more than 200 characters. + The applicationId MUST match the associated application package Metadata "id" + attribute. + pattern: ^[-a-z0-9]{1,200}$ + id: + type: string + description: The unique identifier UUID of the deployment specification. Needs + to be assigned by the Workload Orchestration Software. + pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ + required: + - applicationId + - id + appParameterValue: + type: object + description: Defines a configurable parameter for the application. + properties: + name: + type: string + description: Name of the parameter. + value: + description: The parameter's default value. Accepted data types are string, + integer, double, boolean, array[string], array[integer], array[double], array[boolean]. + anyOf: + - type: boolean + - type: integer + - type: number + - type: string + targets: + type: array + items: + $ref: '#/components/schemas/appParameterTarget' + description: Used to indicate which component the value should be applied to + when installing, or updating, the application. See the [Target](#target-attributes) + section below. + required: + - targets + appDeploymentSpec: + type: object + description: Specification details of the desired state. + properties: + deploymentProfile: + $ref: '#/components/schemas/appDeploymentProfile' + description: Section that defines deployment details including type and components. + parameters: + type: object + description: Describes the configured parameters applied via the end-user. + required: + - deploymentProfile + - parameters + Resources: + type: object + description: Required resources element specifying the resources required to install + the application. + properties: + cpu: + $ref: '#/components/schemas/CPU' + description: CPU element specifying the CPU requirements for the application. + See the [CPU](#cpu-attributes) section below. + memory: + type: string + description: The minimum amount of memory required. The value is given in binary + units (`Ki` = Kibibytes, `Mi` = Mebibytes, `Gi` = Gibibytes). This is defined + by the application developer. After deployment of the application, the device + MUST provide this amount of memory for the application. + pattern: ^[0-9]+(Mi|Gi|Ki)$ + storage: + type: string + description: The amount of storage required for the application to run. This + encompasses the installed application and the data it needs to store. The + value is given in binary units (`Ki` = Kibibytes, `Mi` = Mebibytes, `Gi` = + Gibibytes, `Ti` Tebibytes, `Pi` = Pebibytes, `Ei` = Exbibytes). This is defined + by the application developer. After deployment of the application, the device + MUST provide this amount of storage for the application + pattern: ^[0-9]+(Mi|Gi|Ki|Ti|Pi|Ei)$ + peripherals: + type: array + items: + $ref: '#/components/schemas/DevicePeripheral' + description: Peripherals element specifying the peripherals required to run + the application. See the [Peripheral](#peripheral-attributes) section below. + interfaces: + type: array + items: + $ref: '#/components/schemas/DeviceCommunicationInterface' + description: Interfaces element specifying the communication interfaces required + to run the application. See the [Communication Interfaces](#communicationinterface-attributes) + section below. + CPU: + type: object + description: CPU element specifying the CPU requirements for the application. + properties: + cores: + type: number + description: The required amount of CPU cores the application must use to run + in its full functionality. Specified as decimal units of CPU cores (e.g., + `0.5` is half a core). This is defined by the application developer. After + deployment of the application, the device MUST provide this number of CPU + cores for the application. + architectures: + type: array + items: + type: string + description: Permissible CPU architecture values. + enum: + - amd64 + - x86_64 + - arm64 + - arm + - riscv64 + - other + description: The CPU architectures supported by the application. This can be + e.g. amd64, x86_64, arm64, arm. See the [CpuArchitectureType](#cpuarchitecturetype) + definition for all permissible values. Multiple arcitecture types can be specified, + as the deployment profile may support multiple CPU architectures. + required: + - cores + DevicePeripheral: + type: object + description: Peripheral hardware of a device. + properties: + type: + type: string + description: Permissible peripheral types. + enum: + - gpu + - display + - camera + - microphone + - speaker + - other + manufacturer: + type: string + description: The name of the manufacturer. If `manufacturer` is specified as + a requirement here, it may be difficult to find devices that can host the application. + Please use these requirements with caution. + model: + type: string + description: The model of the peripheral. If `model` is specified as a requirement + here, it may be difficult to find devices that can host the application. Please + use these requirements with caution. + required: + - type + DeviceCommunicationInterface: + type: object + description: Communication interface of a device. + properties: + type: + type: string + description: Permissible communication interface types. + enum: + - ethernet + - wifi + - cellular + - bluetooth + - usb + - canbus + - rs232 + - other + required: + - type + UnsignedAppStateManifest: + type: object + description: Manifest from the Workload Fleet Manager, representing the complete + desired workload configuration assigned to the device. + properties: + manifestVersion: + type: integer + description: Monotonically increasing unsigned 64-bit integer in the inclusive + range [1, 2^64-1]. Prevents rollback attacks. + minimum: 1 + maximum: 18446744073709551615 + bundle: + $ref: '#/components/schemas/DeploymentBundleRef' + description: Package optimization containing multiple ApplicationDeployment + YAMLs. + deployments: + type: array + items: + $ref: '#/components/schemas/DeploymentManifestRef' + description: List of deployment objects describing each workload. + required: + - manifestVersion + - bundle + - deployments + DeploymentBundleRef: + type: object + description: Describes an archive containing all referenced ApplicationDeployment + YAMLs. If there are zero deployments (i.e., the deployments array is empty), this + field MUST be present with the value null. An empty archive MUST NOT be served. + properties: + mediaType: + type: string + description: MUST be application/vnd.margo.bundle.v1+tar+gzip, which denotes + a gzip-compressed tar archive (commonly delivered as a .tar.gz) whose root + contains one or more ApplicationDeployment YAML files. Servers MUST set the + HTTP Content-Type to this media type. The archive MUST contain exactly the + set of YAML files referenced by deployments. + enum: + - application/vnd.margo.bundle.v1+tar+gzip + digest: + type: string + description: Digest of the bundle archive. MUST equal the digest computed over + the exact sequence of bytes in the bundle endpoint's HTTP 200 OK response + body. See Protocol - Digest for further details. + pattern: ^[a-zA-Z0-9_-]+:[0-9a-fA-F]+$ + sizeBytes: + type: integer + description: Optional unsigned 64-bit advisory estimate of the decoded payload + length in bytes for the bundle archive. Provided for bandwidth estimation + and update planning. MUST NOT be used for integrity verification. + minimum: 0 + url: + type: string + format: uri + description: Content-addressable retrieval endpoint for the bundle of the form + /api/v1/clients/{clientId}/bundles/{digest} where {digest} equals bundle.digest. + pattern: ^/[a-zA-Z0-9-._~:/?#\[\]@!$&'()*+,;=]+$ + required: + - mediaType + - digest + - url + DeploymentManifestRef: + type: object + description: Reference to an individual ApplicationDeployment within the desired + state manifest. + properties: + deploymentId: + type: string + description: The UUID of the deployment. MUST equal metadata.annotations.id + in the ApplicationDeployment. + digest: + type: string + description: "Digest of the corresponding ApplicationDeployment YAML file.\n\ + \ MUST equal the digest computed over the exact sequence of bytes in the individual\ + \ deployment endpoint's HTTP 200 OK response body.\n See Protocol - Digest\ + \ for further details." + pattern: ^[a-zA-Z0-9_-]+:[0-9a-fA-F]+$ + sizeBytes: + type: integer + description: Optional unsigned 64-bit advisory estimate of the decoded payload + length in bytes for the ApplicationDeployment YAML. Provided for bandwidth + estimation and update planning. MUST NOT be used for integrity verification. + minimum: 0 + url: + type: string + format: uri + description: Content-addressable retrieval endpoint for the ApplicationDeployment + YAML of the form /api/v1/clients/{clientId}/deployments/{deploymentId}/{digest} + where {digest} equals deployments[].digest. + pattern: ^/[a-zA-Z0-9-._~:/?#\[\]@!$&'()*+,;=]+$ + required: + - deploymentId + - digest + - url + DeviceCapabilitiesManifest: + type: object + description: Capabilities of a device on which applications can be deployed. + properties: + apiVersion: + type: string + description: Identifier of the version the API resource follows. + kind: + type: string + description: Must be `DeviceCapabilitiesManifest`. + enum: + - DeviceCapabilitiesManifest + properties: + $ref: '#/components/schemas/Properties' + description: Element that defines characteristics about the device. See the + [Properties Attributes](#properties-attributes) section below. + required: + - apiVersion + - kind + - properties + Properties: + type: object + description: Device properties reported to the WFM. + properties: + id: + type: string + description: Unique deviceID assigned to the device via the Device Owner. + vendor: + type: string + description: Defines the device vendor. + modelNumber: + type: string + description: Defines the model number of the device. + serialNumber: + type: string + description: Defines the serial number of the device. + roles: + type: array + items: + type: string + description: Role a device can provide to the Margo environment. + enum: + - Standalone Cluster + - Standalone Device + - Cluster Leader + - Gateway + description: 'Element that defines the device role it can provide to the Margo + environment. MUST be one of the following: Standalone Cluster, Cluster Leader, + Standalone Device, or Gateway.' + resources: + $ref: '#/components/schemas/Resources' + description: Element that defines the device's resources available to the application + deployed on the device. See the [Resources Attributes](#resources-attributes) + section below. + required: + - id + - vendor + - modelNumber + - serialNumber + - roles + - resources + DeploymentStatusManifest: + type: object + description: Manifest sent by the device client to report the deployment status + of a workload. + properties: + apiVersion: + type: string + kind: + type: string + enum: + - DeploymentStatusManifest + deploymentId: + type: string + description: The unique identifier of the deployment whose status is being reported. + deviceId: + type: string + description: Id of the device hosting the deployment. Includes the full device + hierarchy if applicable. This attribute is required when reporting on behalf + of a child-device. + pattern: ^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*(\/\*)?$ + status: + $ref: '#/components/schemas/Status' + description: Overall status of the deployment. + components: + type: array + items: + $ref: '#/components/schemas/ComponentStatus' + description: Per-component status list. + required: + - apiVersion + - kind + - deploymentId + - status + - components + Status: + type: object + description: Overall deployment state and optional error details. + properties: + state: &id001 + type: string + description: Permissible deployment states. + enum: + - pending + - installing + - installed + - failed + - removing + - removed + error: + $ref: '#/components/schemas/Error' + description: Optional error details when the state is `failed`. + required: + - state + Error: + type: object + description: Error details associated with a failed deployment state. + properties: + code: + type: string + source: + type: string + description: Identifies the source of the error. It is set to the device id, + with its full hierarchy if applicable, of the device generating the error, + or to the component name of the component generating the error. + message: + type: string + ComponentStatus: + type: object + description: Status of a component deployment. + properties: + name: + type: string + state: *id001 + error: + $ref: '#/components/schemas/Error' + description: Optional error details when the state is `failed`. + required: + - name + - state + DeviceId: + type: string + pattern: ^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*$ + description: 'Unique identifier of a device or device hierarchy. Format: "{id}[/{id}[/{id}...]]". + The top-level id is required and must include only unreserved characters as specified + in RFC3986. Subsequent ids indicate child devices in a gateway hierarchy and must + also use only unreserved characters.' + DeviceId_with_asterisk: + type: string + pattern: ^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*(\/\*)?$ diff --git a/system-design/specification/observability/collecting-workload-observability-data.md b/docs/specification/observability/collecting-workload-observability-data.md similarity index 100% rename from system-design/specification/observability/collecting-workload-observability-data.md rename to docs/specification/observability/collecting-workload-observability-data.md diff --git a/system-design/specification/observability/consuming-workload-observability-data.md b/docs/specification/observability/consuming-workload-observability-data.md similarity index 100% rename from system-design/specification/observability/consuming-workload-observability-data.md rename to docs/specification/observability/consuming-workload-observability-data.md diff --git a/system-design/specification/observability/publishing-workload-observability-data.md b/docs/specification/observability/publishing-workload-observability-data.md similarity index 100% rename from system-design/specification/observability/publishing-workload-observability-data.md rename to docs/specification/observability/publishing-workload-observability-data.md diff --git a/legacy/README.md b/legacy/README.md new file mode 100644 index 00000000..08b77327 --- /dev/null +++ b/legacy/README.md @@ -0,0 +1,48 @@ +# Legacy + +This directory contains artifacts from the pre-migration era that should +disappear once this branch has been merged into `pre-draft`. + +- `doc-generation/` — original generation scripts (superseded by `tools/`). +- `src-specification/` — original per-resource schemas and templates + (superseded by `model/` and `tools/templates/`). +- `validate-openapi.py` — one-shot migration aid comparing the generated + OpenAPI spec against the hand-written `pre-draft` branch. + +## Usage: `validate-openapi.py` + +Requires Poetry dependencies (`poetry install`). Run from repository root. + +### Full comparison + +``` +poetry run python legacy/validate-openapi.py +``` + +Prints a structural diff between the generated OpenAPI spec and the `pre-draft` +branch: added/removed schemas, changed properties, type mismatches. + +### Single schema detail + +``` +poetry run python legacy/validate-openapi.py --schema +poetry run python legacy/validate-openapi.py -s +``` + +> [!Tip] +> Example: `poetry run python legacy/validate-openapy.py --schema DeploymentBundleRef` + +Shows verbose details for one schema only. `` is the name as it +appears in the generated output's `components/schemas`. + +### With YAML dump + +``` +poetry run python legacy/validate-openapi.py --schema --yaml +poetry run python legacy/validate-openapi.py -s -y +``` + +Prints the pre-draft and generated YAML for the given schema. + +> [!Tip] +> Example: `poetry run python legacy/validate-openapy.py --schema DeploymentBundleRef --yaml` diff --git a/doc-generation/check-examples.bash b/legacy/doc-generation/check-examples.bash similarity index 100% rename from doc-generation/check-examples.bash rename to legacy/doc-generation/check-examples.bash diff --git a/doc-generation/configurations/application-package-api.json b/legacy/doc-generation/configurations/application-package-api.json similarity index 100% rename from doc-generation/configurations/application-package-api.json rename to legacy/doc-generation/configurations/application-package-api.json diff --git a/doc-generation/configurations/desired-state-api.json b/legacy/doc-generation/configurations/desired-state-api.json similarity index 100% rename from doc-generation/configurations/desired-state-api.json rename to legacy/doc-generation/configurations/desired-state-api.json diff --git a/doc-generation/generate-documentation.bash b/legacy/doc-generation/generate-documentation.bash similarity index 100% rename from doc-generation/generate-documentation.bash rename to legacy/doc-generation/generate-documentation.bash diff --git a/src/specification/applications/application-description.linkml.yaml b/legacy/src-specification/applications/application-description.linkml.yaml similarity index 100% rename from src/specification/applications/application-description.linkml.yaml rename to legacy/src-specification/applications/application-description.linkml.yaml diff --git a/src/specification/applications/resources/class.md.jinja2 b/legacy/src-specification/applications/resources/class.md.jinja2 similarity index 100% rename from src/specification/applications/resources/class.md.jinja2 rename to legacy/src-specification/applications/resources/class.md.jinja2 diff --git a/src/specification/applications/resources/examples/invalid/ApplicationDescription-001.yaml b/legacy/src-specification/applications/resources/examples/invalid/ApplicationDescription-001.yaml similarity index 100% rename from src/specification/applications/resources/examples/invalid/ApplicationDescription-001.yaml rename to legacy/src-specification/applications/resources/examples/invalid/ApplicationDescription-001.yaml diff --git a/src/specification/applications/resources/examples/invalid/ApplicationDescription-002.yaml b/legacy/src-specification/applications/resources/examples/invalid/ApplicationDescription-002.yaml similarity index 100% rename from src/specification/applications/resources/examples/invalid/ApplicationDescription-002.yaml rename to legacy/src-specification/applications/resources/examples/invalid/ApplicationDescription-002.yaml diff --git a/src/specification/applications/resources/examples/valid/ApplicationDescription-001.yaml b/legacy/src-specification/applications/resources/examples/valid/ApplicationDescription-001.yaml similarity index 100% rename from src/specification/applications/resources/examples/valid/ApplicationDescription-001.yaml rename to legacy/src-specification/applications/resources/examples/valid/ApplicationDescription-001.yaml diff --git a/src/specification/applications/resources/examples/valid/ApplicationDescription-002.yaml b/legacy/src-specification/applications/resources/examples/valid/ApplicationDescription-002.yaml similarity index 100% rename from src/specification/applications/resources/examples/valid/ApplicationDescription-002.yaml rename to legacy/src-specification/applications/resources/examples/valid/ApplicationDescription-002.yaml diff --git a/src/specification/applications/resources/index.md.jinja2 b/legacy/src-specification/applications/resources/index.md.jinja2 similarity index 100% rename from src/specification/applications/resources/index.md.jinja2 rename to legacy/src-specification/applications/resources/index.md.jinja2 diff --git a/src/specification/margo-management-interface/desired-state.linkml.yaml b/legacy/src-specification/margo-management-interface/desired-state.linkml.yaml similarity index 100% rename from src/specification/margo-management-interface/desired-state.linkml.yaml rename to legacy/src-specification/margo-management-interface/desired-state.linkml.yaml diff --git a/src/specification/margo-management-interface/resources/examples/invalid/DesiredState-001.yaml b/legacy/src-specification/margo-management-interface/resources/examples/invalid/DesiredState-001.yaml similarity index 100% rename from src/specification/margo-management-interface/resources/examples/invalid/DesiredState-001.yaml rename to legacy/src-specification/margo-management-interface/resources/examples/invalid/DesiredState-001.yaml diff --git a/src/specification/margo-management-interface/resources/examples/invalid/DesiredState-002.yaml b/legacy/src-specification/margo-management-interface/resources/examples/invalid/DesiredState-002.yaml similarity index 100% rename from src/specification/margo-management-interface/resources/examples/invalid/DesiredState-002.yaml rename to legacy/src-specification/margo-management-interface/resources/examples/invalid/DesiredState-002.yaml diff --git a/src/specification/margo-management-interface/resources/examples/invalid/DesiredState-003.yaml b/legacy/src-specification/margo-management-interface/resources/examples/invalid/DesiredState-003.yaml similarity index 100% rename from src/specification/margo-management-interface/resources/examples/invalid/DesiredState-003.yaml rename to legacy/src-specification/margo-management-interface/resources/examples/invalid/DesiredState-003.yaml diff --git a/src/specification/margo-management-interface/resources/examples/invalid/DesiredState-004.yaml b/legacy/src-specification/margo-management-interface/resources/examples/invalid/DesiredState-004.yaml similarity index 100% rename from src/specification/margo-management-interface/resources/examples/invalid/DesiredState-004.yaml rename to legacy/src-specification/margo-management-interface/resources/examples/invalid/DesiredState-004.yaml diff --git a/src/specification/margo-management-interface/resources/examples/valid/DesiredState-001.yaml b/legacy/src-specification/margo-management-interface/resources/examples/valid/DesiredState-001.yaml similarity index 100% rename from src/specification/margo-management-interface/resources/examples/valid/DesiredState-001.yaml rename to legacy/src-specification/margo-management-interface/resources/examples/valid/DesiredState-001.yaml diff --git a/src/specification/margo-management-interface/resources/examples/valid/DesiredState-002.yaml b/legacy/src-specification/margo-management-interface/resources/examples/valid/DesiredState-002.yaml similarity index 100% rename from src/specification/margo-management-interface/resources/examples/valid/DesiredState-002.yaml rename to legacy/src-specification/margo-management-interface/resources/examples/valid/DesiredState-002.yaml diff --git a/src/specification/margo-management-interface/resources/examples/valid/gateway-autonomous.yaml b/legacy/src-specification/margo-management-interface/resources/examples/valid/gateway-autonomous.yaml similarity index 100% rename from src/specification/margo-management-interface/resources/examples/valid/gateway-autonomous.yaml rename to legacy/src-specification/margo-management-interface/resources/examples/valid/gateway-autonomous.yaml diff --git a/src/specification/margo-management-interface/resources/examples/valid/gateway-directed.yaml b/legacy/src-specification/margo-management-interface/resources/examples/valid/gateway-directed.yaml similarity index 100% rename from src/specification/margo-management-interface/resources/examples/valid/gateway-directed.yaml rename to legacy/src-specification/margo-management-interface/resources/examples/valid/gateway-directed.yaml diff --git a/src/specification/margo-management-interface/resources/index.md.jinja2 b/legacy/src-specification/margo-management-interface/resources/index.md.jinja2 similarity index 100% rename from src/specification/margo-management-interface/resources/index.md.jinja2 rename to legacy/src-specification/margo-management-interface/resources/index.md.jinja2 diff --git a/legacy/validate-openapi.py b/legacy/validate-openapi.py new file mode 100644 index 00000000..eab8692e --- /dev/null +++ b/legacy/validate-openapi.py @@ -0,0 +1,877 @@ +#!/usr/bin/env python3 +"""Compare the pre-draft OpenAPI spec with the generated one at object level.""" + +import argparse +import json +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +import yaml + + +# ── Color support ────────────────────────────────────────────── +_USE_COLOR = sys.stdout.isatty() + + +def _c(code: str, text: str) -> str: + return f"\033[{code}m{text}\033[0m" if _USE_COLOR else text + + +def bold(text: str) -> str: + return _c("1", text) + + +def green(text: str) -> str: + return _c("32", text) + + +def red(text: str) -> str: + return _c("31", text) + + +def yellow(text: str) -> str: + return _c("33", text) + + +def cyan(text: str) -> str: + return _c("36", text) + + +def dim(text: str) -> str: + return _c("2", text) + + +# ─────────────────────────────────────────────────────────────── + + +ROOT = Path(__file__).resolve().parent.parent +PRE_DRAFT_FILE = "system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml" +GENERATED_FILE = "system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml" +# GENERATED_FILE = "build/artifacts/openapi/workload-management-api-1.0.0.openapi.yaml" + + +def get_pre_draft_spec() -> dict: + """Retrieve the OpenAPI spec from the pre-draft branch.""" + result = subprocess.run( + ["git", "show", f"pre-draft:{PRE_DRAFT_FILE}"], + capture_output=True, + text=True, + cwd=ROOT, + ) + if result.returncode == 0: + return yaml.safe_load(result.stdout) + print(f"ERROR: could not retrieve pre-draft file: {result.stderr}", file=sys.stderr) + sys.exit(1) + + +def get_generated_spec() -> dict: + """Read the generated OpenAPI spec from disk.""" + path = ROOT / GENERATED_FILE + if not path.exists(): + print(f"ERROR: generated file not found: {path}", file=sys.stderr) + sys.exit(1) + with open(path) as f: + return yaml.safe_load(f) + + +def deep_diff(label: str, ref, gen, path: str = "", errors: list | None = None) -> list: + """Recursively compare two YAML-parsed objects and collect differences.""" + if errors is None: + errors = [] + + if type(ref) != type(gen): + errors.append( + f"{path}: TYPE MISMATCH — {type(ref).__name__} vs {type(gen).__name__}" + ) + return errors + + if isinstance(ref, dict): + ref_keys = set(ref.keys()) + gen_keys = set(gen.keys()) + only_ref = ref_keys - gen_keys + only_gen = gen_keys - ref_keys + if only_ref: + errors.append(f"{path}: MISSING IN GENERATED — {sorted(only_ref)}") + if only_gen: + errors.append(f"{path}: EXTRA IN GENERATED — {sorted(only_gen)}") + common = ref_keys & gen_keys + for key in sorted(common): + deep_diff(label, ref[key], gen[key], f"{path}.{key}", errors) + elif isinstance(ref, list): + if len(ref) != len(gen): + errors.append(f"{path}: LIST LENGTH MISMATCH — {len(ref)} vs {len(gen)}") + else: + for i, (r, g) in enumerate(zip(ref, gen)): + deep_diff(label, r, g, f"{path}[{i}]", errors) + elif isinstance(ref, str): + if ref != gen: + errors.append(f"{path}: STRING MISMATCH — {ref!r} vs {gen!r}") + elif isinstance(ref, (int, float)): + if ref != gen: + errors.append(f"{path}: NUMBER MISMATCH — {ref!r} vs {gen!r}") + elif isinstance(ref, bool): + if ref != gen: + errors.append(f"{path}: BOOL MISMATCH — {ref!r} vs {gen!r}") + elif ref is None: + if gen is not None: + errors.append(f"{path}: NULL MISMATCH — None vs {gen!r}") + else: + if ref != gen: + errors.append(f"{path}: VALUE MISMATCH — {ref!r} vs {gen!r}") + + return errors + + +def _is_desc_only(err: str) -> bool: + """True if the error is a description-only string mismatch.""" + return re.match(r".*\.description: STRING MISMATCH", err) is not None + + +def _is_aprop(err: str) -> bool: + """True if the error is about additionalProperties.""" + return "additionalPropert" in err + + +def _categorize_errors(errors: list[str]) -> dict[str, list[str]]: + """Split errors into description-only, additionalProperties, and structural.""" + desc: list[str] = [] + aprop: list[str] = [] + struct: list[str] = [] + for e in errors: + if _is_desc_only(e): + desc.append(e) + elif _is_aprop(e): + aprop.append(e) + else: + struct.append(e) + return {"desc": desc, "aprop": aprop, "struct": struct} + + +def compare_section(label: str, ref: dict, gen: dict, path: str, errors: list) -> None: + """Compare a top-level section.""" + section_errors = deep_diff(label, ref.get(path, {}), gen.get(path, {}), path) + if section_errors: + print(f"\n {label} ({red(str(len(section_errors)))} diff(s)):") + for e in section_errors: + print(f" - {dim(e)}") + else: + print(f" {label}: {green('✓ IDENTICAL')}") + + +MARKER_LINE = ( + " # Schemas removed because they are neither direct nor transitive requirements:" +) + +TEMPLATE_FILE = "tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml" + +# Pattern for standardized INLINED comments: INLINED into (OpenAPI)[/(LinkML)] +INLINED_PATTERN = re.compile( + r"#\s*-\s*(\S+)\s+INLINED\s+into\s+\S+\(OpenAPI\)(?:/\S+\(LinkML\))?" +) + + +def get_inlined_schemas() -> set[str]: + """Scan the template file for INLINED comment markers and return schema names.""" + path = ROOT / TEMPLATE_FILE + if not path.exists(): + return set() + result: set[str] = set() + with open(path) as f: + for line in f: + m = INLINED_PATTERN.search(line) + if m: + result.add(m.group(1)) + return result + + +def get_xlinkml_renames() -> dict[str, str]: + """Extract x-linkml-source renames from the template file.""" + renames: dict[str, str] = {} + path = ROOT / TEMPLATE_FILE + if not path.exists(): + return renames + with open(path) as f: + tpl = yaml.safe_load(f) + schemas = tpl.get("components", {}).get("schemas", {}) + for name, spec in schemas.items(): + src = spec.get("x-linkml-source") + if src and src != name: + renames[name] = src + return renames + + +def find_rename_candidates( + ref_schemas: dict, gen_schemas: dict +) -> list[tuple[str, str, float]]: + """Heuristically match missing ref schemas to extra gen schemas by structure. + + Compares property sets, required fields, and type (excluding descriptions). + Returns (ref_name, gen_name, score) tuples sorted by descending score. + """ + + def _signature(name: str, s: dict) -> dict: + props = ( + set(s.get("properties", {}).keys()) + if isinstance(s.get("properties"), dict) + else set() + ) + req = set(s.get("required", [])) + typ = s.get("type") + return {"props": props, "req": req, "type": typ, "name": name} + + ref_sigs = {n: _signature(n, s) for n, s in ref_schemas.items()} + gen_sigs = {n: _signature(n, s) for n, s in gen_schemas.items()} + + candidates: list[tuple[str, str, float]] = [] + for rn, rs in ref_sigs.items(): + if rn in gen_schemas: + continue + for gn, gs in gen_sigs.items(): + if gn in ref_schemas: + continue + if rs["type"] != gs["type"]: + continue + prop_union = rs["props"] | gs["props"] + if not prop_union: + continue + prop_score = len(rs["props"] & gs["props"]) / len(prop_union) + req_score = ( + 1.0 + if not rs["req"] and not gs["req"] + else len(rs["req"] & gs["req"]) / len(rs["req"] | gs["req"]) + ) + score = 0.7 * prop_score + 0.3 * req_score + if score >= 0.3: + candidates.append((rn, gn, round(score, 3))) + candidates.sort(key=lambda x: -x[2]) + return candidates + + +def get_not_needed_schemas() -> set[str]: + """Scan the generated file text for a block comment listing removed schemas. + + Looks for a line matching MARKER_LINE, then collects every following + `` # `` line until a non-matching line is encountered. + """ + path = ROOT / GENERATED_FILE + if not path.exists(): + return set() + result = set() + in_block = False + schema_pat = re.compile(r"^ # (\S+)") + with open(path) as f: + for line in f: + if line.rstrip("\n") == MARKER_LINE: + in_block = True + continue + if in_block: + m = schema_pat.match(line) + if m: + result.add(m.group(1)) + else: + break + return result + + +def _schema_signature(s: dict) -> dict: + """Extract a comparable signature from a schema dict, ignoring descriptions.""" + if not isinstance(s, dict): + return {} + props = s.get("properties", {}) + return { + "type": s.get("type"), + "properties": set(props.keys()) if isinstance(props, dict) else set(), + "required": set(s.get("required", [])), + "enum": sorted(s.get("enum", [])) if isinstance(s.get("enum"), list) else [], + "additionalProperties": isinstance(s.get("additionalProperties"), dict) + or s.get("additionalProperties") is True, + } + + +def _collect_inline_properties(ref_schemas: dict) -> list[tuple[str, str, dict]]: + """Walk all pre-draft schemas and collect inline object properties. + + Only collects nested properties (not the top-level schema itself) + to avoid false matches where a gen schema is structurally similar + to a parent schema that happens to share property names. + + Returns [(parent_name, property_path, inline_schema), ...] where + property_path is e.g. 'properties.foo.items' for nested inline objects. + """ + results: list[tuple[str, str, dict]] = [] + + def _walk(obj, parent: str, path: str, depth: int = 0): + if depth > 8: + return + if isinstance(obj, dict): + # Recurse into properties (skip top-level by requiring depth > 0 for the object itself) + props = obj.get("properties", {}) + if isinstance(props, dict): + for pname, pval in props.items(): + if isinstance(pval, dict) and "$ref" not in pval: + # Collect this inline property + if "properties" in pval or "enum" in pval or "items" in pval: + results.append((parent, f"{path}.properties.{pname}", pval)) + _walk(pval, parent, f"{path}.properties.{pname}", depth + 1) + # Recurse into items + items = obj.get("items", {}) + if isinstance(items, dict) and "$ref" not in items: + if "properties" in items or "enum" in items: + results.append((parent, f"{path}.items", items)) + _walk(items, parent, f"{path}.items", depth + 1) + # Recurse into additionalProperties + ap = obj.get("additionalProperties", {}) + if isinstance(ap, dict) and "$ref" not in ap: + if "properties" in ap or "enum" in ap: + results.append((parent, f"{path}.additionalProperties", ap)) + _walk(ap, parent, f"{path}.additionalProperties", depth + 1) + # Recurse into anyOf / oneOf / allOf + for comb in ("anyOf", "oneOf", "allOf"): + for i, entry in enumerate(obj.get(comb, [])): + if isinstance(entry, dict) and "$ref" not in entry: + if "properties" in entry or "enum" in entry: + results.append((parent, f"{path}.{comb}[{i}]", entry)) + _walk(entry, parent, f"{path}.{comb}[{i}]", depth + 1) + elif isinstance(obj, list): + for i, item in enumerate(obj): + if isinstance(item, dict) and "$ref" not in item: + if "properties" in item or "enum" in item: + results.append((parent, f"{path}[{i}]", item)) + _walk(item, parent, f"{path}[{i}]", depth + 1) + + for schema_name, schema_def in ref_schemas.items(): + if isinstance(schema_def, dict): + _walk(schema_def, schema_name, schema_name) + + return results + + +def find_inlined_candidates( + ref_schemas: dict, gen_schemas: dict, extra_gen: set[str] +) -> list[tuple[str, str, float]]: + """For each extra gen schema, find a matching inline property in the pre-draft. + + Returns [(gen_name, parent.property_path, score), ...] sorted by descending score. + """ + inline_props = _collect_inline_properties(ref_schemas) + gen_sigs = { + n: _schema_signature(s) for n, s in gen_schemas.items() if n in extra_gen + } + if not gen_sigs: + return [] + + candidates: list[tuple[str, str, float]] = [] + for gen_name, gs in gen_sigs.items(): + for parent_name, prop_path, inline_def in inline_props: + rs = _schema_signature(inline_def) + if rs["type"] and rs["type"] != gs["type"]: + continue + if rs["enum"] and rs["enum"] != gs["enum"]: + continue + prop_union = rs["properties"] | gs["properties"] + if not prop_union: + continue + prop_score = len(rs["properties"] & gs["properties"]) / len(prop_union) + req_score = ( + 1.0 + if not rs["required"] and not gs["required"] + else len(rs["required"] & gs["required"]) + / len(rs["required"] | gs["required"]) + ) + score = round(0.7 * prop_score + 0.3 * req_score, 3) + candidates.append((gen_name, prop_path, score)) + + candidates.sort(key=lambda x: -x[2]) + return candidates + + +def _resolve_schema_name( + raw: str, gen_schemas: dict, xlinkml_renames: dict +) -> str | None: + """Resolve an OpenAPI or LinkML name to the generated schema name.""" + if raw in gen_schemas: + return raw + # Check if it's a LinkML name → find the OpenAPI name via renames + for openapi_name, linkml_name in xlinkml_renames.items(): + if linkml_name == raw or linkml_name.split(".")[0] == raw: + return openapi_name + # Check dotted path: Class.slot → synthetic key + for openapi_name, linkml_name in xlinkml_renames.items(): + if linkml_name == raw: + return openapi_name + return None + + +def _collect_refs(obj, results: set[str], prefix: str = "#/components/schemas/"): + """Recursively collect all $ref target names from a schema definition.""" + if isinstance(obj, dict): + if ( + "$ref" in obj + and isinstance(obj["$ref"], str) + and obj["$ref"].startswith(prefix) + ): + results.add(obj["$ref"].replace(prefix, "")) + for v in obj.values(): + _collect_refs(v, results, prefix) + elif isinstance(obj, list): + for item in obj: + _collect_refs(item, results, prefix) + + +def _find_referers( + schema_name: str, gen_schemas: dict, gen_paths: dict +) -> tuple[list[str], list[str]]: + """Find all paths and schemas that reference the given schema.""" + path_refs: list[str] = [] + schema_refs: list[str] = [] + target = f"#/components/schemas/{schema_name}" + + # Check paths + for path, methods in gen_paths.items(): + for method, spec in methods.items(): + + def _check(obj): + if isinstance(obj, dict): + if "$ref" in obj and obj["$ref"] == target: + path_refs.append(f"{method.upper()} {path}") + return + for v in obj.values(): + _check(v) + elif isinstance(obj, list): + for item in obj: + _check(item) + + _check(spec) + + # Check schemas + for name, schema in gen_schemas.items(): + if name == schema_name: + continue + found: set[str] = set() + _collect_refs(schema, found) + if schema_name in found: + schema_refs.append(name) + + # Deduplicate and sort + return sorted(set(path_refs)), sorted(set(schema_refs)) + + +def _show_schema_detail(raw_name: str, show_yaml: bool = False) -> None: + """Show detailed information about a single schema.""" + ref = get_pre_draft_spec() + gen = get_generated_spec() + xlinkml_renames = get_xlinkml_renames() + gen_schemas = gen.get("components", {}).get("schemas", {}) + ref_schemas = ref.get("components", {}).get("schemas", {}) + gen_paths = gen.get("paths", {}) + ref_comp = ref.get("components", {}) + gen_comp = gen.get("components", {}) + + ref_keys = set(ref_schemas.keys()) + gen_keys = set(gen_schemas.keys()) + only_ref = ref_keys - gen_keys + only_gen = gen_keys - ref_keys + common = ref_keys & gen_keys + + schema_name = _resolve_schema_name(raw_name, gen_schemas, xlinkml_renames) + if schema_name is None: + print(f"{red('Schema not found:')} {raw_name}") + print(f" {dim('Try an OpenAPI schema name or a LinkML class/type name.')}") + print(f" {dim('OpenAPI names:')} {', '.join(sorted(gen_schemas.keys()))}") + return + + linkml_src = None + for on, ln in xlinkml_renames.items(): + if on == schema_name: + linkml_src = ln + break + + print(f"\n{bold(schema_name)}") + if linkml_src: + print(f" LinkML source: {dim(linkml_src)}") + + if schema_name in gen_schemas: + gen_s = gen_schemas[schema_name] + refs_from: set[str] = set() + _collect_refs(gen_s, refs_from) + path_refs, schema_refs = _find_referers(schema_name, gen_schemas, gen_paths) + + # Status + if schema_name in common: + s_errors = deep_diff("", ref_schemas[schema_name], gen_s, schema_name) + cats = _categorize_errors(s_errors) + status = ( + f"{red(str(len(s_errors)))} diff(s)" if s_errors else green("identical") + ) + print(f" Status: {status}") + if cats["desc"]: + print(f" {dim('Description-only')} ({len(cats['desc'])}):") + for e in cats["desc"]: + print(f" - {dim(e)}") + if cats["aprop"]: + print(f" {yellow('additionalProperties')} ({len(cats['aprop'])}):") + for e in cats["aprop"]: + print(f" - {yellow(e)}") + if cats["struct"]: + print(f" {red('Structural')} ({len(cats['struct'])}):") + for e in cats["struct"]: + print(f" - {red(e)}") + elif schema_name in only_gen: + print(f" Status: {yellow('EXTRA — not in pre-draft')}") + elif schema_name in only_ref: + print(f" Status: {red('MISSING — not in generated output')}") + else: + print(f" Status: {green('not in comparison')}") + + # Referers (paths) + if path_refs: + print(f" {dim('Referenced by endpoints')} ({len(path_refs)}):") + for pr in path_refs: + print(f" {green(pr)}") + else: + print(f" {dim('Referenced by endpoints:')} {dim('none')}") + + # Referers (schemas) + if schema_refs: + print(f" {dim('Referenced by schemas')} ({len(schema_refs)}):") + for sr in schema_refs: + print(f" {sr}") + else: + print(f" {dim('Referenced by schemas:')} {dim('none')}") + + # References from this schema + if refs_from: + print(f" {dim('References to other schemas')} ({len(refs_from)}):") + for rf in sorted(refs_from): + print(f" {rf}") + else: + print(f" {dim('References to other schemas:')} {dim('none')}") + + # Inline / extra context + if schema_name in only_gen: + candidates = find_inlined_candidates( + ref_schemas, {schema_name: gen_s}, {schema_name} + ) + if candidates: + best = max(candidates, key=lambda x: x[2]) + print( + f" {yellow('Inferred inlined')} in pre-draft at {best[1]} (score {best[2]})" + ) + elif schema_name in common: + pre_s = ref_schemas.get(schema_name, {}) + props = pre_s.get("properties", {}) + if isinstance(props, dict): + inlined_props = [ + k + for k, v in props.items() + if isinstance(v, dict) and "$ref" not in v and "properties" in v + ] + if inlined_props: + print( + f" {dim('Pre-draft inlined properties:')} {', '.join(inlined_props)}" + ) + + # Property-level comparison + if schema_name in common: + pre_s = ref_schemas[schema_name] + gen_props = gen_s.get("properties", {}) + pre_props = pre_s.get("properties", {}) + if isinstance(gen_props, dict) and isinstance(pre_props, dict): + gen_keys_set = set(gen_props.keys()) + pre_keys_set = set(pre_props.keys()) + only_pre = pre_keys_set - gen_keys_set + only_gen_p = gen_keys_set - pre_keys_set + if only_pre: + print( + f" {red('Properties only in pre-draft:')} {sorted(only_pre)}" + ) + if only_gen_p: + print( + f" {yellow('Properties only in generated:')} {sorted(only_gen_p)}" + ) + + # YAML dump + if show_yaml: + print(f"\n{dim('─' * 40)}") + pre_s = ( + ref_schemas.get(schema_name, ref_schemas.get(raw_name)) + if schema_name in common + else None + ) + gen_s = gen_schemas.get(schema_name) + if pre_s: + print(f"\n{bold('Pre-draft')} ({schema_name}):") + print( + yaml.dump( + pre_s, + default_flow_style=False, + sort_keys=False, + allow_unicode=True, + ).rstrip() + ) + if gen_s: + print(f"\n{bold('Generated')} ({schema_name}):") + print( + yaml.dump( + gen_s, + default_flow_style=False, + sort_keys=False, + allow_unicode=True, + ).rstrip() + ) + + else: + if schema_name in only_ref: + print(f"\n {bold(schema_name)}") + print(f" Status: {red('MISSING — in pre-draft but not generated')}") + pre_s = ref_schemas.get(schema_name, {}) + if pre_s: + print(f" {dim('Pre-draft definition:')}") + print(f" type: {pre_s.get('type', 'N/A')}") + props = pre_s.get("properties", {}) + if isinstance(props, dict): + for pk, pv in props.items(): + ref_type = ( + pv.get("type", "object") if isinstance(pv, dict) else "N/A" + ) + print(f" {pk}: {ref_type}") + + +def main(): + parser = argparse.ArgumentParser( + description="Compare pre-draft vs generated OpenAPI spec" + ) + parser.add_argument( + "--schema", + "-s", + help="Show verbose details for a single schema only (name in generated output)", + ) + parser.add_argument( + "--yaml", + "-y", + action="store_true", + help="Show YAML of both pre-draft and generated schemas (use with -s)", + ) + args = parser.parse_args() + + # Single-schema mode: skip everything and show only the schema detail + if args.schema: + _show_schema_detail(args.schema, args.yaml) + return + + print(cyan("=" * 60)) + print(cyan("OpenAPI Spec Validation: pre-draft vs generated")) + print(cyan("=" * 60)) + + ref = get_pre_draft_spec() + gen = get_generated_spec() + + # 1. Info + print(f"\n{cyan('--- Top-level ---')}") + for key in ["openapi", "info"]: + compare_section(key, ref, gen, key, []) + + # 2. Servers + compare_section("servers", ref, gen, "servers", []) + + # 3. Security + compare_section("security", ref, gen, "security", []) + + # 4. Paths + print(f"\n{cyan('--- Paths ---')}") + ref_paths = ref.get("paths", {}) + gen_paths = gen.get("paths", {}) + ref_pkeys = set(ref_paths.keys()) + gen_pkeys = set(gen_paths.keys()) + only_ref_p = ref_pkeys - gen_pkeys + only_gen_p = gen_pkeys - ref_pkeys + common_p = ref_pkeys & gen_pkeys + + if only_ref_p: + print( + f" {red('MISSING IN GENERATED')} ({len(only_ref_p)}): {sorted(only_ref_p)}" + ) + if only_gen_p: + print( + f" {yellow('EXTRA IN GENERATED')} ({len(only_gen_p)}): {sorted(only_gen_p)}" + ) + + for path in sorted(common_p): + ref_methods = ref_paths[path] + gen_methods = gen_paths[path] + ref_mkeys = set(ref_methods.keys()) + gen_mkeys = set(gen_methods.keys()) + only_ref_m = ref_mkeys - gen_mkeys + only_gen_m = gen_mkeys - ref_mkeys + + if only_ref_m or only_gen_m: + print( + f" {yellow(path)}: METHOD DIFF — ref={sorted(ref_mkeys)} gen={sorted(gen_mkeys)}" + ) + else: + all_method_ok = True + for method in sorted(ref_mkeys): + errs = deep_diff( + path, ref_methods[method], gen_methods[method], f"{path}.{method}" + ) + if errs: + all_method_ok = False + print(f" {path}.{method}: {red(str(len(errs)))} diff(s)") + for e in errs: + print(f" - {dim(e)}") + if all_method_ok: + print(f" {path}: {green('✓ IDENTICAL')}") + + # 5. Components (excluding schemas — handled separately) + print(f"\n{cyan('--- Components (non-schemas) ---')}") + ref_comp = ref.get("components", {}) + gen_comp = gen.get("components", {}) + comp_keys = set(ref_comp.keys()) | set(gen_comp.keys()) + for ck in sorted(comp_keys): + if ck == "schemas": + continue + compare_section(f"components.{ck}", ref_comp, gen_comp, ck, []) + + # 6. Schemas (detailed) + print(f"\n{cyan('--- components/schemas ---')}") + ref_schemas = ref_comp.get("schemas", {}) + gen_schemas = gen_comp.get("schemas", {}) + not_needed = get_not_needed_schemas() + inlined = get_inlined_schemas() + xlinkml_renames = get_xlinkml_renames() + rename_candidates = find_rename_candidates(ref_schemas, gen_schemas) + + ref_keys = set(ref_schemas.keys()) + gen_keys = set(gen_schemas.keys()) + only_ref = ref_keys - gen_keys + only_gen = gen_keys - ref_keys + common = ref_keys & gen_keys + intentionally_omitted = (only_ref & not_needed) | (only_ref & inlined) + truly_missing = only_ref - not_needed - inlined + + inlined_candidates = find_inlined_candidates(ref_schemas, gen_schemas, only_gen) + # Deduplicate: pick the best match per gen schema + best_per_schema: dict[str, tuple[str, float]] = {} + for gen_name, prop_path, score in inlined_candidates: + prev = best_per_schema.get(gen_name) + if prev is None or score > prev[1]: + best_per_schema[gen_name] = (prop_path, score) + high_scoring_inlined = [] + explained_extra: set[str] = set() + for gen_name, (prop_path, score) in sorted( + best_per_schema.items(), key=lambda x: -x[1][1] + ): + if score >= 0.9: + high_scoring_inlined.append((gen_name, prop_path, score)) + explained_extra.add(gen_name) + unexplained_extra = only_gen - explained_extra + + print( + f"\n Schemas ({bold(str(len(ref_keys)))} ref, {bold(str(len(gen_keys)))} gen, {bold(str(len(not_needed)))} NOT-NEEDED, {bold(str(len(inlined)))} INLINED):" + ) + + if xlinkml_renames: + print(f" {dim('x-linkml-source RENAMES')} ({len(xlinkml_renames)}):") + for openapi_name, linkml_name in sorted(xlinkml_renames.items()): + print(f" {openapi_name} ← {linkml_name}") + + if intentionally_omitted: + not_needed_only = intentionally_omitted & not_needed + inlined_only = intentionally_omitted & inlined + if not_needed_only: + print( + f" {dim('NOT-NEEDED')} ({len(not_needed_only)}): {sorted(not_needed_only)}" + ) + if inlined_only: + print( + f" {dim('INLINED SUBSCHEMAS')} ({len(inlined_only)}): {sorted(inlined_only)}" + ) + + if truly_missing: + print( + f" {red('MISSING IN GENERATED')} ({len(truly_missing)}): {sorted(truly_missing)}" + ) + + if high_scoring_inlined: + print(f" {yellow('INFERRED INLINED')} ({len(high_scoring_inlined)}):") + for gen_name, prop_path, score in sorted( + high_scoring_inlined, key=lambda x: -x[2] + ): + print(f" {gen_name} inlined into {prop_path} (score {score})") + + if unexplained_extra: + print( + f" {yellow('EXTRA IN GENERATED')} ({len(unexplained_extra)}): {sorted(unexplained_extra)}" + ) + + if rename_candidates: + print( + f" {dim('RENAME CANDIDATES')} (missing → extra, by structural similarity):" + ) + shown = 0 + for ref_name, gen_name, score in rename_candidates: + if ref_name in truly_missing and gen_name in unexplained_extra: + print(f" {ref_name} ↔ {gen_name} (score {score})") + shown += 1 + if shown >= 5: + print(f" ... ({len(rename_candidates) - 5} more)") + break + + elif common: + # ── Full summary with categorization ──────────────────── + all_aprop_schemas: set[str] = set() + changed_names: list[str] = [] + for name in sorted(common): + ref_s = ref_schemas[name] + gen_s = gen_schemas[name] + s_errors = deep_diff("", ref_s, gen_s, name) + if not s_errors: + continue + changed_names.append(name) + cats = _categorize_errors(s_errors) + if cats["aprop"]: + all_aprop_schemas.add(name) + # Only show this schema in detail if it has structural changes + if cats["struct"] or ( + cats["desc"] and not cats["aprop"] and not cats["struct"] + ): + non_desc = len(s_errors) - len(cats["desc"]) + if non_desc: + print(f" {name}: {red(str(non_desc))} non-description diff(s)") + else: + print( + f" {name}: {dim('description-only')} ({len(cats['desc'])} diff(s))" + ) + + # Grouped additionalProperties report + if all_aprop_schemas: + print( + f" {yellow('additionalProperties:')} present in {len(all_aprop_schemas)} schema(s)" + ) + print( + f" {dim('added by LinkML JSON Schema generator (additionalProperties: false)')}" + ) + print(f" {dim('affected:')} {', '.join(sorted(all_aprop_schemas))}") + + unchanged = len(common) - len(changed_names) + print( + f" {dim('Common schemas:')} {green(str(unchanged))} identical, {yellow(str(len(changed_names)))} changed" + ) + else: + print(f" {green('No common schemas')}") + + print(f"\n{cyan('=' * 60)}") + total_errors = len(deep_diff("", ref, gen, "")) + if total_errors == 0: + print(f"{green('RESULT: FULLY IDENTICAL ✓')}") + else: + print(f"{red(f'RESULT: {total_errors} difference(s) found')}") + print(cyan("=" * 60)) + + +if __name__ == "__main__": + main() diff --git a/mkdocs.yml b/mkdocs.yml index 8a2af410..61c9da75 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Specification (pre-draft) -docs_dir: system-design +docs_dir: build/site nav: - What is Margo?: index.md @@ -8,9 +8,11 @@ nav: - specification/margo-management-interface/api-requirements-and-security.md - specification/margo-management-interface/certificate-api.md - specification/margo-management-interface/device-client-onboarding.md - - specification/margo-management-interface/device-capabilities.md + - specification/margo-management-interface/device-capabilities-api.md + - Device Capabilities: specification/margo-management-interface/device-capabilities.md - specification/margo-management-interface/desired-state.md - - specification/margo-management-interface/deployment-status.md + - specification/margo-management-interface/deployment-status-api.md + - Deployment Status: specification/margo-management-interface/deployment-status.md - specification/margo-management-interface/management-interface-swagger.md - Applications: - specification/applications/application-description.md @@ -21,6 +23,15 @@ nav: - specification/observability/publishing-workload-observability-data.md - specification/observability/collecting-workload-observability-data.md - specification/observability/consuming-workload-observability-data.md + - Data Model: + - Overview: data-model/index.md + - ApplicationDescription: data-model/ApplicationDescription.md + - ApplicationDeployment: data-model/ApplicationDeployment.md + - DeviceCapabilitiesManifest: data-model/DeviceCapabilitiesManifest.md + - DesiredStateManifest: data-model/DesiredStateManifest.md + - DeploymentStatusManifest: data-model/DeploymentStatusManifest.md + - Make a Contribution: + - make-a-contribution/contributing.md theme: name: material @@ -49,6 +60,8 @@ theme: markdown_extensions: - toc: toc_depth: 2 + - admonition + - pymdownx.details - pymdownx.superfences: custom_fences: - name: mermaid @@ -62,6 +75,8 @@ extra_css: extra_javascript: - assets/swagger-ui-bundle.js - assets/swagger-ui-standalone-preset.js + - assets/svg-pan-zoom.min.js + - assets/svg-pan-zoom-init.js extra: diff --git a/model/application-deployment.linkml.yaml b/model/application-deployment.linkml.yaml new file mode 100644 index 00000000..bd6fbf03 --- /dev/null +++ b/model/application-deployment.linkml.yaml @@ -0,0 +1,110 @@ +# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json +id: https://specification.margo.org/application_deployment_schema +name: ApplicationDeployment +description: >- + Each workload is represented as an `ApplicationDeployment` YAML file that specifies its components, configuration, and parameters. + This resource is delivered via the Desired State API and referenced by `id` in the Deployment Status API. +version: 1.0.0 #Arne: update later +prefixes: + linkml: https://w3id.org/linkml/ + margo: https://specification.margo.org/ +imports: + - linkml:types + - margo-deployments.linkml + +default_prefix: margo +default_range: string + +classes: + ApplicationDeployment: + description: >- + A class representing the desired state of an entity. + attributes: + apiVersion: + description: Identifier of the version of the API the object definition follows. + required: true + range: string + kind: + description: Must be `ApplicationDeployment`. + equals_string: "ApplicationDeployment" + required: true + range: string + designates_type: true + metadata: + description: >- + Metadata element specifying characteristics about the application deployment. + See the [Metadata Attributes](#metadata-attributes) section below. + range: DeploymentMetadata + required: true + spec: + description: >- + Spec element that defines deployment profile and parameters associated with the application deployment. + See the [Spec Attributes](#spec-attributes) section below. + range: Spec + required: true + + DeploymentMetadata: + description: >- + Metadata associated with the desired state. + attributes: + annotations: + description: >- + Defines the application ID and unique identifier associated to the deployment specification. + Needs to be assigned by the Workload Orchestration Software. + See the [Annotation Attributes](#annotations-attributes) section below. + range: DeploymentAnnotations + required: true + name: + description: >- + When deploying to Kubernetes, the manifests name. + The name is chosen by the workload orchestration vendor and is not displayed anywhere. + required: true + range: string + namespace: + description: When deploying to Kubernetes, the namespace the manifest is added under. The namespace is chosen by the workload orchestration solution vendor. + required: true + range: string + deviceId: + range: HierarchicalDeviceId + required: true + + DeploymentAnnotations: + description: >- + A class representing annotations. + attributes: + applicationId: + description: >- + An identifier for the application. + The id is used to help create unique identifiers where required, such as namespaces. + The id must be lower case letters and numbers and MAY contain dashes. + Uppercase letters, underscores and periods MUST NOT be used. + The id MUST NOT be more than 200 characters. + The applicationId MUST match the associated application package Metadata "id" attribute. + range: string + required: true + pattern: "^[-a-z0-9]{1,200}$" + id: + description: >- + The unique identifier UUID of the deployment specification. + Needs to be assigned by the Workload Orchestration Software. + range: string + required: true + pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + + Spec: + description: >- + Specification details of the desired state. + attributes: + deploymentProfile: + description: >- + Section that defines deployment details including type and components. + range: DeploymentProfile + required: true + parameters: + description: >- + Describes the configured parameters applied via the end-user. + range: Parameter + required: true + multivalued: true + inlined: true + inlined_as_list: false diff --git a/model/application-description.linkml.yaml b/model/application-description.linkml.yaml new file mode 100644 index 00000000..7c783277 --- /dev/null +++ b/model/application-description.linkml.yaml @@ -0,0 +1,382 @@ +# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json +id: https://specification.margo.org/application-schema +name: ApplicationDescription +title: Application Description +description: >- + The purpose of the Application Description is to enable an application's discovery, configuration, and deployment on edge devices. + To deploy an application the end user specifies values for the [parameters](#defining-configurable-application-parameters) given in + an Application Description (e.g., through a UI of the WFM) to instantiate an `ApplicationDeployment`, + which defines the [desired state](../margo-management-interface/desired-state.md) for an application. + + The structure of the ApplicationDescription object to be provided in the YAML document + specified in this page. + Some examples are provided also [at the bottom of this page](#examples). +version: 1.0.0 +prefixes: + linkml: https://w3id.org/linkml/ + margo: https://specification.margo.org/ +imports: + - linkml:types + - margo-resources.linkml + - margo-deployments.linkml + +default_prefix: margo +# default_range: string # https://github.com/linkml/linkml/issues/1483 + +# Class Definitions +classes: + ApplicationDescription: + description: Root class for an application description. + attributes: + apiVersion: + description: Identifier of the version of the API the object definition follows. + required: true + range: string + kind: + description: Specifies the object type; must be `ApplicationDescription`. + range: string + required: true + equals_string: "ApplicationDescription" + designates_type: true + metadata: + description: >- + Metadata element specifying characteristics about the application deployment. + See the [Metadata Attributes](#metadata-attributes) section below. + range: ApplicationMetadata + required: true + deploymentProfiles: + description: >- + Deployment profiles element specifying the types of deployments the application supports. + See the [Deployment](#deploymentprofile-attributes) section below. + range: DeploymentProfileDescription + multivalued: true + inlined: true + inlined_as_list: true + required: true + parameters: + description: >- + Parameters element specifying the configurable parameters to use when installing, or updating, the application. + See the [Parameter](#parameter-attributes) section below. + range: Parameter + multivalued: true + inlined: true + inlined_as_list: false + configuration: + description: >- + Configuration element specifying how parameters should be displayed to the user for setting the value + as well as the rules to use to validate the user's input. + See the [Configuration](#configuration-attributes) section below. + range: Configuration + + ApplicationMetadata: + description: Metadata about the application. + attributes: + id: + description: >- + An identifier for the application. The id is used to help create unique identifiers where required, + such as namespaces. The id must be lower case letters and numbers and MAY contain dashes. + Uppercase letters, underscores and periods MUST NOT be used. The id MUST NOT be more than 200 characters. + range: string + required: true + pattern: ^[a-z0-9-]{1,200}$ + name: + description: >- + The application's official name. + This name is for display purposes only and can container whitespace and special characters. + range: string + required: true + description: + range: string + version: + description: The application's version. + range: string + required: true + catalog: + description: >- + Catalog element specifying the application's metadata for enabling its discovery. + See the [Catalog](#catalog-attributes) section below. + range: Catalog + required: true + + Catalog: + description: Catalog metadata for displaying the application. + attributes: + application: + description: >- + Application element specifying the application specific metadata. + See the [Application Metadata](#applicationmetadata-attributes) section below. + range: CatalogApplicationMetadata + author: + description: >- + Author element specifying metadata about the application's author. + See the [Author Metadata](#author-attributes) section below. + range: Author + multivalued: true + inlined: true + inlined_as_list: true + organization: + description: >- + Organization element specifying metadata about the organization/company providing the application. + See the [Organization Metadata](#organization-attributes) section below. + range: Organization + multivalued: true + required: true + inlined: true + inlined_as_list: true + + CatalogApplicationMetadata: + description: Metadata specific to the application. + attributes: + descriptionFile: + description: Link to the file containing the application's full description. The file should be a markdown file. + range: string + icon: + description: Link to the icon file (e.g., in PNG format). + range: string + licenseFile: + description: Link to the file that details the application's license. The file should either be a plain text, markdown or PDF file. + range: string + releaseNotes: + description: Statement about the changes for this application's release. The file should either be a markdown or PDF file. + range: string + site: + description: Link to the application's website. + range: string + tagline: + description: The application's slogan. + range: string + tags: + description: An array of strings that can be used to provide additional context for the application in a user interface to assist with task such as categorizing, searching, etc. + range: string + multivalued: true + inlined: true + inlined_as_list: true + + Author: + description: Information about the application's author. + attributes: + name: + description: The name of the application's creator. + range: string + email: + description: Email address of the application's creator. + range: string + pattern: .*@[a-z0-9.-]* + + Organization: + description: Information about the providing organization. + attributes: + name: + description: Organization responsible for the application's development and distribution. + range: string + required: true + site: + description: Link to the organization's website. + range: string + + DeploymentProfileDescription: + description: Represents a deployment configuration for the application. + is_a: DeploymentProfile + attributes: + id: + description: >- + An identifier for the deployment profile, given by the application developer, used to uniquely identify this deployment profile from others within this application description's scope. + range: string + required: true + description: + description: >- + This human-readable description of a deployment profile allows for providing additional context about the deployment profile. E.g., the application developer can use this to describe the deployment profile's purpose, + such as the intended use case. Additionally, the application developer can use this to provide further details about the resources, peripherals, and interfaces required to run the application. + range: string + requiredResources: + description: >- + Required resources element specifying the resources required to install the application. + See the [Required Resources](#requiredresources-attributes) section below. + The consequences (e.g., aborting / blocking the installation or execution of the application) of not meeting these required resources are not defined (yet) by margo. + range: Resources + + HelmDeploymentProfileDescription: + is_a: DeploymentProfileDescription + slot_usage: + type: + equals_string: "helm.v3" + components: + range: HelmComponent + + ComposeDeploymentProfileDescription: + is_a: DeploymentProfileDescription + slot_usage: + type: + equals_string: "compose" + components: + range: ComposeComponent + + Configuration: + description: Configuration layout and validation rules. + attributes: + sections: + description: >- + Sections are used to group related parameters together, + so it is possible to present a user interface with a logical grouping of the parameters in each section. + See the [Section](#section-attributes) section below. + range: Section + multivalued: true + inlined: true + inlined_as_list: true + required: true + schema: + description: >- + Schema is used to provide details about how to validate each parameter value. + At a minimum, the parameter value must be validated to match the schema's data type. + The schema indicates additional rules the provided value must satisfy to be considered valid input. + See the [Schema](#schema-attributes) section below. + range: Schema + multivalued: true + inlined: true + inlined_as_list: true + required: true + + Section: + description: Named sections within the configuration layout. + attributes: + name: + description: >- + The name of the section. This may be used in the user interface to show the grouping of the associated parameters within the section. + range: string + required: true + settings: + description: >- + Settings are used to provide instructions to the workload orchestration software vendor for displaying parameters to the user. + A user MUST be able to provide values for all settings. + See the [Setting](#setting-attributes) section below. + range: Setting + multivalued: true + inlined: true + inlined_as_list: true + required: true + + Setting: + description: Individual configuration settings. + attributes: + parameter: + description: The name of the [parameter](#parameter-attributes) the setting is associated with. + range: string + required: true + name: + description: The parameter's display name to show in the user interface. + range: string + required: true + description: + description: The parameters's short description to provide additional context to the user in the user interface about what the parameter is for. + range: string + immutable: + description: If true, indicates the parameter value MUST not be changed once it has been set and used to install the application. Default is false if not provided. + range: boolean + schema: + description: The name of the schema definition to use to validate the parameter's value. See the [Schema](#schema-attributes) section below. + range: Schema + inlined: false + required: true + + Schema: + description: Defines data type and rules for validating user provided parameter values. Subclasses (see below) define for each data type their own set of validation rules that can be used. The value MUST be validated against all rules defined in the schema. + attributes: + name: + description: The name of the schema rule. This used in the [setting](#setting-attributes) to link the setting to the schema rule. + range: string + required: true + identifier: true + dataType: + description: >- + Indicates the expected data type for the user provided value. + Accepted values are string, integer, double, boolean, array[string], array[integer], array[double], array[boolean]. + At a minimum, the provided parameter value MUST match the schema's data type if no other validation rules are provided. + range: string + required: true + #validationRule: + # description: >- + # Defines the validation rules to use to validate the user provided parameter value. + # The rules are based on the schema's data type and are listed below. + # The value MUST be validated against any validation rules defined in the schema. + # range: ValidationRule + # required: false + + TextValidationSchema: + is_a: Schema + description: Extends schema to define a string/text-specific set of validation rules that can be used. + attributes: + allowEmpty: + description: >- + If true, indicates a value must be provided. Default is false if not provided. + range: boolean + minLength: + description: If set, indicates the minimum number of characters the value must have to be considered valid. + range: integer + maxLength: + description: If set, indicates the maximum number of characters the value must have to be considered valid. + range: integer + regexMatch: + description: If set, indicates a regular expression to use to validate the value. + range: string + + BooleanValidationSchema: + is_a: Schema + description: Extends schema to define a boolean-specific set of validation rules that can be used. + attributes: + allowEmpty: + description: >- + If true, indicates a value must be provided. Default is false if not provided. + range: boolean + + NumericIntegerValidationSchema: + is_a: Schema + description: Extends schema to define a integer-specific set of validation rules that can be used. + attributes: + allowEmpty: + description: >- + If true, indicates a value must be provided. Default is false if not provided. + range: boolean + minValue: + description: If set, indicates the minimum allowed integer value the value must have to be considered valid. + range: integer + maxValue: + description: If set, indicates the maximum allowed integer value the value must have to be considered valid. + range: integer + + NumericDoubleValidationSchema: + is_a: Schema + description: Extends schema to define a double-specific set of validation rules that can be used. + attributes: + allowEmpty: + description: >- + If true, indicates a value must be provided. Default is false if not provided. + range: boolean + minValue: + description: If set, indicates the minimum value to be considered valid. + range: float + maxValue: + description: If set, indicates the maximum value to be considered valid. + range: float + minPrecision: + description: If set, indicates the minimum level of precision the value must have to be considered valid. + range: integer + maxPrecision: + description: If set, indicates the maximum level of precision the value must have to be considered valid. + range: integer + + SelectValidationSchema: + is_a: Schema + description: Extends schema to define a specific set of validation rules that can be used for select options. + attributes: + allowEmpty: + description: >- + If true, indicates a value must be provided. Default is false if not provided. + range: boolean + multiselect: + description: If true, indicates multiple values can be selected. If multiple values can be selected the resulting value is an array of the selected values. The default is false if not provided. + range: boolean + options: + description: This provides the list of acceptable options the user can select from. The data type for each option must match the parameter setting’s data type. + range: string + multivalued: true + required: true diff --git a/model/deployment-status.linkml.yaml b/model/deployment-status.linkml.yaml new file mode 100644 index 00000000..899fa202 --- /dev/null +++ b/model/deployment-status.linkml.yaml @@ -0,0 +1,104 @@ +# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json +id: https://specification.margo.org/deployment-status +name: DeploymentStatus +description: >- + Schema for reporting the deployment status of workloads from a device to the WFM. +version: 1.0.0 +prefixes: + linkml: https://w3id.org/linkml/ + margo: https://specification.margo.org/ +imports: + - linkml:types + +default_prefix: margo +default_range: string + +classes: + DeploymentStatusManifest: + description: >- + Manifest sent by the device client to report the deployment status of a workload. + attributes: + apiVersion: + range: string + required: true + kind: + range: string + required: true + equals_string: "DeploymentStatusManifest" + designates_type: true + deploymentId: + description: >- + The unique identifier of the deployment whose status is being reported. + range: string + required: true + deviceId: + description: >- + Id of the device hosting the deployment. Includes the full device hierarchy if applicable. + This attribute is required when reporting on behalf of a child-device. + range: HierarchicalDeviceId + status: + description: >- + Overall status of the deployment. + range: Status + required: true + components: + description: >- + Per-component status list. + range: ComponentStatus + required: true + multivalued: true + + Status: + description: >- + Overall deployment state and optional error details. + slots: + - state + - error + + Error: + description: >- + Error details associated with a failed deployment state. + attributes: + code: + range: string + source: + description: >- + Identifies the source of the error. It is set to the device id, with its full hierarchy + if applicable, of the device generating the error, or to the component name of the + component generating the error. + range: string + message: + range: string + + ComponentStatus: + is_a: Status + description: >- + Status of a component deployment. + attributes: + name: + range: string + required: true + +slots: + state: + description: >- + Current deployment state. + range: State + required: true + + error: + description: >- + Optional error details when the state is `failed`. + range: Error + +enums: + State: + description: >- + Permissible deployment states. + permissible_values: + pending: + installing: + installed: + failed: + removing: + removed: diff --git a/model/desired-state-manifest.linkml.yaml b/model/desired-state-manifest.linkml.yaml new file mode 100644 index 00000000..e441f63f --- /dev/null +++ b/model/desired-state-manifest.linkml.yaml @@ -0,0 +1,127 @@ +# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json +id: https://specification.margo.org/desired-state-manifest-schema +name: DesiredStateManifest +description: >- + Each workload is represented as an `ApplicationDeployment` YAML file that specifies its components, configuration, and parameters. +version: 1.0.0 +prefixes: + linkml: https://w3id.org/linkml/ + margo: https://specification.margo.org/ +imports: + - linkml:types + +default_prefix: margo +default_range: string + +classes: + DesiredStateManifest: + description: >- + Manifest from the Workload Fleet Manager, representing the complete desired workload configuration assigned to the device. + attributes: + manifestVersion: + description: >- + Monotonically increasing unsigned 64-bit integer in the inclusive range [1, 2^64-1]. Prevents rollback attacks. + range: ManifestVersion + required: true + bundle: + description: >- + Package optimization containing multiple ApplicationDeployment YAMLs. + range: Bundle + required: true + # The key must always be present, but its value MUST be null when there + # are zero deployments. UNCOMMITTED decouples "key required" from + # "value non-null", yielding a nullable sub-schema. + value_presence: UNCOMMITTED + deployments: + description: >- + List of deployment objects describing each workload. + multivalued: true + inlined: true + inlined_as_list: true + range: Deployment + required: true + + Bundle: + description: >- + Describes an archive containing all referenced ApplicationDeployment YAMLs. + If there are zero deployments (i.e., the deployments array is empty), this field MUST be present with the value null. + An empty archive MUST NOT be served. + attributes: + mediaType: + description: >- + MUST be application/vnd.margo.bundle.v1+tar+gzip, + which denotes a gzip-compressed tar archive (commonly delivered as a .tar.gz) whose root contains one or more ApplicationDeployment YAML files. + Servers MUST set the HTTP Content-Type to this media type. + The archive MUST contain exactly the set of YAML files referenced by deployments. + range: string + equals_string: "application/vnd.margo.bundle.v1+tar+gzip" + required: true + digest: + description: >- + Digest of the bundle archive. + MUST equal the digest computed over the exact sequence of bytes in the bundle endpoint's HTTP 200 OK response body. + See Protocol - Digest for further details. + range: DigestType + required: true + sizeBytes: + description: >- + Optional unsigned 64-bit advisory estimate of the decoded payload length in bytes for the bundle archive. + Provided for bandwidth estimation and update planning. + MUST NOT be used for integrity verification. + range: SizeBytesType + url: + description: >- + Content-addressable retrieval endpoint for the bundle of the form /api/v1/clients/{clientId}/bundles/{digest} where {digest} equals bundle.digest. + range: UrlType + required: true + + Deployment: + description: >- + Reference to an individual ApplicationDeployment within the desired state manifest. + attributes: + deploymentId: + description: >- + The UUID of the deployment. + MUST equal metadata.annotations.id in the ApplicationDeployment. + range: string + required: true + digest: + description: >- + Digest of the corresponding ApplicationDeployment YAML file. + MUST equal the digest computed over the exact sequence of bytes in the individual deployment endpoint's HTTP 200 OK response body. + See Protocol - Digest for further details. + range: DigestType + required: true + sizeBytes: + description: >- + Optional unsigned 64-bit advisory estimate of the decoded payload length in bytes for the ApplicationDeployment YAML. + Provided for bandwidth estimation and update planning. + MUST NOT be used for integrity verification. + range: SizeBytesType + url: + description: >- + Content-addressable retrieval endpoint for the ApplicationDeployment YAML of the form /api/v1/clients/{clientId}/deployments/{deploymentId}/{digest} where {digest} equals deployments[].digest. + range: UrlType + required: true + +types: + DigestType: + description: >- + Hash that identifies an element by its content. + Digests have to follow the pattern: ":". + For example: "sha256:8a9b07...". + uri: xsd:string + base: string + repr: str + pattern: "^[a-zA-Z0-9_-]+:[0-9a-fA-F]+$" + SizeBytesType: + description: Size of an element in bytes. + uri: xsd:nonNegativeInteger + base: int + minimum_value: 0 + UrlType: + description: Endpoint (URL without schema and domain) associated to an element. + uri: xsd:anyURI + base: URI + repr: str + pattern: "^/[a-zA-Z0-9-._~:/?#\\[\\]@!$&'()*+,;=]+$" diff --git a/model/device-capabilities.linkml.yaml b/model/device-capabilities.linkml.yaml new file mode 100644 index 00000000..9eb69669 --- /dev/null +++ b/model/device-capabilities.linkml.yaml @@ -0,0 +1,121 @@ +# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json +id: https://specification.margo.org/device-capabilities +name: DeviceCapabilities +title: Device Capabilities +description: >- + Schema for defining device capabilities. + The purpose of the Device Capabilities is that WFMs can match application deployments with the capabilities of the target devices. +version: 1.0.0 +prefixes: + linkml: https://w3id.org/linkml/ + margo: https://specification.margo.org/ +imports: + - linkml:types + - margo-resources.linkml + +default_prefix: margo +# default_range: string # https://github.com/linkml/linkml/issues/1483 + +# Class Definitions +classes: + DeviceCapabilitiesManifest: + description: >- + Capabilities of a device on which applications can be deployed. + attributes: + apiVersion: + description: Identifier of the version the API resource follows. + range: string + required: true + kind: + description: Must be `DeviceCapabilitiesManifest`. + range: string + required: true + equals_string: "DeviceCapabilitiesManifest" + designates_type: true + properties: + description: >- + Element that defines characteristics about the device. + See the [Properties Attributes](#properties-attributes) section below. + required: true + range: Properties + + Properties: + description: >- + Device properties reported to the WFM. + attributes: + id: + description: Unique deviceID assigned to the device via the Device Owner. + range: string + required: true + vendor: + description: Defines the device vendor. + range: string + required: true + modelNumber: + description: Defines the model number of the device. + range: string + required: true + serialNumber: + description: Defines the serial number of the device. + range: string + required: true + roles: + description: >- + Element that defines the device role it can provide to the Margo environment. + MUST be one of the following: Standalone Cluster, Cluster Leader, Standalone Device, or Gateway. + required: true + multivalued: true + inlined: false + range: DeviceRole + resources: + description: >- + Element that defines the device's resources available to the application deployed on the device. + See the [Resources Attributes](#resources-attributes) section below. + required: true + range: Resources + + # Override Resources for device capabilities (availability offering): + # all fields are required here, whereas in application descriptions (resource claiming) + # they are optional. Include the slots list so the OpenAPI generator emits the full schema. + DeviceResources: + slots: + - cpu + - memory + - storage + - peripherals + - interfaces + slot_usage: + cpu: + required: true + memory: + required: true + storage: + required: true + peripherals: + required: true + interfaces: + required: true + +# Enumeration Definitions +enums: + DeviceRole: + description: >- + Role a device can provide to the Margo environment. + permissible_values: + Standalone Cluster: + description: >- + Select this role to run Helm applications. + See [Edge compute devices](../../concepts/edge-compute-devices/devices#standalone-cluster-role-details) for more information. + Standalone Device: + description: >- + Select this role to run Compose applications. + See [Edge compute devices](../../concepts/edge-compute-devices/devices#standalone-device-role-details) for more information. + Cluster Leader: + description: >- + Select this role for the leader node of a multi-node Helm cluster. + See [Edge compute devices](../../concepts/edge-compute-devices/devices#cluster-leader-role-details) for more information. + Gateway: + description: >- + Select this role for a see-thru gateway that connects one or more child-devices to the WFM, + exposing each child-device individually with its own capabilities. + See [Gateways](../../concepts/gateways/gateways.md) for more information. diff --git a/model/diagrams/all-classes.png b/model/diagrams/all-classes.png new file mode 100644 index 00000000..a6db6586 Binary files /dev/null and b/model/diagrams/all-classes.png differ diff --git a/model/diagrams/application-deployment.png b/model/diagrams/application-deployment.png new file mode 100644 index 00000000..551bbb69 Binary files /dev/null and b/model/diagrams/application-deployment.png differ diff --git a/model/diagrams/application-description.png b/model/diagrams/application-description.png new file mode 100644 index 00000000..a1367140 Binary files /dev/null and b/model/diagrams/application-description.png differ diff --git a/model/diagrams/desired-state-manifest.png b/model/diagrams/desired-state-manifest.png new file mode 100644 index 00000000..68525a50 Binary files /dev/null and b/model/diagrams/desired-state-manifest.png differ diff --git a/model/diagrams/device-capabilities.png b/model/diagrams/device-capabilities.png new file mode 100644 index 00000000..4a79d152 Binary files /dev/null and b/model/diagrams/device-capabilities.png differ diff --git a/model/diagrams/margo-data-model.png b/model/diagrams/margo-data-model.png new file mode 100644 index 00000000..2911969e Binary files /dev/null and b/model/diagrams/margo-data-model.png differ diff --git a/model/examples/invalid/ApplicationDeployment-001.yaml b/model/examples/invalid/ApplicationDeployment-001.yaml new file mode 100644 index 00000000..e3438545 --- /dev/null +++ b/model/examples/invalid/ApplicationDeployment-001.yaml @@ -0,0 +1,92 @@ +# Demonstrates validation of metadata.annotations.id +# Invalid kind of Kubernetes custom resource. +apiVersion: application.margo.org/v1alpha1 +# `kind` != `ApplicationDeployment` +kind: SomethingErroneous +metadata: + annotations: + applicationId: com-northstartida-digitron-orchestrator + id: a3e2f5dc-912e-494f-8395-52cf3769bc06 + name: com-northstartida-digitron-orchestrator-deployment + namespace: margo-poc +spec: + deploymentProfile: + type: helm.v3 + components: + - name: database-services + properties: + repository: oci://quay.io/charts/realtime-database-services + revision: 2.3.7 + timeout: 8m30s + wait: true + - name: digitron-orchestrator + properties: + repository: oci://northstarida.azurecr.io/charts/northstarida-digitron-orchestrator + revision: 1.0.9 + wait: true + parameters: + adminName: + value: Some One + targets: + - pointer: administrator.name + components: + - digitron-orchestrator + adminPrincipalName: + value: someone@somewhere.com + targets: + - pointer: administrator.userPrincipalName + components: + - digitron-orchestrator + cpuLimit: + value: "4" + targets: + - pointer: settings.limits.cpu + components: + - digitron-orchestrator + idpClientId: + value: 123-ABC + targets: + - pointer: idp.clientId + components: + - digitron-orchestrator + idpName: + value: Azure AD + targets: + - pointer: idp.name + components: + - digitron-orchestrator + idpProvider: + value: aad + targets: + - pointer: idp.provider + components: + - digitron-orchestrator + idpUrl: + value: https://123-abc.com + targets: + - pointer: idp.providerUrl + components: + - digitron-orchestrator + - pointer: idp.providerMetadata + components: + - digitron-orchestrator + memoryLimit: + value: "16384" + targets: + - pointer: settings.limits.memory + components: + - digitron-orchestrator + pollFrequency: + value: "120" + targets: + - pointer: settings.pollFrequency + components: + - digitron-orchestrator + - database-services + siteId: + value: SID-123-ABC + targets: + - pointer: settings.siteId + components: + - digitron-orchestrator + - database-services diff --git a/model/examples/invalid/ApplicationDeployment-002.yaml b/model/examples/invalid/ApplicationDeployment-002.yaml new file mode 100644 index 00000000..8876caa5 --- /dev/null +++ b/model/examples/invalid/ApplicationDeployment-002.yaml @@ -0,0 +1,92 @@ +# Demonstrates validation of metadata.annotations.applicationId +# Invalid characters being used. +apiVersion: application.margo.org/v1alpha1 +kind: ApplicationDeployment +metadata: + annotations: + # id contains invalid characters: upper-case letters ('A' and 'Z'), special characters ('.' and '_') + applicationId: com_northstartida.Digitron-Orchestrator + id: a3e2f5dc-912e-494f-8395-52cf3769bc06 + name: com-northstartida-digitron-orchestrator-deployment + namespace: margo-poc +spec: + deploymentProfile: + type: helm.v3 + components: + - name: database-services + properties: + repository: oci://quay.io/charts/realtime-database-services + revision: 2.3.7 + timeout: 8m30s + wait: true + - name: digitron-orchestrator + properties: + repository: oci://northstarida.azurecr.io/charts/northstarida-digitron-orchestrator + revision: 1.0.9 + wait: true + parameters: + adminName: + value: Some One + targets: + - pointer: administrator.name + components: + - digitron-orchestrator + adminPrincipalName: + value: someone@somewhere.com + targets: + - pointer: administrator.userPrincipalName + components: + - digitron-orchestrator + cpuLimit: + value: "4" + targets: + - pointer: settings.limits.cpu + components: + - digitron-orchestrator + idpClientId: + value: 123-ABC + targets: + - pointer: idp.clientId + components: + - digitron-orchestrator + idpName: + value: Azure AD + targets: + - pointer: idp.name + components: + - digitron-orchestrator + idpProvider: + value: aad + targets: + - pointer: idp.provider + components: + - digitron-orchestrator + idpUrl: + value: https://123-abc.com + targets: + - pointer: idp.providerUrl + components: + - digitron-orchestrator + - pointer: idp.providerMetadata + components: + - digitron-orchestrator + memoryLimit: + value: "16384" + targets: + - pointer: settings.limits.memory + components: + - digitron-orchestrator + pollFrequency: + value: "120" + targets: + - pointer: settings.pollFrequency + components: + - digitron-orchestrator + - database-services + siteId: + value: SID-123-ABC + targets: + - pointer: settings.siteId + components: + - digitron-orchestrator + - database-services diff --git a/model/examples/invalid/ApplicationDeployment-003.yaml b/model/examples/invalid/ApplicationDeployment-003.yaml new file mode 100644 index 00000000..62513516 --- /dev/null +++ b/model/examples/invalid/ApplicationDeployment-003.yaml @@ -0,0 +1,92 @@ +# Demonstrates validation of metadata.annotations.applicationId +# Too long. +apiVersion: application.margo.org/v1alpha1 +kind: ApplicationDeployment +metadata: + annotations: + # id is over 200 characters long (it is 201 characters long) + applicationId: "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901" + id: a3e2f5dc-912e-494f-8395-52cf3769bc06 + name: com-northstartida-digitron-orchestrator-deployment + namespace: margo-poc +spec: + deploymentProfile: + type: helm.v3 + components: + - name: database-services + properties: + repository: oci://quay.io/charts/realtime-database-services + revision: 2.3.7 + timeout: 8m30s + wait: true + - name: digitron-orchestrator + properties: + repository: oci://northstarida.azurecr.io/charts/northstarida-digitron-orchestrator + revision: 1.0.9 + wait: true + parameters: + adminName: + value: Some One + targets: + - pointer: administrator.name + components: + - digitron-orchestrator + adminPrincipalName: + value: someone@somewhere.com + targets: + - pointer: administrator.userPrincipalName + components: + - digitron-orchestrator + cpuLimit: + value: "4" + targets: + - pointer: settings.limits.cpu + components: + - digitron-orchestrator + idpClientId: + value: 123-ABC + targets: + - pointer: idp.clientId + components: + - digitron-orchestrator + idpName: + value: Azure AD + targets: + - pointer: idp.name + components: + - digitron-orchestrator + idpProvider: + value: aad + targets: + - pointer: idp.provider + components: + - digitron-orchestrator + idpUrl: + value: https://123-abc.com + targets: + - pointer: idp.providerUrl + components: + - digitron-orchestrator + - pointer: idp.providerMetadata + components: + - digitron-orchestrator + memoryLimit: + value: "16384" + targets: + - pointer: settings.limits.memory + components: + - digitron-orchestrator + pollFrequency: + value: "120" + targets: + - pointer: settings.pollFrequency + components: + - digitron-orchestrator + - database-services + siteId: + value: SID-123-ABC + targets: + - pointer: settings.siteId + components: + - digitron-orchestrator + - database-services diff --git a/model/examples/invalid/ApplicationDeployment-004.yaml b/model/examples/invalid/ApplicationDeployment-004.yaml new file mode 100644 index 00000000..ec985bec --- /dev/null +++ b/model/examples/invalid/ApplicationDeployment-004.yaml @@ -0,0 +1,92 @@ +# Demonstrates validation of metadata.annotations.id +# Not a valid UUID. +apiVersion: application.margo.org/v1alpha1 +kind: ApplicationDeployment +metadata: + annotations: + applicationId: com-northstartida-digitron-orchestrator + # id is not a valid UUID + id: this-is-definitively-not-a-uuid + name: com-northstartida-digitron-orchestrator-deployment + namespace: margo-poc +spec: + deploymentProfile: + type: helm.v3 + components: + - name: database-services + properties: + repository: oci://quay.io/charts/realtime-database-services + revision: 2.3.7 + timeout: 8m30s + wait: true + - name: digitron-orchestrator + properties: + repository: oci://northstarida.azurecr.io/charts/northstarida-digitron-orchestrator + revision: 1.0.9 + wait: true + parameters: + adminName: + value: Some One + targets: + - pointer: administrator.name + components: + - digitron-orchestrator + adminPrincipalName: + value: someone@somewhere.com + targets: + - pointer: administrator.userPrincipalName + components: + - digitron-orchestrator + cpuLimit: + value: "4" + targets: + - pointer: settings.limits.cpu + components: + - digitron-orchestrator + idpClientId: + value: 123-ABC + targets: + - pointer: idp.clientId + components: + - digitron-orchestrator + idpName: + value: Azure AD + targets: + - pointer: idp.name + components: + - digitron-orchestrator + idpProvider: + value: aad + targets: + - pointer: idp.provider + components: + - digitron-orchestrator + idpUrl: + value: https://123-abc.com + targets: + - pointer: idp.providerUrl + components: + - digitron-orchestrator + - pointer: idp.providerMetadata + components: + - digitron-orchestrator + memoryLimit: + value: "16384" + targets: + - pointer: settings.limits.memory + components: + - digitron-orchestrator + pollFrequency: + value: "120" + targets: + - pointer: settings.pollFrequency + components: + - digitron-orchestrator + - database-services + siteId: + value: SID-123-ABC + targets: + - pointer: settings.siteId + components: + - digitron-orchestrator + - database-services diff --git a/model/examples/invalid/ApplicationDescription-001.yaml b/model/examples/invalid/ApplicationDescription-001.yaml new file mode 100644 index 00000000..02c3ae6d --- /dev/null +++ b/model/examples/invalid/ApplicationDescription-001.yaml @@ -0,0 +1,59 @@ +apiVersion: margo.org/v1-alpha1 +kind: ApplicationSpecification +metadata: + id: com-northstartida-hello-world + name: Hello World + description: A basic hello world application + version: "1.0" + catalog: + application: + icon: ./resources/hw-logo.png + tagline: Northstar Industrial Application's hello world application. + descriptionFile: ./resources/description.md + releaseNotes: ./resources/release-notes.md + licenseFile: ./resources/license.pdf + site: http://www.northstar-ida.com + tags: ["monitoring"] + author: + - name: Roger Wilkershank + email: rpwilkershank@northstar-ida.com + organization: + - name: Northstar Industrial Applications + site: http://northstar-ida.com +deploymentProfiles: + - type: helm.v3 + id: com-northstartida-hello-world-helm.v3-a + components: + - name: hello-world + properties: + repository: oci://northstarida.azurecr.io/charts/hello-world + revision: 1.0.1 + wait: true +parameters: + greeting: + value: Hello + targets: + - pointer: global.config.appGreeting + components: ["hello-world"] + greetingAddressee: + value: World + targets: + - pointer: global.config.appGreetingAddressee + components: ["hello-world"] +configuration: + sections: + - name: General Settings + settings: + - parameter: greeting + name: Greeting + description: The greeting to use. + schema: requireText + - parameter: greetingAddressee + name: Greeting Addressee + description: The person, or group, the greeting addresses. + schema: requireText + schema: + - name: requireText + dataType: string + maxLength: 45 + allowEmpty: false diff --git a/model/examples/invalid/DeploymentStatusManifest-001.json b/model/examples/invalid/DeploymentStatusManifest-001.json new file mode 100644 index 00000000..92dcd322 --- /dev/null +++ b/model/examples/invalid/DeploymentStatusManifest-001.json @@ -0,0 +1,14 @@ +{ + "apiVersion": "v1", + "kind": "WrongKind", + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "status": { + "state": "installed" + }, + "components": [ + { + "name": "frontend", + "state": "installed" + } + ] +} diff --git a/model/examples/invalid/DeploymentStatusManifest-002.json b/model/examples/invalid/DeploymentStatusManifest-002.json new file mode 100644 index 00000000..031d997f --- /dev/null +++ b/model/examples/invalid/DeploymentStatusManifest-002.json @@ -0,0 +1,14 @@ +{ + "apiVersion": "v1", + "kind": "DeploymentStatusManifest", + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "status": { + "state": "running" + }, + "components": [ + { + "name": "frontend", + "state": "installed" + } + ] +} diff --git a/model/examples/invalid/DeploymentStatusManifest-003.json b/model/examples/invalid/DeploymentStatusManifest-003.json new file mode 100644 index 00000000..672f3fc6 --- /dev/null +++ b/model/examples/invalid/DeploymentStatusManifest-003.json @@ -0,0 +1,13 @@ +{ + "apiVersion": "v1", + "kind": "DeploymentStatusManifest", + "status": { + "state": "installed" + }, + "components": [ + { + "name": "frontend", + "state": "installed" + } + ] +} diff --git a/model/examples/invalid/DesiredStateManifest-001.json b/model/examples/invalid/DesiredStateManifest-001.json new file mode 100644 index 00000000..c0488575 --- /dev/null +++ b/model/examples/invalid/DesiredStateManifest-001.json @@ -0,0 +1,15 @@ +{ + "manifestVersion": 0, + "bundle": { + "mediaType": "application/vnd.margo.bundle.v1+tar+gzip", + "digest": "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "/api/v1/clients/1234/bundles/sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + }, + "deployments": [ + { + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "digest": "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "/api/v1/clients/1234/deployments/a3e2f5dc-912e-494f-8395-52cf3769bc06/sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + } + ] +} diff --git a/model/examples/invalid/DesiredStateManifest-002.json b/model/examples/invalid/DesiredStateManifest-002.json new file mode 100644 index 00000000..431dd775 --- /dev/null +++ b/model/examples/invalid/DesiredStateManifest-002.json @@ -0,0 +1,15 @@ +{ + "manifestVersion": 101, + "bundle": { + "mediaType": "application/json", + "digest": "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "/api/v1/clients/1234/bundles/sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + }, + "deployments": [ + { + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "digest": "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "/api/v1/clients/1234/deployments/a3e2f5dc-912e-494f-8395-52cf3769bc06/sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + } + ] +} diff --git a/model/examples/invalid/DesiredStateManifest-003.json b/model/examples/invalid/DesiredStateManifest-003.json new file mode 100644 index 00000000..16bac6aa --- /dev/null +++ b/model/examples/invalid/DesiredStateManifest-003.json @@ -0,0 +1,15 @@ +{ + "manifestVersion": 101, + "bundle": { + "mediaType": "application/vnd.margo.bundle.v1+tar+gzip", + "digest": "invalid-digest", + "url": "/api/v1/clients/1234/bundles/invalid-digest" + }, + "deployments": [ + { + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "digest": "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "/api/v1/clients/1234/deployments/a3e2f5dc-912e-494f-8395-52cf3769bc06/sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + } + ] +} diff --git a/model/examples/invalid/DesiredStateManifest-004.json b/model/examples/invalid/DesiredStateManifest-004.json new file mode 100644 index 00000000..a53980ac --- /dev/null +++ b/model/examples/invalid/DesiredStateManifest-004.json @@ -0,0 +1,10 @@ +{ + "manifestVersion": 101, + "deployments": [ + { + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "digest": "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "/api/v1/clients/1234/deployments/a3e2f5dc-912e-494f-8395-52cf3769bc06/sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + } + ] +} diff --git a/model/examples/invalid/DeviceCapabilitiesManifest-001.json b/model/examples/invalid/DeviceCapabilitiesManifest-001.json new file mode 100644 index 00000000..0388d106 --- /dev/null +++ b/model/examples/invalid/DeviceCapabilitiesManifest-001.json @@ -0,0 +1,37 @@ +{ + "apiVersion": "device.margo.org/v1alpha1", + "kind": "DeviceCapabilitiesManifest", + "properties": { + "id": "northstarida.xtapro.k8s.edge", + "vendor": "Northstar Industrial devices", + "modelNumber": "332ANZE1-N1", + "serialNumber": "PF45343-AA", + "roles": [ + "Non existing role" + ], + "resources": { + "cpu": { + "cores": 24, + "architectures": [ + "x86_64" + ] + }, + "memory": "59Gi", + "storage": "1862Gi", + "peripherals": [ + { + "type": "gpu", + "manufacturer": "NVIDIA" + } + ], + "interfaces": [ + { + "type": "ethernet" + }, + { + "type": "wifi" + } + ] + } + } +} diff --git a/model/examples/invalid/DeviceCapabilitiesManifest-002.json b/model/examples/invalid/DeviceCapabilitiesManifest-002.json new file mode 100644 index 00000000..4c7afc02 --- /dev/null +++ b/model/examples/invalid/DeviceCapabilitiesManifest-002.json @@ -0,0 +1,36 @@ +{ + "apiVersion": "device.margo.org/v1alpha1", + "kind": "DeviceCapabilitiesManifest", + "properties": { + "id": "northstarida.xtapro.k8s.edge", + "modelNumber": "332ANZE1-N1", + "serialNumber": "PF45343-AA", + "roles": [ + "Standalone Device" + ], + "resources": { + "cpu": { + "cores": 24, + "architectures": [ + "x86_64" + ] + }, + "memory": "59Gi", + "storage": "1862Gi", + "peripherals": [ + { + "type": "gpu", + "manufacturer": "NVIDIA" + } + ], + "interfaces": [ + { + "type": "ethernet" + }, + { + "type": "wifi" + } + ] + } + } +} diff --git a/model/examples/invalid/DeviceCapabilitiesManifest-003.json b/model/examples/invalid/DeviceCapabilitiesManifest-003.json new file mode 100644 index 00000000..f0a16de2 --- /dev/null +++ b/model/examples/invalid/DeviceCapabilitiesManifest-003.json @@ -0,0 +1,37 @@ +{ + "apiVersion": "device.margo.org/v1alpha1", + "kind": "DeviceCapabilitiesManifest", + "properties": { + "id": "northstarida.xtapro.k8s.edge", + "vendor": "Northstar Industrial devices", + "modelNumber": "332ANZE1-N1", + "serialNumber": "PF45343-AA", + "roles": [ + "Standalone Cluster" + ], + "resources": { + "cpu": { + "cores": 24, + "architectures": [ + "riscv" + ] + }, + "memory": "59Gi", + "storage": "1862Gi", + "peripherals": [ + { + "type": "gpu", + "manufacturer": "NVIDIA" + } + ], + "interfaces": [ + { + "type": "ethernet" + }, + { + "type": "wifi" + } + ] + } + } +} diff --git a/model/examples/valid/ApplicationDeployment-compose.yaml b/model/examples/valid/ApplicationDeployment-compose.yaml new file mode 100644 index 00000000..e48606ec --- /dev/null +++ b/model/examples/valid/ApplicationDeployment-compose.yaml @@ -0,0 +1,65 @@ +apiVersion: application.margo.org/v1alpha1 +kind: ApplicationDeployment +metadata: + annotations: + applicationId: com-northstartida-digitron-orchestrator + id: ad9b614e-8912-45f4-a523-372358765def + name: com-northstartida-digitron-orchestrator-deployment + namespace: margo-poc +spec: + deploymentProfile: + type: compose + components: + - name: digitron-orchestrator-docker + properties: + keyLocation: https://northsitarida.com/digitron/docker/public-key.asc + packageLocation: https://northsitarida.com/digitron/docker/digitron-orchestrator.tar.gz + parameters: + adminName: + value: Some One + targets: + - pointer: ENV.ADMIN_NAME + components: + - digitron-orchestrator-docker + adminPrincipalName: + value: someone@somewhere.com + targets: + - pointer: ENV.ADMIN_PRINCIPALNAME + components: + - digitron-orchestrator-docker + idpClientId: + value: 123-ABC + targets: + - pointer: ENV.IDP_CLIENT_ID + components: + - digitron-orchestrator-docker + idpName: + value: Azure AD + targets: + - pointer: ENV.IDP_NAME + components: + - digitron-orchestrator-docker + idpProvider: + value: aad + targets: + - pointer: ENV.IDP_PROVIDER + components: + - digitron-orchestrator-docker + idpUrl: + value: https://123-abc.com + targets: + - pointer: ENV.IDP_URL + components: + - digitron-orchestrator-docker + pollFrequency: + value: "120" + targets: + - pointer: ENV.POLL_FREQUENCY + components: + - digitron-orchestrator-docker + siteId: + value: SID-123-ABC + targets: + - pointer: ENV.SITE_ID + components: + - digitron-orchestrator-docker diff --git a/model/examples/valid/ApplicationDeployment-helm.yaml b/model/examples/valid/ApplicationDeployment-helm.yaml new file mode 100644 index 00000000..9baa6032 --- /dev/null +++ b/model/examples/valid/ApplicationDeployment-helm.yaml @@ -0,0 +1,89 @@ +apiVersion: application.margo.org/v1alpha1 +kind: ApplicationDeployment +metadata: + annotations: + applicationId: com-northstartida-digitron-orchestrator + id: a3e2f5dc-912e-494f-8395-52cf3769bc06 + name: com-northstartida-digitron-orchestrator-deployment + namespace: margo-poc +spec: + deploymentProfile: + type: helm.v3 + components: + - name: database-services + properties: + repository: oci://quay.io/charts/realtime-database-services + revision: 2.3.7 + timeout: 8m30s + wait: true + - name: digitron-orchestrator + properties: + repository: oci://northstarida.azurecr.io/charts/northstarida-digitron-orchestrator + revision: 1.0.9 + wait: true + parameters: + adminName: + value: Some One + targets: + - pointer: administrator.name + components: + - digitron-orchestrator + adminPrincipalName: + value: someone@somewhere.com + targets: + - pointer: administrator.userPrincipalName + components: + - digitron-orchestrator + cpuLimit: + value: "4" + targets: + - pointer: settings.limits.cpu + components: + - digitron-orchestrator + idpClientId: + value: 123-ABC + targets: + - pointer: idp.clientId + components: + - digitron-orchestrator + idpName: + value: Azure AD + targets: + - pointer: idp.name + components: + - digitron-orchestrator + idpProvider: + value: aad + targets: + - pointer: idp.provider + components: + - digitron-orchestrator + idpUrl: + value: https://123-abc.com + targets: + - pointer: idp.providerUrl + components: + - digitron-orchestrator + - pointer: idp.providerMetadata + components: + - digitron-orchestrator + memoryLimit: + value: "16384" + targets: + - pointer: settings.limits.memory + components: + - digitron-orchestrator + pollFrequency: + value: "120" + targets: + - pointer: settings.pollFrequency + components: + - digitron-orchestrator + - database-services + siteId: + value: SID-123-ABC + targets: + - pointer: settings.siteId + components: + - digitron-orchestrator + - database-services diff --git a/model/examples/valid/ApplicationDescription-helm_and_compose.yaml b/model/examples/valid/ApplicationDescription-helm_and_compose.yaml new file mode 100644 index 00000000..2d76d63d --- /dev/null +++ b/model/examples/valid/ApplicationDescription-helm_and_compose.yaml @@ -0,0 +1,208 @@ +apiVersion: margo.org/v1-alpha1 +kind: ApplicationDescription +metadata: + id: com-northstartida-digitron-orchestrator + name: Digitron orchestrator + description: The Digitron orchestrator application + version: 1.2.1 + catalog: + application: + icon: ./resources/ndo-logo.png + tagline: Northstar Industrial Application's next-gen, AI driven, Digitron instrument orchestrator. + descriptionFile: ./resources/description.md + releaseNotes: ./resources/release-notes.md + licenseFile: ./resources/license.pdf + site: http://www.northstar-ida.com + tags: ["optimization", "instrumentation"] + author: + - name: Roger Wilkershank + email: rpwilkershank@northstar-ida.com + organization: + - name: Northstar Industrial Applications + site: http://northstar-ida.com +deploymentProfiles: + - type: helm.v3 + id: com-northstartida-digitron-orchestrator-helm.v3-a + description: This allows to install / run the application as a Helm chart deployment. + The device where this application is installed needs to have a screen and a keyboard (as indicated in the required peripherals). + components: + - name: database-services + properties: + repository: oci://quay.io/charts/realtime-database-services + revision: 2.3.7 + wait: true + timeout: 8m30s + - name: digitron-orchestrator + properties: + repository: oci://northstarida.azurecr.io/charts/northstarida-digitron-orchestrator + revision: 1.0.9 + wait: true + requiredResources: + cpu: + cores: 1.5 + architectures: + - amd64 + - x86_64 + memory: 1024Mi + storage: 10Gi + peripherals: + - type: gpu + manufacturer: NVIDIA + - type: display + interfaces: + - type: ethernet + - type: bluetooth + - type: compose + id: com-northstartida-digitron-orchestrator-compose-a + components: + - name: digitron-orchestrator-docker + properties: + packageLocation: https://northsitarida.com/digitron/docker/digitron-orchestrator.tar.gz + keyLocation: https://northsitarida.com/digitron/docker/public-key.asc +parameters: + idpName: + targets: + - pointer: idp.name + components: ["digitron-orchestrator"] + - pointer: ENV.IDP_NAME + components: ["digitron-orchestrator-docker"] + idpProvider: + targets: + - pointer: idp.provider + components: ["digitron-orchestrator"] + - pointer: ENV.IDP_PROVIDER + components: ["digitron-orchestrator-docker"] + idpClientId: + targets: + - pointer: idp.clientId + components: ["digitron-orchestrator"] + - pointer: ENV.IDP_CLIENT_ID + components: ["digitron-orchestrator-docker"] + idpUrl: + targets: + - pointer: idp.providerUrl + components: ["digitron-orchestrator"] + - pointer: idp.providerMetadata + components: ["digitron-orchestrator"] + - pointer: ENV.IDP_URL + components: ["digitron-orchestrator-docker"] + adminName: + targets: + - pointer: administrator.name + components: ["digitron-orchestrator"] + - pointer: ENV.ADMIN_NAME + components: ["digitron-orchestrator-docker"] + adminPrincipalName: + targets: + - pointer: administrator.userPrincipalName + components: ["digitron-orchestrator"] + - pointer: ENV.ADMIN_PRINCIPALNAME + components: ["digitron-orchestrator-docker"] + pollFrequency: + value: 30 + targets: + - pointer: settings.pollFrequency + components: ["digitron-orchestrator", "database-services"] + - pointer: ENV.POLL_FREQUENCY + components: ["digitron-orchestrator-docker"] + siteId: + targets: + - pointer: settings.siteId + components: ["digitron-orchestrator", "database-services"] + - pointer: ENV.SITE_ID + components: ["digitron-orchestrator-docker"] + cpuLimit: + value: 1 + targets: + - pointer: settings.limits.cpu + components: ["digitron-orchestrator"] + memoryLimit: + value: 16384 + targets: + - pointer: settings.limits.memory + components: ["digitron-orchestrator"] +configuration: + sections: + - name: General + settings: + - parameter: pollFrequency + name: Poll Frequency + description: How often the service polls for updated data in seconds + schema: pollRange + - parameter: siteId + name: Site Id + description: Special identifier for the site (optional) + schema: optionalText + - name: Identity Provider + settings: + - parameter: idpName + name: Name + description: The name of the Identity Provider to use + immutable: true + schema: requiredText + - parameter: idpProvider + name: Provider + description: Provider something something + immutable: true + schema: requiredText + - parameter: idpClientId + name: Client ID + description: The client id + immutable: true + schema: requiredText + - parameter: idpUrl + name: Provider URL + description: The url of the Identity Provider + immutable: true + schema: url + - name: Administrator + settings: + - parameter: adminName + name: Presentation Name + description: The presentation name of the administrator + schema: requiredText + - parameter: adminPrincipalName + name: Principal Name + description: The principal name of the administrator + schema: email + - name: Resource Limits + settings: + - parameter: cpuLimit + name: CPU Limit + description: Maximum number of CPU cores to allow the application to consume + schema: cpuRange + - parameter: memoryLimit + name: Memory Limit + description: Maximum number of memory to allow the application to consume + schema: memoryRange + schema: + - name: requiredText + dataType: string + maxLength: 45 + allowEmpty: false + - name: email + dataType: string + allowEmpty: false + regexMatch: .*@[a-z0-9.-]* + - name: url + dataType: string + allowEmpty: false + regexMatch: ^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$ + - name: pollRange + dataType: integer + minValue: 30 + maxValue: 360 + allowEmpty: false + - name: optionalText + dataType: string + minLength: 5 + allowEmpty: true + - name: cpuRange + dataType: double + minValue: 0.5 + maxPrecision: 1 + allowEmpty: false + - name: memoryRange + dataType: integer + minValue: 16384 + allowEmpty: false diff --git a/model/examples/valid/ApplicationDescription-helm_only.yaml b/model/examples/valid/ApplicationDescription-helm_only.yaml new file mode 100644 index 00000000..158abb88 --- /dev/null +++ b/model/examples/valid/ApplicationDescription-helm_only.yaml @@ -0,0 +1,59 @@ +apiVersion: margo.org/v1-alpha1 +kind: ApplicationDescription +metadata: + id: com-northstartida-hello-world + name: Hello World + description: A basic hello world application + version: "1.0" + catalog: + application: + icon: ./resources/hw-logo.png + tagline: Northstar Industrial Application's hello world application. + descriptionFile: ./resources/description.md + releaseNotes: ./resources/release-notes.md + licenseFile: ./resources/license.pdf + site: http://www.northstar-ida.com + tags: ["monitoring"] + author: + - name: Roger Wilkershank + email: rpwilkershank@northstar-ida.com + organization: + - name: Northstar Industrial Applications + site: http://northstar-ida.com +deploymentProfiles: + - type: helm.v3 + id: com-northstartida-hello-world-helm.v3-a + components: + - name: hello-world + properties: + repository: oci://northstarida.azurecr.io/charts/hello-world + revision: 1.0.1 + wait: true +parameters: + greeting: + value: Hello + targets: + - pointer: global.config.appGreeting + components: ["hello-world"] + greetingAddressee: + value: World + targets: + - pointer: global.config.appGreetingAddressee + components: ["hello-world"] +configuration: + sections: + - name: General Settings + settings: + - parameter: greeting + name: Greeting + description: The greeting to use. + schema: requireText + - parameter: greetingAddressee + name: Greeting Addressee + description: The person, or group, the greeting addresses. + schema: requireText + schema: + - name: requireText + dataType: string + maxLength: 45 + allowEmpty: false diff --git a/model/examples/valid/DeploymentStatusManifest-001.json b/model/examples/valid/DeploymentStatusManifest-001.json new file mode 100644 index 00000000..01153e65 --- /dev/null +++ b/model/examples/valid/DeploymentStatusManifest-001.json @@ -0,0 +1,22 @@ +{ + "apiVersion": "v1", + "kind": "DeploymentStatusManifest", + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "status": { + "state": "installed" + }, + "components": [ + { + "name": "frontend", + "state": "installed" + }, + { + "name": "backend", + "state": "failed", + "error": { + "code": "ERR_IMAGE_PULL", + "message": "Failed to pull image: access denied" + } + } + ] +} diff --git a/model/examples/valid/DesiredStateManifest-001.json b/model/examples/valid/DesiredStateManifest-001.json new file mode 100644 index 00000000..3fb1b073 --- /dev/null +++ b/model/examples/valid/DesiredStateManifest-001.json @@ -0,0 +1,15 @@ +{ + "manifestVersion": 101, + "bundle": { + "mediaType": "application/vnd.margo.bundle.v1+tar+gzip", + "digest": "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "/api/v1/clients/1234/bundles/sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + }, + "deployments": [ + { + "deploymentId": "a3e2f5dc-912e-494f-8395-52cf3769bc06", + "digest": "sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "url": "/api/v1/clients/1234/deployments/a3e2f5dc-912e-494f-8395-52cf3769bc06/sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + } + ] +} diff --git a/model/examples/valid/DeviceCapabilitiesManifest-001.json b/model/examples/valid/DeviceCapabilitiesManifest-001.json new file mode 100644 index 00000000..502bd828 --- /dev/null +++ b/model/examples/valid/DeviceCapabilitiesManifest-001.json @@ -0,0 +1,37 @@ +{ + "apiVersion": "device.margo.org/v1alpha1", + "kind": "DeviceCapabilitiesManifest", + "properties": { + "id": "northstarida.xtapro.k8s.edge", + "vendor": "Northstar Industrial devices", + "modelNumber": "332ANZE1-N1", + "serialNumber": "PF45343-AA", + "roles": [ + "Standalone Cluster" + ], + "resources": { + "cpu": { + "cores": 24, + "architectures": [ + "x86_64" + ] + }, + "memory": "59Gi", + "storage": "1862Gi", + "peripherals": [ + { + "type": "gpu", + "manufacturer": "NVIDIA" + } + ], + "interfaces": [ + { + "type": "ethernet" + }, + { + "type": "wifi" + } + ] + } + } +} diff --git a/model/generation-gap.md b/model/generation-gap.md new file mode 100644 index 00000000..109aadfe --- /dev/null +++ b/model/generation-gap.md @@ -0,0 +1,149 @@ +# Generation Gap: pre-draft vs generated OpenAPI spec + + + +- [Generation Gap: pre-draft vs generated OpenAPI spec](#generation-gap-pre-draft-vs-generated-openapi-spec) + - [Summary](#summary) + - [Name renames (x-linkml-source)](#name-renames-x-linkml-source) + - [Schemas not generated as separate entries (INLINED)](#schemas-not-generated-as-separate-entries-inlined) + - [Schemas detected as inlined (INFERRED INLINED)](#schemas-detected-as-inlined-inferred-inlined) + - [Schemas with no pre-draft equivalent (EXTRA)](#schemas-with-no-pre-draft-equivalent-extra) + - [Enum inlining](#enum-inlining) + - [Schemas with structural differences (CHANGED)](#schemas-with-structural-differences-changed) + - [ComponentStatus](#componentstatus) + - [DeploymentBundleRef](#deploymentbundleref) + - [DeploymentManifestRef](#deploymentmanifestref) + - [Other changed schemas (14 more)](#other-changed-schemas-14-more) + + + +This document describes the differences between the hand-maintained OpenAPI spec +(on the `pre-draft` branch) and the spec generated by `tools/openapigen.py` +from the LinkML data model. +It is both human-readable and structured for AI consumption. + +Validation is run via: + +```bash +legacy/validate-openapi.py +``` + +Generation (with enums inlined to match pre-draft style): + +```bash +tools/generate-openapi.bash +``` + +Requires `--keep-unreferenced` (`-k`) and `--inline-enums` (`-e`) flags. + +## Summary + +| Metric | Value | +| --------------------------------------------------------------------------------- | ----------------- | +| Pre-draft schemas | 20 | +| Generated schemas | 26 (with `-k -e`) | +| [x-linkml-source renames](#name-renames-x-linkml-source) | 13 | +| [INLINED (explicit comments)](#schemas-not-generated-as-separate-entries-inlined) | 2 | +| [INFERRED INLINED](#schemas-detected-as-inlined-inferred-inlined) | 3 | +| [EXTRA](#schemas-with-no-pre-draft-equivalent-extra) | 5 | +| Pre-draft paths | 7 | +| Path changes | 0 (identical) | + +## Name renames (x-linkml-source) + +These schemas are renamed between the LinkML class name and the OpenAPI schema +name via the template. +The left column is the OpenAPI name in the generated +output. + +| OpenAPI name | LinkML class | +| ---------------------------------------------- | ------------------------------------------------------------ | +| `UnsignedAppStateManifest` | `DesiredStateManifest` | +| `DeviceId` | `FlatDeviceId` | +| `DeviceId_with_asterisk` | `HierarchicalDeviceId` | +| `appDeploymentManifest` | `ApplicationDeployment` | +| `appDeploymentMetadata` | `DeploymentMetadata` | +| `appDeploymentProfile` | `DeploymentProfile` | +| `appDeploymentSpec` | `Spec` | +| `appParameterTarget` | `Target` | +| `appParameterValue` | `Parameter.identifier` (dotted path → synthetic `$defs` key) | +| `DeploymentBundleRef` | `Bundle` | +| `DeploymentManifestRef` | `Deployment` | +| `helmApplicationDeploymentProfileComponent` | `HelmComponent` | +| `composeApplicationDeploymentProfileComponent` | `ComposeComponent` | + +## Schemas not generated as separate entries (INLINED) + +These schemas exist in the pre-draft but are intentionally absent from the +generated output because their content is embedded inside another schema. +Declared via `INLINED` comments in the OpenAPI template +(`tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml`). + +| Schema | Inlined into (OpenAPI) | Inlined into (LinkML) | +| --------------------- | -------------------------- | ---------------------- | +| `ManifestVersion` | `UnsignedAppStateManifest` | `DesiredStateManifest` | +| `appDeploymentParams` | `appDeploymentSpec` | `Spec` | + +## Schemas detected as inlined (INFERRED INLINED) + +These schemas are generated as separate entries by LinkML but the pre-draft +defines them inline within a parent schema. +Detected automatically by `validate-openapi.py` via structural comparison. + +| Generated schema | Inlined in pre-draft at | Score | +| ---------------- | -------------------------------------------------- | ----- | +| `Status` | `DeploymentStatusManifest.properties.status` | 1.0 | +| `Error` | `ComponentStatus.properties.error` | 1.0 | +| `Properties` | `DeviceCapabilitiesManifest.properties.properties` | 0.95 | + +## Schemas with no pre-draft equivalent (EXTRA) + +These schemas are generated by LinkML as transitive dependencies but have no +corresponding schema -- separate or inlined -- in the pre-draft spec. +They are pure data model abstractions. + +| Schema | Kind | Origin | +| ----------------------- | ----- | --------------------------------------------------- | +| `CPU` | class | transitive dep of `DeviceResources` | +| `Component` | class | abstract base of `HelmComponent`/`ComposeComponent` | +| `ComponentProperties` | class | combined Helm+Compose properties | +| `DeploymentAnnotations` | class | transitive dep of `DeploymentMetadata` | +| `Resources` | class | transitive dep of `ComponentProperties` | + +## Enum inlining + +With the `--inline-enums` (`-e`) flag, the generator inlines enum type schemas +(`CpuArchitectureType`, `CommunicationInterfaceType`, `DeviceRole`, +`PeripheralType`, `State`) into their parent schemas instead of generating +separate `components/schemas` entries. +This matches the pre-draft style where enums are always embedded. + +Without `-e`, these 5 enum schemas appear as extra entries. +This might become the default in the future. + +## Schemas with structural differences (CHANGED) + +These schemas exist in both outputs but differ in content beyond descriptions. + +### ComponentStatus + +- Generated has `additionalProperties` (not in pre-draft) +- `error` property: pre-draft has inline object with `properties`/`type`; generated uses `$ref: Error` +- `state` property: generated adds `description` + +### DeploymentBundleRef + +- Generated lacks `nullable: true` (present in pre-draft) +- Generated adds `additionalProperties` and `required` +- Descriptions differ (generated includes pre-data-model references) +- Generated adds `pattern` on `digest` property + +### DeploymentManifestRef + +- Generated adds `additionalProperties` +- Descriptions differ significantly +- Generated adds `pattern` on `digest` property + +### Other changed schemas (14 more) + +Run `legacy/validate-openapi.py` for the complete list. diff --git a/model/margo-data-model.linkml.yaml b/model/margo-data-model.linkml.yaml new file mode 100644 index 00000000..ec2e4ada --- /dev/null +++ b/model/margo-data-model.linkml.yaml @@ -0,0 +1,65 @@ +# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json +id: https://specification.margo.org/data-model +name: DataModel +title: Margo Data Model +description: >- + Margo specification involves complex, interrelated data structures that are used for multiple, different purposes. + + This data model documents the mentioned data structures and their relationships. + + ??? info "General information about the Margo data model" + + The Margo data model has been modelled with **[LinkML](https://linkml.io)** + a **modelling language** that does not only offer very poserfull modelling capabilities, + but also very useful conversion and validation tooling. + + All **examples** shown on the documentation of the classes (see + [ApplicationDescription](ApplicationDescription.md#examples) for an example) are being automatically validated + against the model before a new version of the specification gets published. + + The whole [**HTML documentation** of the data model](.) is also automatically generated from the model. + This documentation provides some graphics that help visualicing the relationship between classes. + + Also following **OpenAPI** v3.0.3 YAML specifications are being automatically generated from the model: + + - [workload-management-api-1.0.0.openapi.yaml](../specification/margo-management-interface/management-interface-swagger/) +version: 1.0.0 +prefixes: + margo: https://specification.margo.org/ +imports: + - application-deployment.linkml + - application-description.linkml + - desired-state-manifest.linkml + - device-capabilities.linkml + - deployment-status.linkml + +default_prefix: margo + +subsets: + api_resources: + title: API Resources + +types: + ManifestVersion: + description: >- + Monotonically increasing unsigned 64-bit integer in the inclusive range [1, 2^64-1]. + Prevents rollback attacks. The first manifest MUST use 1. + uri: xsd:unsignedLong + base: int + minimum_value: 1 + maximum_value: 18446744073709551615 + + FlatDeviceId: + description: >- + Unique identifier of a device or device hierarchy. + Format: "{id}[/{id}[/{id}...]]". The top-level id is required and must include only + unreserved characters as specified in RFC3986. Subsequent ids indicate child devices + in a gateway hierarchy and must also use only unreserved characters. + uri: xsd:string + base: str + pattern: '^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*$' + + HierarchicalDeviceId: + uri: xsd:string + base: str + pattern: '^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*(\/\*)?$' diff --git a/model/margo-deployments.linkml.yaml b/model/margo-deployments.linkml.yaml new file mode 100644 index 00000000..328f2ac6 --- /dev/null +++ b/model/margo-deployments.linkml.yaml @@ -0,0 +1,158 @@ +# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json +id: https://specification.margo.org/deployments +name: Deployments +description: >- + Shared deployment profile, component, parameter, and target types used by both + ApplicationDescription and ApplicationDeployment schemas. +version: 1.0.0 +prefixes: + linkml: https://w3id.org/linkml/ + margo: https://specification.margo.org/ +imports: + - linkml:types + +default_prefix: margo +default_range: string + +classes: + DeploymentProfile: + description: >- + Represents a deployment configuration for the application. + slots: + - type + - components + + HelmDeploymentProfile: + is_a: DeploymentProfile + slot_usage: + type: + equals_string: "helm.v3" + components: + range: HelmComponent + + ComposeDeploymentProfile: + is_a: DeploymentProfile + slot_usage: + type: + equals_string: "compose" + components: + range: ComposeComponent + + Component: + description: >- + A class representing a component of a deployment profile. + attributes: + name: + description: >- + A unique name used to identify the component package. For helm installations the name will be used as the chart name. + The name must be lower case letters and numbers and MAY contain dashes. + Uppercase letters, underscores and periods MUST NOT be used. + required: true + range: string + properties: + description: >- + A dictionary element specifying the component packages's deployment details. + See the [Component Properties](#componentproperties-attributes) section below. + range: ComponentProperties + required: true + + HelmComponent: + is_a: Component + + ComposeComponent: + is_a: Component + + ComponentProperties: + description: >- + Properties dictionary for component deployment details. + attributes: + repository: + description: Repository location for the component. + range: string + revision: + description: Revision version for the component. + range: string + wait: + description: If True, indicates the device waits for the component installation to complete. + range: boolean + timeout: + description: Time to wait for component installation to complete, formatted as "##m##s". + range: string + packageLocation: + description: URL indicating the Compose package's location. + range: string + keyLocation: + description: URL for the public key used to validate a digitally signed package. + range: string + + Parameter: + description: >- + Defines a configurable parameter for the application. + attributes: + name: + description: Name of the parameter. + identifier: true + required: true + range: string + value: + description: >- + The parameter's default value. + Accepted data types are string, integer, double, boolean, array[string], array[integer], array[double], array[boolean]. + any_of: #support for arrays still TBD + - range: boolean + - range: integer + - range: double + - range: string + targets: + description: >- + Used to indicate which component the value should be applied to when installing, or updating, the application. + See the [Target](#target-attributes) section below. + range: Target + required: true + multivalued: true + inlined: true + inlined_as_list: true + + Target: + description: >- + Specifies where the parameter applies in the deployment. + attributes: + pointer: + description: >- + The name of the parameter in the deployment configuration. + For Helm deployments, this is the dot notation for the matching element in the `values.yaml` file. This follows the same naming convention you would use with the `--set` command line argument with the `helm install` command. + For compose deployments, this is the name of the environment variable to set. + range: string + required: true + components: + description: >- + Indicates which deployment profile [component](#component-attributes the parameter target applies to. + The component name specified here MUST match a component name in the [deployment profiles](#deploymentprofile-attributes) section. + range: string + multivalued: true + required: true + +slots: + type: + description: >- + Defines the type of this deployment configuration for the application. + The allowed values are `helm.v3`, to indicate the deployment profile's format is Helm version 3, + and `compose` to indicate the deployment profile's format is a Compose file. + When installing the application on a device supporting the Kubernetes platform, all `helm.v3` components, + and only `helm.v3` components, will be provided to the device in same order they are listed in the application description file. + When installing the application on a device supporting Compose, all `compose` components, + and only `compose` components, will be provided to the device in the same order they are listed in the application description file. + The device will install the components in the same order they are listed in the application description file. + range: string + required: true + pattern: ^(helm\.v3|compose)$ + + components: + description: >- + Component element indicating the components to deploy when installing the application. + See the [Component](#component-attributes) section below. + range: Component + multivalued: true + required: true + inlined: true + inlined_as_list: true diff --git a/model/margo-resources.linkml.yaml b/model/margo-resources.linkml.yaml new file mode 100644 index 00000000..2451021f --- /dev/null +++ b/model/margo-resources.linkml.yaml @@ -0,0 +1,180 @@ +# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json +id: https://specification.margo.org/device-resources +name: DeviceResources +description: >- + Shared resource types used by both device capabilities and application descriptions + to describe hardware resources (CPU, memory, storage, peripherals, communication interfaces). +version: 1.0.0 +prefixes: + linkml: https://w3id.org/linkml/ + margo: https://specification.margo.org/ +imports: + - linkml:types + +default_prefix: margo +default_range: string + +classes: + Resources: + description: >- + Required resources element specifying the resources required to install the application. + slots: + - cpu + - memory + - storage + - peripherals + - interfaces + + CPU: + description: >- + CPU element specifying the CPU requirements for the application. + attributes: + cores: + description: + The required amount of CPU cores the application must use to run in its full functionality. + Specified as decimal units of CPU cores (e.g., `0.5` is half a core). + This is defined by the application developer. + After deployment of the application, the device MUST provide this number of CPU cores for the application. + rank: 10 + range: double + required: true + architectures: + description: + The CPU architectures supported by the application. This can be e.g. amd64, x86_64, arm64, arm. + See the [CpuArchitectureType](#cpuarchitecturetype) definition for all permissible values. + Multiple arcitecture types can be specified, as the deployment profile may support multiple CPU architectures. + rank: 20 + range: CpuArchitectureType + multivalued: true + inlined: false + + DevicePeripheral: + description: >- + Peripheral hardware of a device. + attributes: + type: + description: + The type of peripheral. This can be e.g. GPU, display, camera, microphone, speaker. + See the [PeriperalType](#peripheraltype) definition for all permissible values. + rank: 20 + range: PeripheralType + required: true + manufacturer: + description: The name of the manufacturer. If `manufacturer` is specified as a requirement here, it may be difficult to find devices that can host the application. Please use these requirements with caution. + rank: 30 + range: string + model: + description: The model of the peripheral. If `model` is specified as a requirement here, it may be difficult to find devices that can host the application. Please use these requirements with caution. + rank: 40 + range: string + + DeviceCommunicationInterface: + description: >- + Communication interface of a device. + attributes: + type: + description: + The type of a communication interface. This can be e.g. Ethernet, WiFi, Cellular, Bluetooth, USB, CANBus, RS232. + See the [CommunicationInterfaceType](#communicationinterfacetype) definition for all permissible values. + rank: 30 + range: CommunicationInterfaceType + required: true + +slots: + cpu: + description: >- + CPU element specifying the CPU requirements for the application. + See the [CPU](#cpu-attributes) section below. + range: CPU + + memory: + description: The minimum amount of memory required. + The value is given in binary units (`Ki` = Kibibytes, `Mi` = Mebibytes, `Gi` = Gibibytes). + This is defined by the application developer. + After deployment of the application, the device MUST provide this amount of memory for the application. + range: string + pattern: ^[0-9]+(Mi|Gi|Ki)$ + + storage: + description: + The amount of storage required for the application to run. This encompasses the installed application and the data it needs to store. + The value is given in binary units (`Ki` = Kibibytes, `Mi` = Mebibytes, `Gi` = Gibibytes, `Ti` Tebibytes, `Pi` = Pebibytes, `Ei` = Exbibytes). + This is defined by the application developer. + After deployment of the application, the device MUST provide this amount of storage for the application + range: string + pattern: ^[0-9]+(Mi|Gi|Ki|Ti|Pi|Ei)$ + + peripherals: + description: >- + Peripherals element specifying the peripherals required to run the application. + See the [Peripheral](#peripheral-attributes) section below. + range: DevicePeripheral + multivalued: true + inlined: true + inlined_as_list: true + + interfaces: + description: >- + Interfaces element specifying the communication interfaces required to run the application. + See the [Communication Interfaces](#communicationinterface-attributes) section below. + range: DeviceCommunicationInterface + multivalued: true + inlined: true + inlined_as_list: true + +enums: + CpuArchitectureType: + description: >- + Permissible CPU architecture values. + permissible_values: + amd64: + description: AMD 64-bit architecture. + x86_64: + description: x86 64-bit architecture. + arm64: + description: ARM 64-bit architecture. + arm: + description: ARM 32-bit architecture. + riscv64: + description: RISC-V 64-bit architecture. + other: + description: >- + Any other CPU architecture not listed here. The application developer MUST provide a description of the architecture in the deployment profile's `description` field. + + CommunicationInterfaceType: + description: >- + Permissible communication interface types. + permissible_values: + ethernet: + description: This type stands for an Ethernet interface. + wifi: + description: This type stands for an WiFi interface. + cellular: + description: This type stands for cellular communication technologies such as 5G, LTE, 3G, 2G, .... + bluetooth: + description: This type stands for a Bluetooth or Bluetooth Low-Energy (BLE) interface. + usb: + description: This type stands for a USB interface. + canbus: + description: This type stands for a CANBus interface. + rs232: + description: This type stands for a RS232 interface. + other: + description: This type stands for any other communication interface not listed here. The application developer MUST provide a description of the interface in the deployment profile's `description` field. + + PeripheralType: + description: >- + Permissible peripheral types. + permissible_values: + gpu: + description: This type stands for a Graphics Processing Unit (GPU) peripheral. + display: + description: This type stands for a display peripheral. + camera: + description: This type stands for a camera peripheral. + microphone: + description: This type stands for a microphone peripheral. + speaker: + description: This type stands for a speaker peripheral. + other: + description: This type stands for any other peripheral not listed here. The application developer MUST provide a description of the peripheral in the deployment profile's `description` field. diff --git a/poetry.lock b/poetry.lock index 661c5c89..fb2ea849 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,16 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] [[package]] name = "annotated-types" @@ -56,12 +68,12 @@ files = [ ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\""] [[package]] name = "babel" @@ -118,14 +130,48 @@ rdflib = ">=0.4.2" [[package]] name = "chardet" -version = "5.2.0" -description = "Universal encoding detector for Python 3" +version = "7.4.3" +description = "Universal character encoding detector" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, - {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, + {file = "chardet-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0c79b13c9908ac7dfe0a74116ebc9a0f28b2319d23c32f3dfcdfbe1279c7eaf"}, + {file = "chardet-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bba8bea1b28d927b3e99e47deafe53658d34497c0a891d95ff1ba8ff6663f01c"}, + {file = "chardet-7.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23163921dccf3103ce59540b0443c106d2c0a0ff2e0503e05196f5e6fdea453f"}, + {file = "chardet-7.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfb54563fe5f130da17c44c6a4e2e8052ba628e5ab4eab7ef8190f736f0f8f72"}, + {file = "chardet-7.4.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3990fffcc6a6045f2234ab72752ad037e3b2d48c72037f244d42738db397eb75"}, + {file = "chardet-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c7116b0452994734ccff35e154b44240090eb0f4f74b9106292668133557c175"}, + {file = "chardet-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09"}, + {file = "chardet-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97"}, + {file = "chardet-7.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8"}, + {file = "chardet-7.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9"}, + {file = "chardet-7.4.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4"}, + {file = "chardet-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578"}, + {file = "chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971"}, + {file = "chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a"}, + {file = "chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235"}, + {file = "chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb"}, + {file = "chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f"}, + {file = "chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101"}, + {file = "chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9"}, + {file = "chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a"}, + {file = "chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357"}, + {file = "chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d"}, + {file = "chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb"}, + {file = "chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131"}, + {file = "chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531"}, + {file = "chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4"}, + {file = "chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea"}, + {file = "chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7"}, + {file = "chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93"}, + {file = "chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c"}, + {file = "chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e"}, + {file = "chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11"}, + {file = "chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c"}, + {file = "chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04"}, + {file = "chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e"}, + {file = "chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56"}, ] [[package]] @@ -245,14 +291,14 @@ files = [ [[package]] name = "click" -version = "8.1.7" +version = "8.3.3" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"}, + {file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"}, ] [package.dependencies] @@ -310,7 +356,7 @@ files = [ wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools ; python_version >= \"3.12\"", "sphinx (<2)", "tox"] [[package]] name = "distlib" @@ -324,6 +370,18 @@ files = [ {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] +[[package]] +name = "docutils" +version = "0.22.4" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}, + {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, +] + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -351,7 +409,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "fqdn" @@ -407,7 +465,7 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" +markers = "python_version == \"3.12\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -515,6 +573,18 @@ 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.5.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, + {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -615,37 +685,6 @@ files = [ [package.dependencies] hbreader = "*" -[[package]] -name = "jsonpatch" -version = "1.33" -description = "Apply JSON-Patches (RFC 6902)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -groups = ["main"] -files = [ - {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, - {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, -] - -[package.dependencies] -jsonpointer = ">=1.9" - -[[package]] -name = "jsonpath-ng" -version = "1.7.0" -description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, - {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, - {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, -] - -[package.dependencies] -ply = "*" - [[package]] name = "jsonpointer" version = "3.0.0" @@ -660,14 +699,14 @@ files = [ [[package]] name = "jsonschema" -version = "4.23.0" +version = "4.26.0" description = "An implementation of JSON Schema validation for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, ] [package.dependencies] @@ -676,17 +715,38 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format\""} idna = {version = "*", optional = true, markers = "extra == \"format\""} isoduration = {version = "*", optional = true, markers = "extra == \"format\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} -rpds-py = ">=0.7.1" +rpds-py = ">=0.25.0" uri-template = {version = "*", optional = true, markers = "extra == \"format\""} webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format\""} [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-path" +version = "0.5.0" +description = "JSONSchema Spec with object-oriented paths" +optional = false +python-versions = "<4.0.0,>=3.10" +groups = ["main"] +files = [ + {file = "jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2"}, + {file = "jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +pathable = ">=0.6.0,<0.7.0" +PyYAML = ">=5.1" +referencing = "<0.38.0" + +[package.extras] +requests = ["requests (>=2.31.0,<3.0.0)"] [[package]] name = "jsonschema-specifications" @@ -704,82 +764,115 @@ files = [ referencing = ">=0.31.0" [[package]] -name = "linkml" -version = "1.8.5" -description = "Linked Open Data Modeling Language" +name = "lazy-object-proxy" +version = "1.12.0" +description = "A fast and thorough lazy object proxy." optional = false -python-versions = "<4.0.0,>=3.8.1" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "linkml-1.8.5-py3-none-any.whl", hash = "sha256:5a45577a4bb380f3a128f45764545cb2da92dd3310a110de7dc5355796d5ac43"}, - {file = "linkml-1.8.5.tar.gz", hash = "sha256:8f31834560ade4b7f1aebc973d22b31951d7061643d32bdcba258a650db9b140"}, + {file = "lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519"}, + {file = "lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6"}, + {file = "lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b"}, + {file = "lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8"}, + {file = "lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8"}, + {file = "lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab"}, + {file = "lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff"}, + {file = "lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad"}, + {file = "lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00"}, + {file = "lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508"}, + {file = "lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa"}, + {file = "lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370"}, + {file = "lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede"}, + {file = "lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9"}, + {file = "lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0"}, + {file = "lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308"}, + {file = "lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23"}, + {file = "lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac"}, + {file = "lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f"}, + {file = "lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3"}, + {file = "lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a"}, + {file = "lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a"}, + {file = "lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95"}, + {file = "lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5"}, + {file = "lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f"}, + {file = "lazy_object_proxy-1.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae575ad9b674d0029fc077c5231b3bc6b433a3d1a62a8c363df96974b5534728"}, + {file = "lazy_object_proxy-1.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31020c84005d3daa4cc0fa5a310af2066efe6b0d82aeebf9ab199292652ff036"}, + {file = "lazy_object_proxy-1.12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800f32b00a47c27446a2b767df7538e6c66a3488632c402b4fb2224f9794f3c0"}, + {file = "lazy_object_proxy-1.12.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:15400b18893f345857b9e18b9bd87bd06aba84af6ed086187add70aeaa3f93f1"}, + {file = "lazy_object_proxy-1.12.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3d3964fbd326578bcdfffd017ef101b6fb0484f34e731fe060ba9b8816498c36"}, + {file = "lazy_object_proxy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:424a8ab6695400845c39f13c685050eab69fa0bbac5790b201cd27375e5e41d7"}, + {file = "lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402"}, + {file = "lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61"}, ] +[[package]] +name = "linkml" +version = "1.11.0.post183.dev0+e5d97c45a" +description = "Linked Open Data Modeling Language" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [] +develop = false + [package.dependencies] antlr4-python3-runtime = ">=4.9.0,<4.10" -click = ">=7.0" +click = ">=8.2" graphviz = ">=0.10.1" hbreader = "*" isodate = ">=0.6.0" jinja2 = ">=3.1.0" jsonasobj2 = ">=1.0.3,<2.0.0" jsonschema = {version = ">=4.0.0", extras = ["format"]} -linkml-dataops = "*" -linkml-runtime = ">=1.8.1,<2.0.0" +linkml-runtime = ">=1.10.0,<2.0.0" +openapi-spec-validator = ">=0.8.4" openpyxl = "*" parse = "*" prefixcommons = ">=0.1.7" prefixmaps = ">=0.2.2" -pydantic = ">=1.0.0,<3.0.0" -pyjsg = ">=0.11.6" -pyshex = ">=0.7.20" -pyshexc = ">=0.8.3" +pydantic = ">=2.0.0,<3.0.0" +pyjsg = ">=0.12.3" +pyshex = ">=0.9.0" +pyshexc = ">=0.10.3" python-dateutil = "*" pyyaml = "*" rdflib = ">=6.0.0" requests = ">=2.22" +sphinx-click = ">=6.0.0" sqlalchemy = ">=1.4.31" watchdog = ">=0.9.0" -[package.extras] -black = ["black (>=24.0.0)"] -numpydantic = ["numpydantic (>=1.6.1)"] -shacl = ["pyshacl (>=0.25.0,<0.26.0)"] -tests = ["black (>=24.0.0)", "numpydantic (>=1.6.1)", "pyshacl (>=0.25.0,<0.26.0)"] - -[[package]] -name = "linkml-dataops" -version = "0.1.0" -description = "LinkML Data Operations API" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "linkml_dataops-0.1.0-py3-none-any.whl", hash = "sha256:193cf7f659e5f07946d2c2761896910d5f7151d91282543b1363801f68307f4c"}, - {file = "linkml_dataops-0.1.0.tar.gz", hash = "sha256:4550eab65e78b70dc3b9c651724a94ac2b1d1edb2fbe576465f1d6951a54ed04"}, -] - -[package.dependencies] -jinja2 = "*" -jsonpatch = "*" -jsonpath-ng = "*" -linkml-runtime = ">=1.1.6" -"ruamel.yaml" = "*" +[package.source] +type = "git" +url = "https://github.com/linkml/linkml.git" +reference = "HEAD" +resolved_reference = "e5d97c45a6b84e0cb5fbb9665a965c4b0c70194f" +subdirectory = "packages/linkml" [[package]] name = "linkml-runtime" -version = "1.8.3" +version = "1.11.0.post183.dev0+e5d97c45a" description = "Runtime environment for LinkML, the Linked open data modeling language" optional = false -python-versions = "<4.0,>=3.8" +python-versions = ">=3.10" groups = ["main"] -files = [ - {file = "linkml_runtime-1.8.3-py3-none-any.whl", hash = "sha256:0750920f1348fffa903d99e7b5834ce425a2a538285aff9068dbd96d05caabd1"}, - {file = "linkml_runtime-1.8.3.tar.gz", hash = "sha256:5b7f682eef54aaf0a59c50eeacdb11463b43b124a044caf496cde59936ac05c8"}, -] +files = [] +develop = false [package.dependencies] -click = "*" +click = ">=8.2" curies = ">=0.5.4" deprecated = "*" hbreader = "*" @@ -789,10 +882,21 @@ jsonschema = ">=3.2.0" prefixcommons = ">=0.1.12" prefixmaps = ">=0.1.4" pydantic = ">=1.10.2,<3.0.0" +pyoxigraph = ">=0.5.6" pyyaml = "*" rdflib = ">=6.0.0" requests = "*" +[package.extras] +dev = ["coverage", "requests-cache (>=1.3.2)"] + +[package.source] +type = "git" +url = "https://github.com/linkml/linkml.git" +reference = "HEAD" +resolved_reference = "e5d97c45a6b84e0cb5fbb9665a965c4b0c70194f" +subdirectory = "packages/linkml_runtime" + [[package]] name = "markdown" version = "3.7" @@ -921,7 +1025,7 @@ watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-get-deps" @@ -998,6 +1102,49 @@ files = [ {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, ] +[[package]] +name = "openapi-schema-validator" +version = "0.9.0" +description = "OpenAPI schema validation for Python" +optional = false +python-versions = "<4.0.0,>=3.10.0" +groups = ["main"] +files = [ + {file = "openapi_schema_validator-0.9.0-py3-none-any.whl", hash = "sha256:faa3bbe7c3aa8ca2087ad83f709dc3b7d920283153a570c03e24ea182558aa25"}, + {file = "openapi_schema_validator-0.9.0.tar.gz", hash = "sha256:b72db64315b89d21834cd3ffef37e3e6893bc876327be2d366e8424b1029afd3"}, +] + +[package.dependencies] +jsonschema = ">=4.19.1,<5.0.0" +jsonschema-specifications = ">=2024.10.1" +pydantic = ">=2.0.0,<3.0.0" +pydantic-settings = ">=2.0.0,<3.0.0" +referencing = ">=0.37.0,<0.38.0" +rfc3339-validator = "*" + +[package.extras] +ecma-regex = ["regress (>=2025.10.1)"] + +[[package]] +name = "openapi-spec-validator" +version = "0.9.0" +description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "openapi_spec_validator-0.9.0-py3-none-any.whl", hash = "sha256:222fecffc7714f6d0a6ad62c0e4b66cc2b7dbfafb7b93acfc6c308abbdb51af8"}, + {file = "openapi_spec_validator-0.9.0.tar.gz", hash = "sha256:6d648cff6490ebb799dcfe273792f2941c050158854c721f086599d845da78b8"}, +] + +[package.dependencies] +jsonschema = ">=4.26.0,<5.0.0" +jsonschema-path = ">=0.5.0,<0.6.0" +lazy-object-proxy = ">=1.7.1,<2.0" +openapi-schema-validator = ">=0.9.0,<0.10.0" +pydantic = ">=2.0.0,<3.0.0" +pydantic-settings = ">=2.0.0,<3.0.0" + [[package]] name = "openpyxl" version = "3.1.5" @@ -1053,6 +1200,18 @@ files = [ {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, ] +[[package]] +name = "pathable" +version = "0.6.0" +description = "Object-oriented paths" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566"}, + {file = "pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58"}, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1098,18 +1257,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "ply" -version = "3.11" -description = "Python Lex & Yacc" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, - {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, -] - [[package]] name = "prefixcommons" version = "0.1.12" @@ -1163,7 +1310,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -1278,6 +1425,30 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-settings" +version = "2.14.2" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440"}, + {file = "pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "types-boto3[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pygments" version = "2.18.0" @@ -1295,19 +1466,20 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjsg" -version = "0.11.10" +version = "0.12.4" description = "Python JSON Schema Grammar interpreter" optional = false -python-versions = "*" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "PyJSG-0.11.10-py3-none-any.whl", hash = "sha256:10af60ff42219be7e85bf7f11c19b648715b0b29eb2ddbd269e87069a7c3f26d"}, - {file = "PyJSG-0.11.10.tar.gz", hash = "sha256:4bd6e3ff2833fa2b395bbe803a2d72a5f0bab5b7285bccd0da1a1bc0aee88bfa"}, + {file = "pyjsg-0.12.4-py3-none-any.whl", hash = "sha256:a57ae58bfd7192b32654a0024bc6462fb459d54e837f0b2b5cff0726aad2e557"}, + {file = "pyjsg-0.12.4.tar.gz", hash = "sha256:bb1c0ff1f50846d2b5185b182e28b0b6978eae51a2078ce3eb1e0f28dea7b9ab"}, ] [package.dependencies] antlr4-python3-runtime = ">=4.9.3,<4.10.0" jsonasobj = ">=1.2.1" +requests = "*" [[package]] name = "pymdown-extensions" @@ -1328,6 +1500,51 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.12)"] +[[package]] +name = "pyoxigraph" +version = "0.5.9" +description = "Python bindings of Oxigraph, a SPARQL database and RDF toolkit" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyoxigraph-0.5.9-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c711156407663e2182e4ea07c959f8e471f1b6ecaee1f00ce3accca7a53d9917"}, + {file = "pyoxigraph-0.5.9-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:70ac4792acee8c86f795b0db785b467afbb02daf58e2beb6e6ef3c3f43f4c222"}, + {file = "pyoxigraph-0.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:95347d64417299f91128ccfee486dcb14d2c6674a9b9a62e5c6978b651a2ccf2"}, + {file = "pyoxigraph-0.5.9-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:baffb41d914b761b06cde61eeb0a35dd5f0fa4808f71ae9902fdd1179e70e553"}, + {file = "pyoxigraph-0.5.9-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:56b78aab5a5688ede88404372574785ba74e8d82b2cd1c0b0623a03b7069967f"}, + {file = "pyoxigraph-0.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:eee3db30ecb6836fdc05ddcbc6aa79ed521afcbfa707a8561b7e5891c4fb4ff8"}, + {file = "pyoxigraph-0.5.9-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6ab699861035163e89bc512ce20aa6e91b654e4d33114c9f5facab08f0fe3d7e"}, + {file = "pyoxigraph-0.5.9-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5a8a1b2debadb5fe79f8b89cbe1193e9c0e6fc1cf0c9431b6be706234beeabbe"}, + {file = "pyoxigraph-0.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:d04806073905f448a48811b217115e71224be7f1d4075d1f5f5ec07a016f42ae"}, + {file = "pyoxigraph-0.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:379bef7f8fc38f638f358b1e12bc5bdc908a0e7d47157f4399bf103c727a66da"}, + {file = "pyoxigraph-0.5.9-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bcac65148bddcd0ae24ee1bf20a2e89cc225a926b9e9996eb64dcce60400d1a3"}, + {file = "pyoxigraph-0.5.9-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:57f3619c7860f4c95ddab077e4a3dedb7ca4cf191bd81096db835264d414ec5b"}, + {file = "pyoxigraph-0.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:4f8ff48b873157ab38e2595a56d6d2008471a45853f5fffc645658c6f69c07db"}, + {file = "pyoxigraph-0.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:b829233ea4445ccd1032d02e9189432a77e16888a79313498aa501b8731dc925"}, + {file = "pyoxigraph-0.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9ca7cd7666336fcbafd9a2ec7d598dd859b7bd2ca7b0838a0f7b92dd3828c28"}, + {file = "pyoxigraph-0.5.9-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f9154bea122c0bab11eda7604b27ceb424ab8ba1637250503008b8c6632ea405"}, + {file = "pyoxigraph-0.5.9-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4558d430bbad6e6b4ba98e0e89a2e28402211069d38e6e9b00083ae2d9d2d175"}, + {file = "pyoxigraph-0.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:79caf78136a8312e506beb607910cc5a93662a05a173aa9b560ce9d08801384f"}, + {file = "pyoxigraph-0.5.9-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:917d976dcb813d613d0ddd7da1c9dec6ad02ee815015f393c703cf0804946653"}, + {file = "pyoxigraph-0.5.9-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:68f8daf082ea4bf9583abd10b64e23cd2c4a3285338a5ec24254181d45e63083"}, + {file = "pyoxigraph-0.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:b8884b0ce3ccbac99ebc2c995614a13dc5f5d86b0adb847f36aeb1c713d12946"}, + {file = "pyoxigraph-0.5.9-cp38-abi3-macosx_10_14_x86_64.whl", hash = "sha256:8b998bc479a54a8905cdeaad621d0f7fed212abf9f1cbededfde4c51fc8e3bb8"}, + {file = "pyoxigraph-0.5.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:5c9f93db5e14a03ac1e3934cece3fb6f7c0a9f4bde33082c72c788c12bf65ba4"}, + {file = "pyoxigraph-0.5.9-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe19bd1835a7245caad06cc9bb1a5c861882dc074fdfa24ba2626e3bbf9866a"}, + {file = "pyoxigraph-0.5.9-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3bd1925a8185320bcb8c549cccafbb423cc3957513890e6f420b8e355055052a"}, + {file = "pyoxigraph-0.5.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f619aac7199b2ba91cade2fe69f64b4c73abb1e6b33735b0ff7205a753e609cd"}, + {file = "pyoxigraph-0.5.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:09071f6c08b9489723dec96e2d96f64a15b9be2d165d3bbf67d40712884ba7a3"}, + {file = "pyoxigraph-0.5.9-cp38-abi3-win_amd64.whl", hash = "sha256:efd3d03bd2a36f9b0bdf3ce70d76ce5278c481fe961d14c2bb6efcac10f57ae2"}, + {file = "pyoxigraph-0.5.9-cp38-abi3-win_arm64.whl", hash = "sha256:94c2a8b52c1ed6e445a235a4f89cd460eea936f399d28df5e9927826bf52f032"}, + {file = "pyoxigraph-0.5.9-cp38-cp38-win_amd64.whl", hash = "sha256:276ca12ca2cc20b123812af78a489d35f871f1ee5579d9f98d6c65f038fcd9ee"}, + {file = "pyoxigraph-0.5.9-cp39-cp39-win_amd64.whl", hash = "sha256:dd3a801b56c383cf4b078bd51cc1b86498b1a1f6e3e2f56406a00c3239fc97ca"}, + {file = "pyoxigraph-0.5.9-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f39a6175a80a55c837981d4d68f42380071bb1d45af124de488fcc8a61a81af3"}, + {file = "pyoxigraph-0.5.9-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71dba053e5efc0002fbd4ace3119b9d3aec8a6c5b164ed7409a2429bf71171b8"}, + {file = "pyoxigraph-0.5.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:70ffb46ae49f52b18a49c3fb63d906ae9c189de4fe4dbf5453279bda4d27af4e"}, + {file = "pyoxigraph-0.5.9.tar.gz", hash = "sha256:fe2bea0f41f5284b6dad99ea718d7ff03600068cdf8736b63a9e6cd05f056b19"}, +] + [[package]] name = "pyparsing" version = "3.2.0" @@ -1364,45 +1581,45 @@ testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytes [[package]] name = "pyshex" -version = "0.8.1" -description = "Python ShEx Implementation" +version = "0.9.0" +description = "Python ShEx interpreter" optional = false -python-versions = ">=3.6" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "PyShEx-0.8.1-py3-none-any.whl", hash = "sha256:6da1b10123e191abf8dcb6bf3e54aa3e1fcf771df5d1a0ed453217c8900c8e6a"}, - {file = "PyShEx-0.8.1.tar.gz", hash = "sha256:3c5c4d45fe27faaadae803cb008c41acf8ee784da7868b04fd84967e75be70d0"}, + {file = "pyshex-0.9.0-py3-none-any.whl", hash = "sha256:d81344deed686b7c169f23156221ae281225e2ba02b14fe9810335afdefffa9d"}, + {file = "pyshex-0.9.0.tar.gz", hash = "sha256:87288b5e5613f734f55f0085334558218ff618fb1061aabdcee19841092b3eca"}, ] [package.dependencies] cfgraph = ">=0.2.1" chardet = "*" -pyshexc = "0.9.1" +pyshexc = ">=0.10.3" rdflib-shim = "*" requests = ">=2.22.0" -shexjsg = ">=0.8.2" +shexjsg = ">=0.9.0" sparqlslurper = ">=0.5.1" sparqlwrapper = ">=1.8.5" urllib3 = "*" [[package]] name = "pyshexc" -version = "0.9.1" +version = "0.10.3.post1" description = "PyShExC - Python ShEx compiler" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "PyShExC-0.9.1-py2.py3-none-any.whl", hash = "sha256:efc55ed5cb2453e9df569b03e282505e96bb06597934288f3b23dd980ef10028"}, - {file = "PyShExC-0.9.1.tar.gz", hash = "sha256:35a9975d4b9afeb20ef710fb6680871756381d0c39fbb5470b3b506581a304d3"}, + {file = "pyshexc-0.10.3.post1-py3-none-any.whl", hash = "sha256:5d247f2822ef9864152545935d93a07dce66640608ea9414c96f69da7fe7a168"}, + {file = "pyshexc-0.10.3.post1.tar.gz", hash = "sha256:80d9d067c80af9a796e3c1c47d2207edf2e9a9fc39d3ca0ce5dd2019334ea915"}, ] [package.dependencies] antlr4-python3-runtime = ">=4.9.3,<4.10.0" -chardet = "*" +chardet = ">=7.4.1" jsonasobj = ">=1.2.1" pyjsg = ">=0.11.10" -rdflib-shim = "*" +rdflib-shim = ">=1.0.3" shexjsg = ">=0.8.1" [[package]] @@ -1455,6 +1672,21 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.2.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pytrie" version = "0.4.0" @@ -1603,19 +1835,20 @@ rdflib-jsonld = "0.6.1" [[package]] name = "referencing" -version = "0.35.1" +version = "0.37.0" description = "JSON Referencing + Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, - {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, ] [package.dependencies] attrs = ">=22.2.0" rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "regex" @@ -1771,195 +2004,157 @@ files = [ ] [[package]] -name = "rpds-py" -version = "0.21.0" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "rpds_py-0.21.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a017f813f24b9df929674d0332a374d40d7f0162b326562daae8066b502d0590"}, - {file = "rpds_py-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:20cc1ed0bcc86d8e1a7e968cce15be45178fd16e2ff656a243145e0b439bd250"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad116dda078d0bc4886cb7840e19811562acdc7a8e296ea6ec37e70326c1b41c"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:808f1ac7cf3b44f81c9475475ceb221f982ef548e44e024ad5f9e7060649540e"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de552f4a1916e520f2703ec474d2b4d3f86d41f353e7680b597512ffe7eac5d0"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efec946f331349dfc4ae9d0e034c263ddde19414fe5128580f512619abed05f1"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b80b4690bbff51a034bfde9c9f6bf9357f0a8c61f548942b80f7b66356508bf5"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085ed25baac88953d4283e5b5bd094b155075bb40d07c29c4f073e10623f9f2e"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8efac2a1273eed2354397a51216ae1e198ecbce9036fba4e7610b308b6153"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:95a5bad1ac8a5c77b4e658671642e4af3707f095d2b78a1fdd08af0dfb647624"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3e53861b29a13d5b70116ea4230b5f0f3547b2c222c5daa090eb7c9c82d7f664"}, - {file = "rpds_py-0.21.0-cp310-none-win32.whl", hash = "sha256:ea3a6ac4d74820c98fcc9da4a57847ad2cc36475a8bd9683f32ab6d47a2bd682"}, - {file = "rpds_py-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:b8f107395f2f1d151181880b69a2869c69e87ec079c49c0016ab96860b6acbe5"}, - {file = "rpds_py-0.21.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5555db3e618a77034954b9dc547eae94166391a98eb867905ec8fcbce1308d95"}, - {file = "rpds_py-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97ef67d9bbc3e15584c2f3c74bcf064af36336c10d2e21a2131e123ce0f924c9"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab2c2a26d2f69cdf833174f4d9d86118edc781ad9a8fa13970b527bf8236027"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e8921a259f54bfbc755c5bbd60c82bb2339ae0324163f32868f63f0ebb873d9"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a7ff941004d74d55a47f916afc38494bd1cfd4b53c482b77c03147c91ac0ac3"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5145282a7cd2ac16ea0dc46b82167754d5e103a05614b724457cffe614f25bd8"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de609a6f1b682f70bb7163da745ee815d8f230d97276db049ab447767466a09d"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40c91c6e34cf016fa8e6b59d75e3dbe354830777fcfd74c58b279dceb7975b75"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d2132377f9deef0c4db89e65e8bb28644ff75a18df5293e132a8d67748397b9f"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0a9e0759e7be10109645a9fddaaad0619d58c9bf30a3f248a2ea57a7c417173a"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e20da3957bdf7824afdd4b6eeb29510e83e026473e04952dca565170cd1ecc8"}, - {file = "rpds_py-0.21.0-cp311-none-win32.whl", hash = "sha256:f71009b0d5e94c0e86533c0b27ed7cacc1239cb51c178fd239c3cfefefb0400a"}, - {file = "rpds_py-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:e168afe6bf6ab7ab46c8c375606298784ecbe3ba31c0980b7dcbb9631dcba97e"}, - {file = "rpds_py-0.21.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d"}, - {file = "rpds_py-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11"}, - {file = "rpds_py-0.21.0-cp312-none-win32.whl", hash = "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952"}, - {file = "rpds_py-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd"}, - {file = "rpds_py-0.21.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937"}, - {file = "rpds_py-0.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976"}, - {file = "rpds_py-0.21.0-cp313-none-win32.whl", hash = "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202"}, - {file = "rpds_py-0.21.0-cp313-none-win_amd64.whl", hash = "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e"}, - {file = "rpds_py-0.21.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2c51d99c30091f72a3c5d126fad26236c3f75716b8b5e5cf8effb18889ced928"}, - {file = "rpds_py-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbd7504a10b0955ea287114f003b7ad62330c9e65ba012c6223dba646f6ffd05"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dcc4949be728ede49e6244eabd04064336012b37f5c2200e8ec8eb2988b209c"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f414da5c51bf350e4b7960644617c130140423882305f7574b6cf65a3081cecb"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9afe42102b40007f588666bc7de82451e10c6788f6f70984629db193849dced1"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b929c2bb6e29ab31f12a1117c39f7e6d6450419ab7464a4ea9b0b417174f044"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8404b3717da03cbf773a1d275d01fec84ea007754ed380f63dfc24fb76ce4592"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e12bb09678f38b7597b8346983d2323a6482dcd59e423d9448108c1be37cac9d"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58a0e345be4b18e6b8501d3b0aa540dad90caeed814c515e5206bb2ec26736fd"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3761f62fcfccf0864cc4665b6e7c3f0c626f0380b41b8bd1ce322103fa3ef87"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c2b2f71c6ad6c2e4fc9ed9401080badd1469fa9889657ec3abea42a3d6b2e1ed"}, - {file = "rpds_py-0.21.0-cp39-none-win32.whl", hash = "sha256:b21747f79f360e790525e6f6438c7569ddbfb1b3197b9e65043f25c3c9b489d8"}, - {file = "rpds_py-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:0626238a43152918f9e72ede9a3b6ccc9e299adc8ade0d67c5e142d564c9a83d"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6b4ef7725386dc0762857097f6b7266a6cdd62bfd209664da6712cb26acef035"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6bc0e697d4d79ab1aacbf20ee5f0df80359ecf55db33ff41481cf3e24f206919"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da52d62a96e61c1c444f3998c434e8b263c384f6d68aca8274d2e08d1906325c"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98e4fe5db40db87ce1c65031463a760ec7906ab230ad2249b4572c2fc3ef1f9f"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30bdc973f10d28e0337f71d202ff29345320f8bc49a31c90e6c257e1ccef4333"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:faa5e8496c530f9c71f2b4e1c49758b06e5f4055e17144906245c99fa6d45356"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32eb88c30b6a4f0605508023b7141d043a79b14acb3b969aa0b4f99b25bc7d4a"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a89a8ce9e4e75aeb7fa5d8ad0f3fecdee813802592f4f46a15754dcb2fd6b061"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:241e6c125568493f553c3d0fdbb38c74babf54b45cef86439d4cd97ff8feb34d"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:3b766a9f57663396e4f34f5140b3595b233a7b146e94777b97a8413a1da1be18"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:af4a644bf890f56e41e74be7d34e9511e4954894d544ec6b8efe1e21a1a8da6c"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:031819f906bb146561af051c7cef4ba2003d28cff07efacef59da973ff7969ba"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b876f2bc27ab5954e2fd88890c071bd0ed18b9c50f6ec3de3c50a5ece612f7a6"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc5695c321e518d9f03b7ea6abb5ea3af4567766f9852ad1560f501b17588c7b"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4de1da871b5c0fd5537b26a6fc6814c3cc05cabe0c941db6e9044ffbb12f04a"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:878f6fea96621fda5303a2867887686d7a198d9e0f8a40be100a63f5d60c88c9"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8eeec67590e94189f434c6d11c426892e396ae59e4801d17a93ac96b8c02a6c"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff2eba7f6c0cb523d7e9cff0903f2fe1feff8f0b2ceb6bd71c0e20a4dcee271"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a429b99337062877d7875e4ff1a51fe788424d522bd64a8c0a20ef3021fdb6ed"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d167e4dbbdac48bd58893c7e446684ad5d425b407f9336e04ab52e8b9194e2ed"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:4eb2de8a147ffe0626bfdc275fc6563aa7bf4b6db59cf0d44f0ccd6ca625a24e"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e78868e98f34f34a88e23ee9ccaeeec460e4eaf6db16d51d7a9b883e5e785a5e"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4991ca61656e3160cdaca4851151fd3f4a92e9eba5c7a530ab030d6aee96ec89"}, - {file = "rpds_py-0.21.0.tar.gz", hash = "sha256:ed6378c9d66d0de903763e7706383d60c33829581f0adff47b6535f1802fa6db"}, -] - -[[package]] -name = "ruamel-yaml" -version = "0.18.6" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +name = "roman-numerals" +version = "4.1.0" +description = "Manipulate well-formed Roman numerals" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, - {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, + {file = "roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"}, + {file = "roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"}, ] -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} - -[package.extras] -docs = ["mercurial (>5.7)", "ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - [[package]] -name = "ruamel-yaml-clib" -version = "0.2.12" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +name = "rpds-py" +version = "2026.6.3" +description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" -files = [ - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, - {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826"}, + {file = "rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4"}, ] [[package]] name = "shexjsg" -version = "0.8.2" -description = "ShExJSG - Astract Syntax Tree for the ShEx 2.0 language" +version = "0.9.0" +description = "ShExJSG - Astract Syntax Tree Definition for the ShEx 2.0 language" optional = false -python-versions = "*" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "ShExJSG-0.8.2-py2.py3-none-any.whl", hash = "sha256:3b0d8432dd313bee9e1343382c5e02e9908dd941a7dd7342bf8c0200fe523766"}, - {file = "ShExJSG-0.8.2.tar.gz", hash = "sha256:f17a629fc577fa344382bdee143cd9ff86588537f9f811f66cea6f63cdbcd0b6"}, + {file = "shexjsg-0.9.0-py3-none-any.whl", hash = "sha256:abf18db2d9895bc46740f68ae699b2ccfe08c783f6e0c038e6077293ad01c0a5"}, + {file = "shexjsg-0.9.0.tar.gz", hash = "sha256:750016fabdb5487b27e2e714145f3602cd3ac4eb0dd9b7d7751d0cde62c0d1d8"}, ] [package.dependencies] -pyjsg = ">=0.11.10" +pyjsg = ">=0.12.3" [[package]] name = "six" @@ -1973,6 +2168,18 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[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"}, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -2023,6 +2230,158 @@ docs = ["sphinx (<5)", "sphinx-rtd-theme"] keepalive = ["keepalive (>=0.5)"] pandas = ["pandas (>=1.3.5)"] +[[package]] +name = "sphinx" +version = "9.1.0" +description = "Python documentation generator" +optional = false +python-versions = ">=3.12" +groups = ["main"] +files = [ + {file = "sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978"}, + {file = "sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.21,<0.23" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +roman-numerals = ">=1.0.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" + +[[package]] +name = "sphinx-click" +version = "6.2.0" +description = "Sphinx extension that automatically documents click applications" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "sphinx_click-6.2.0-py3-none-any.whl", hash = "sha256:1fb1851cb4f2c286d43cbcd57f55db6ef5a8d208bfc3370f19adde232e5803d7"}, + {file = "sphinx_click-6.2.0.tar.gz", hash = "sha256:fc78b4154a4e5159462e36de55b8643747da6cda86b3b52a8bb62289e603776c"}, +] + +[package.dependencies] +click = ">=8.0" +docutils = "*" +sphinx = ">=4.0" + +[package.extras] +docs = ["reno"] +test = ["pytest", "pytest-cov"] + +[[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-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 = "sqlalchemy" version = "2.0.36" @@ -2169,6 +2528,21 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "uri-template" version = "1.3.0" @@ -2197,7 +2571,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2221,7 +2595,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "watchdog" @@ -2355,5 +2729,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = ">=3.12,<4.0" -content-hash = "af27ef8edba6d8bb3c5b19ab7675d604bf134266a66f4ceaa25a9ed8b1391d53" +python-versions = ">=3.12,<3.14" +content-hash = "322c15b52a911b24f53280d3fd3dc6cbb1f071c02468e7a3f5a10303ed1af298" diff --git a/pyproject.toml b/pyproject.toml index abb64ebb..ba353258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,13 +10,16 @@ description = "Margo Specification" authors = [] license = "Open Web Foundation - CLA Copyright Grant 0.9" readme = "README.md" -requires-python = ">=3.12,<4.0" +requires-python = ">=3.12,<3.14" dependencies = [ 'mkdocs>=1.6.1', 'mkdocs-markdownextradata-plugin>=0.2.6', 'mkdocs-material>=9.5.44', - 'linkml>=1.8.5', + 'linkml @ git+https://github.com/linkml/linkml.git#subdirectory=packages/linkml', + 'linkml-runtime @ git+https://github.com/linkml/linkml.git#subdirectory=packages/linkml_runtime', + "openapi-spec-validator (>=0.9.0,<0.10.0)", ] +# 'linkml>=1.11.1', [tool.poetry] requires-poetry = ">=2.0" diff --git a/system-design/css/margo.css b/system-design/css/margo.css deleted file mode 100644 index a7ad33f3..00000000 --- a/system-design/css/margo.css +++ /dev/null @@ -1,4 +0,0 @@ -[data-md-color-scheme="margo"] { - --md-primary-fg-color: hsla(218, 99%, 28%, 1); - --md-footer-bg-color: hsla(218, 99%, 28%, 1); -} diff --git a/system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml b/system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml index 1670e0e1..d2711b26 100644 --- a/system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml +++ b/system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml @@ -396,403 +396,1330 @@ components: description: > Base64-encoded payload signature using SHA-256 and device certificate. Format: public_key;digital_signature + + # Modifications compared to pre-data-model stage: + # - ManifestVersion INLINED into UnsignedAppStateManifest(OpenAPI)/DesiredStateManifest(LinkML) + # - appDeploymentParams INLINED into appDeploymentSpec(OpenAPI)/Spec(LinkML) schemas: - ManifestVersion: - type: number - description: > - Monotonically increasing unsigned 64-bit integer in the inclusive range [1, 2^64-1]. - Prevents rollback attacks. The first manifest MUST use 1. + Component: + type: object + additionalProperties: true + description: A class representing a component of a deployment profile. + properties: + name: + type: string + description: A unique name used to identify the component package. For helm + installations the name will be used as the chart name. The name must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. + properties: + $ref: '#/components/schemas/ComponentProperties' + description: A dictionary element specifying the component packages's deployment + details. See the [Component Properties](#componentproperties-attributes) + section below. + required: + - name + - properties + ComponentProperties: + type: object + additionalProperties: true + description: Properties dictionary for component deployment details. + properties: + repository: + type: string + description: Repository location for the component. + revision: + type: string + description: Revision version for the component. + wait: + type: boolean + description: If True, indicates the device waits for the component installation + to complete. + timeout: + type: string + description: Time to wait for component installation to complete, formatted + as "##m##s". + packageLocation: + type: string + description: URL indicating the Compose package's location. + keyLocation: + type: string + description: URL for the public key used to validate a digitally signed package. + helmApplicationDeploymentProfileComponent: + type: object + additionalProperties: true + description: '' + properties: + name: + type: string + description: A unique name used to identify the component package. For helm + installations the name will be used as the chart name. The name must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. + properties: + $ref: '#/components/schemas/ComponentProperties' + description: A dictionary element specifying the component packages's deployment + details. See the [Component Properties](#componentproperties-attributes) + section below. + required: + - name + - properties + composeApplicationDeploymentProfileComponent: + type: object + additionalProperties: true + description: '' + properties: + name: + type: string + description: A unique name used to identify the component package. For helm + installations the name will be used as the chart name. The name must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. + properties: + $ref: '#/components/schemas/ComponentProperties' + description: A dictionary element specifying the component packages's deployment + details. See the [Component Properties](#componentproperties-attributes) + section below. + required: + - name + - properties + appParameterTarget: + type: object + additionalProperties: true + description: Specifies where the parameter applies in the deployment. + properties: + pointer: + type: string + description: The name of the parameter in the deployment configuration. For + Helm deployments, this is the dot notation for the matching element in the + `values.yaml` file. This follows the same naming convention you would use + with the `--set` command line argument with the `helm install` command. For + compose deployments, this is the name of the environment variable to set. + components: + type: array + items: + type: string + description: Indicates which deployment profile [component](#component-attributes + the parameter target applies to. The component name specified here MUST match + a component name in the [deployment profiles](#deploymentprofile-attributes) + section. + required: + - pointer + - components + DeploymentAnnotations: + type: object + additionalProperties: true + description: A class representing annotations. + properties: + applicationId: + type: string + description: An identifier for the application. The id is used to help create + unique identifiers where required, such as namespaces. The id must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. The id MUST NOT be more than 200 characters. + The applicationId MUST match the associated application package Metadata "id" + attribute. + pattern: ^[-a-z0-9]{1,200}$ + id: + type: string + description: The unique identifier UUID of the deployment specification. Needs + to be assigned by the Workload Orchestration Software. + pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ + required: + - applicationId + - id + appDeploymentMetadata: + type: object + additionalProperties: true + description: Metadata associated with the desired state. + properties: + annotations: + $ref: '#/components/schemas/DeploymentAnnotations' + description: Defines the application ID and unique identifier associated to + the deployment specification. Needs to be assigned by the Workload Orchestration + Software. See the [Annotation Attributes](#annotations-attributes) section + below. + name: + type: string + description: When deploying to Kubernetes, the manifests name. The name is chosen + by the workload orchestration vendor and is not displayed anywhere. + namespace: + type: string + description: When deploying to Kubernetes, the namespace the manifest is added + under. The namespace is chosen by the workload orchestration solution vendor. + deviceId: + type: string + pattern: ^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*(\/\*)?$ + required: + - annotations + - name + - namespace + - deviceId + appDeploymentProfile: + type: object + additionalProperties: true + description: Represents a deployment configuration for the application. + properties: + type: + type: string + description: Defines the type of this deployment configuration for the application. The + allowed values are `helm.v3`, to indicate the deployment profile's format + is Helm version 3, and `compose` to indicate the deployment profile's format + is a Compose file. When installing the application on a device supporting + the Kubernetes platform, all `helm.v3` components, and only `helm.v3` components, + will be provided to the device in same order they are listed in the application + description file. When installing the application on a device supporting + Compose, all `compose` components, and only `compose` components, will be + provided to the device in the same order they are listed in the application + description file. The device will install the components in the same order + they are listed in the application description file. + pattern: ^(helm\.v3|compose)$ + components: + type: array + items: + $ref: '#/components/schemas/Component' + description: Component element indicating the components to deploy when installing + the application. See the [Component](#component-attributes) section below. + required: + - type + - components + Parameter: + type: object + additionalProperties: true + description: Defines a configurable parameter for the application. + properties: + name: + type: string + description: Name of the parameter. + value: + description: The parameter's default value. Accepted data types are string, + integer, double, boolean, array[string], array[integer], array[double], array[boolean]. + anyOf: + - type: boolean + - type: integer + - type: number + - type: string + targets: + type: array + items: + $ref: '#/components/schemas/appParameterTarget' + description: Used to indicate which component the value should be applied to + when installing, or updating, the application. See the [Target](#target-attributes) + section below. + required: + - name + - targets + appDeploymentSpec: + type: object + additionalProperties: true + description: Specification details of the desired state. + properties: + deploymentProfile: + $ref: '#/components/schemas/appDeploymentProfile' + description: Section that defines deployment details including type and components. + parameters: + type: object + additionalProperties: + anyOf: + - $ref: '#/components/schemas/Parameter__identifier_optional' + - type: array + items: + $ref: '#/components/schemas/appParameterTarget' + description: Used to indicate which component the value should be applied + to when installing, or updating, the application. See the [Target](#target-attributes) + section below. + description: Describes the configured parameters applied via the end-user. + required: + - deploymentProfile + - parameters + CPU: + type: object + additionalProperties: true + description: CPU element specifying the CPU requirements for the application. + properties: + cores: + type: number + description: The required amount of CPU cores the application must use to run + in its full functionality. Specified as decimal units of CPU cores (e.g., + `0.5` is half a core). This is defined by the application developer. After + deployment of the application, the device MUST provide this number of CPU + cores for the application. + architectures: + type: array + items: + type: string + description: Permissible CPU architecture values. + enum: + - amd64 + - x86_64 + - arm64 + - arm + - riscv64 + - other + description: The CPU architectures supported by the application. This can be + e.g. amd64, x86_64, arm64, arm. See the [CpuArchitectureType](#cpuarchitecturetype) + definition for all permissible values. Multiple arcitecture types can be specified, + as the deployment profile may support multiple CPU architectures. + required: + - cores + DeviceCommunicationInterface: + type: object + additionalProperties: true + description: Communication interface of a device. + properties: + type: + type: string + description: Permissible communication interface types. + enum: + - ethernet + - wifi + - cellular + - bluetooth + - usb + - canbus + - rs232 + - other + required: + - type + DevicePeripheral: + type: object + additionalProperties: true + description: Peripheral hardware of a device. + properties: + type: + type: string + description: Permissible peripheral types. + enum: + - gpu + - display + - camera + - microphone + - speaker + - other + manufacturer: + type: string + description: The name of the manufacturer. If `manufacturer` is specified as + a requirement here, it may be difficult to find devices that can host the application. + Please use these requirements with caution. + model: + type: string + description: The model of the peripheral. If `model` is specified as a requirement + here, it may be difficult to find devices that can host the application. Please + use these requirements with caution. + required: + - type + ApplicationMetadata: + type: object + additionalProperties: true + description: Metadata about the application. + properties: + id: + type: string + description: An identifier for the application. The id is used to help create + unique identifiers where required, such as namespaces. The id must be lower + case letters and numbers and MAY contain dashes. Uppercase letters, underscores + and periods MUST NOT be used. The id MUST NOT be more than 200 characters. + pattern: ^[a-z0-9-]{1,200}$ + name: + type: string + description: The application's official name. This name is for display purposes + only and can container whitespace and special characters. + description: + type: string + version: + type: string + description: The application's version. + catalog: + $ref: '#/components/schemas/Catalog' + description: Catalog element specifying the application's metadata for enabling + its discovery. See the [Catalog](#catalog-attributes) section below. + required: + - id + - name + - version + - catalog + Author: + type: object + additionalProperties: true + description: Information about the application's author. + properties: + name: + type: string + description: The name of the application's creator. + email: + type: string + description: Email address of the application's creator. + pattern: .*@[a-z0-9.-]* + Catalog: + type: object + additionalProperties: true + description: Catalog metadata for displaying the application. + properties: + application: + $ref: '#/components/schemas/CatalogApplicationMetadata' + description: Application element specifying the application specific metadata. + See the [Application Metadata](#applicationmetadata-attributes) section below. + author: + type: array + items: + $ref: '#/components/schemas/Author' + description: Author element specifying metadata about the application's author. + See the [Author Metadata](#author-attributes) section below. + organization: + type: array + items: + $ref: '#/components/schemas/Organization' + description: Organization element specifying metadata about the organization/company + providing the application. See the [Organization Metadata](#organization-attributes) + section below. + required: + - organization + CatalogApplicationMetadata: + type: object + additionalProperties: true + description: Metadata specific to the application. + properties: + descriptionFile: + type: string + description: Link to the file containing the application's full description. + The file should be a markdown file. + icon: + type: string + description: Link to the icon file (e.g., in PNG format). + licenseFile: + type: string + description: Link to the file that details the application's license. The file + should either be a plain text, markdown or PDF file. + releaseNotes: + type: string + description: Statement about the changes for this application's release. The + file should either be a markdown or PDF file. + site: + type: string + description: Link to the application's website. + tagline: + type: string + description: The application's slogan. + tags: + type: array + items: + type: string + description: An array of strings that can be used to provide additional context + for the application in a user interface to assist with task such as categorizing, + searching, etc. + Configuration: + type: object + additionalProperties: true + description: Configuration layout and validation rules. + properties: + sections: + type: array + items: + $ref: '#/components/schemas/Section' + description: Sections are used to group related parameters together, so it is + possible to present a user interface with a logical grouping of the parameters + in each section. See the [Section](#section-attributes) section below. + schema: + type: array + items: + $ref: '#/components/schemas/Schema' + description: Schema is used to provide details about how to validate each parameter + value. At a minimum, the parameter value must be validated to match the schema's + data type. The schema indicates additional rules the provided value must satisfy + to be considered valid input. See the [Schema](#schema-attributes) section + below. + required: + - sections + - schema + DeploymentProfileDescription: + type: object + additionalProperties: true + description: Represents a deployment configuration for the application. + properties: + id: + type: string + description: An identifier for the deployment profile, given by the application + developer, used to uniquely identify this deployment profile from others within + this application description's scope. + description: + type: string + description: This human-readable description of a deployment profile allows + for providing additional context about the deployment profile. E.g., the application + developer can use this to describe the deployment profile's purpose, such + as the intended use case. Additionally, the application developer can use + this to provide further details about the resources, peripherals, and interfaces + required to run the application. + requiredResources: + $ref: '#/components/schemas/Resources' + description: Required resources element specifying the resources required to + install the application. See the [Required Resources](#requiredresources-attributes) + section below. The consequences (e.g., aborting / blocking the installation + or execution of the application) of not meeting these required resources are + not defined (yet) by margo. + type: + type: string + description: Defines the type of this deployment configuration for the application. The + allowed values are `helm.v3`, to indicate the deployment profile's format + is Helm version 3, and `compose` to indicate the deployment profile's format + is a Compose file. When installing the application on a device supporting + the Kubernetes platform, all `helm.v3` components, and only `helm.v3` components, + will be provided to the device in same order they are listed in the application + description file. When installing the application on a device supporting + Compose, all `compose` components, and only `compose` components, will be + provided to the device in the same order they are listed in the application + description file. The device will install the components in the same order + they are listed in the application description file. + pattern: ^(helm\.v3|compose)$ + components: + type: array + items: + $ref: '#/components/schemas/Component' + description: Component element indicating the components to deploy when installing + the application. See the [Component](#component-attributes) section below. + required: + - id + - type + - components + Organization: + type: object + additionalProperties: true + description: Information about the providing organization. + properties: + name: + type: string + description: Organization responsible for the application's development and + distribution. + site: + type: string + description: Link to the organization's website. + required: + - name + Resources: + type: object + additionalProperties: true + description: Required resources element specifying the resources required to install + the application. + properties: + cpu: + $ref: '#/components/schemas/CPU' + description: CPU element specifying the CPU requirements for the application. + See the [CPU](#cpu-attributes) section below. + memory: + type: string + description: The minimum amount of memory required. The value is given in binary + units (`Ki` = Kibibytes, `Mi` = Mebibytes, `Gi` = Gibibytes). This is defined + by the application developer. After deployment of the application, the device + MUST provide this amount of memory for the application. + pattern: ^[0-9]+(Mi|Gi|Ki)$ + storage: + type: string + description: The amount of storage required for the application to run. This + encompasses the installed application and the data it needs to store. The + value is given in binary units (`Ki` = Kibibytes, `Mi` = Mebibytes, `Gi` = + Gibibytes, `Ti` Tebibytes, `Pi` = Pebibytes, `Ei` = Exbibytes). This is defined + by the application developer. After deployment of the application, the device + MUST provide this amount of storage for the application + pattern: ^[0-9]+(Mi|Gi|Ki|Ti|Pi|Ei)$ + peripherals: + type: array + items: + $ref: '#/components/schemas/DevicePeripheral' + description: Peripherals element specifying the peripherals required to run + the application. See the [Peripheral](#peripheral-attributes) section below. + interfaces: + type: array + items: + $ref: '#/components/schemas/DeviceCommunicationInterface' + description: Interfaces element specifying the communication interfaces required + to run the application. See the [Communication Interfaces](#communicationinterface-attributes) + section below. + Schema: + type: object + additionalProperties: true + description: Defines data type and rules for validating user provided parameter + values. Subclasses (see below) define for each data type their own set of validation + rules that can be used. The value MUST be validated against all rules defined + in the schema. + properties: + name: + type: string + description: The name of the schema rule. This used in the [setting](#setting-attributes) + to link the setting to the schema rule. + dataType: + type: string + description: Indicates the expected data type for the user provided value. Accepted + values are string, integer, double, boolean, array[string], array[integer], + array[double], array[boolean]. At a minimum, the provided parameter value + MUST match the schema's data type if no other validation rules are provided. + required: + - name + - dataType + Section: + type: object + additionalProperties: true + description: Named sections within the configuration layout. + properties: + name: + type: string + description: The name of the section. This may be used in the user interface + to show the grouping of the associated parameters within the section. + settings: + type: array + items: + $ref: '#/components/schemas/Setting' + description: Settings are used to provide instructions to the workload orchestration + software vendor for displaying parameters to the user. A user MUST be able + to provide values for all settings. See the [Setting](#setting-attributes) + section below. + required: + - name + - settings + Setting: + type: object + additionalProperties: true + description: Individual configuration settings. + properties: + parameter: + type: string + description: The name of the [parameter](#parameter-attributes) the setting + is associated with. + name: + type: string + description: The parameter's display name to show in the user interface. + description: + type: string + description: The parameters's short description to provide additional context + to the user in the user interface about what the parameter is for. + immutable: + type: boolean + description: If true, indicates the parameter value MUST not be changed once + it has been set and used to install the application. Default is false if not + provided. + schema: + type: string + description: The name of the schema definition to use to validate the parameter's + value. See the [Schema](#schema-attributes) section below. + required: + - parameter + - name + - schema DeploymentBundleRef: - type: [object, 'null'] - description: > - Describes a single archive containing all ApplicationDeployment documents. If there are zero deployments (deployments array is empty) the property MUST be present with the value null (it MUST NOT be omitted). + type: object + additionalProperties: true + description: Describes an archive containing all referenced ApplicationDeployment + YAMLs. If there are zero deployments (i.e., the deployments array is empty), this + field MUST be present with the value null. An empty archive MUST NOT be served. properties: mediaType: type: string - description: > - MUST be application/vnd.margo.bundle.v1+tar+gzip; a gzip-compressed tar whose root contains one or more ApplicationDeployment YAML files. If there are zero deployments then bundle MUST be null (an empty archive MUST NOT be served). The archive MUST contain exactly the set of YAML files referenced by deployments. + description: MUST be application/vnd.margo.bundle.v1+tar+gzip, which denotes + a gzip-compressed tar archive (commonly delivered as a .tar.gz) whose root + contains one or more ApplicationDeployment YAML files. Servers MUST set the + HTTP Content-Type to this media type. The archive MUST contain exactly the + set of YAML files referenced by deployments. + const: application/vnd.margo.bundle.v1+tar+gzip digest: type: string - description: > - The digest of the bundle archive. MUST equal the digest computed over the exact sequence of bytes (per Exact Bytes Rule) in the bundle endpoint's HTTP 200 OK response body. + description: Digest of the bundle archive. MUST equal the digest computed over + the exact sequence of bytes in the bundle endpoint's HTTP 200 OK response + body. See Protocol - Digest for further details. + pattern: ^[a-zA-Z0-9_-]+:[0-9a-fA-F]+$ sizeBytes: - type: number - description: > - Unsigned 64-bit advisory estimate of the decoded payload length in bytes for the bundle archive. Provided for bandwidth estimation and update planning. MUST NOT be used for integrity; digest verification remains mandatory. + type: integer + description: Optional unsigned 64-bit advisory estimate of the decoded payload + length in bytes for the bundle archive. Provided for bandwidth estimation + and update planning. MUST NOT be used for integrity verification. + minimum: 0 url: type: string - description: > - Content-addressable retrieval endpoint of the form /api/v1/clients/{clientId}/bundles/{digest} where {digest} equals bundle.digest. + format: uri + description: Content-addressable retrieval endpoint for the bundle of the form + /api/v1/clients/{clientId}/bundles/{digest} where {digest} equals bundle.digest. + pattern: ^/[a-zA-Z0-9-._~:/?#\[\]@!$&'()*+,;=]+$ + required: + - mediaType + - digest + - url DeploymentManifestRef: type: object - description: > - Reference to a deployment manifest with content addressing and integrity verification. - required: - - deploymentId - - digest - - url + additionalProperties: true + description: Reference to an individual ApplicationDeployment within the desired + state manifest. properties: deploymentId: type: string - description: > - Unique identifier for the application deployment. + description: The UUID of the deployment. MUST equal metadata.annotations.id + in the ApplicationDeployment. digest: type: string - description: > - The digest of the individual ApplicationDeployment YAML file. MUST equal the digest computed over the exact sequence of bytes (per Exact Bytes Rule) in that deployment endpoint's HTTP 200 OK response body. + description: "Digest of the corresponding ApplicationDeployment YAML file.\n\ + \ MUST equal the digest computed over the exact sequence of bytes in the individual\ + \ deployment endpoint's HTTP 200 OK response body.\n See Protocol - Digest\ + \ for further details." + pattern: ^[a-zA-Z0-9_-]+:[0-9a-fA-F]+$ sizeBytes: - type: number - description: > - Unsigned 64-bit advisory estimate of the decoded payload length in bytes for the deployment YAML. Provided for planning or progress display. MUST NOT be used for integrity; digest verification remains mandatory. + type: integer + description: Optional unsigned 64-bit advisory estimate of the decoded payload + length in bytes for the ApplicationDeployment YAML. Provided for bandwidth + estimation and update planning. MUST NOT be used for integrity verification. + minimum: 0 url: type: string - description: > - Content-addressable endpoint of the form /api/v1/clients/{clientId}/deployments/{deploymentId}/{digest}. The {digest} MUST equal deployments[].digest; the referenced resource is immutable - UnsignedAppStateManifest: - type: object + format: uri + description: Content-addressable retrieval endpoint for the ApplicationDeployment + YAML of the form /api/v1/clients/{clientId}/deployments/{deploymentId}/{digest} + where {digest} equals deployments[].digest. + pattern: ^/[a-zA-Z0-9-._~:/?#\[\]@!$&'()*+,;=]+$ required: - - manifestVersion - - bundle - - bundle.mediaType - - bundle.digest - - bundle.url - - deployments - properties: - manifestVersion: - $ref: '#/components/schemas/ManifestVersion' - bundle: - $ref: '#/components/schemas/DeploymentBundleRef' - deployments: - type: array - description: A list of deployment object references for the device. The reference contains some meta info and reference to the url where the deployment is available. - items: - $ref: '#/components/schemas/DeploymentManifestRef' - DeviceCapabilitiesManifest: + - deploymentId + - digest + - url + Properties: type: object - required: [apiVersion, kind, properties] - patternProperties: - '^x-[a-z][a-z0-9-]*-extensions$': - type: object - description: >- - Vendor-specific extension. Keys MUST match x--extensions - where matches [a-z][a-z0-9-]*. - additionalProperties: false + additionalProperties: true + description: Device properties reported to the WFM. properties: - apiVersion: + id: type: string - kind: + description: Unique deviceID assigned to the device via the Device Owner. + vendor: type: string - enum: [DeviceCapabilitiesManifest] - properties: - type: object - required: [id, vendor, modelNumber, serialNumber, roles] - properties: - id: - $ref: '#/components/schemas/DeviceId' - vendor: - type: string - modelNumber: - type: string - serialNumber: - type: string - roles: - type: array - items: - type: string - enum: [Standalone Cluster, Cluster Leader, Standalone Device, Gateway] - resources: - type: object - required: [cpu, memory, storage, peripherals, interfaces] - properties: - cpu: - type: object - required: [cores] - properties: - cores: - type: number - architecture: - type: string - enum: [amd64, arm64, arm] - memory: - type: string - storage: - type: string - peripherals: - type: array - items: - $ref: '#/components/schemas/DevicePeripheral' - interfaces: - type: array - items: - $ref: '#/components/schemas/DeviceCommunicationInterface' - DeviceId: - # format: "{id}[/{id}[/{id}...]]" - # Top-level id is required and must include only unreserved characters as specified in RFC3986. - # Subsequent ids are only used when referencing child devices, and must include only unreserved characters as specified in RFC3986 when present. - type: string - pattern: '^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*$' - DeviceId_with_asterisk: - # format: "{id}[/{id}[/{id}...]/*]" - # Top-level id is required and must include only unreserved characters as specified in RFC3986. - # Subsequent ids are only used when referencing child devices, and must include only unreserved characters as specified in RFC3986 when present. - type: string - pattern: '^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*(\/\*)?$' - + description: Defines the device vendor. + modelNumber: + type: string + description: Defines the model number of the device. + serialNumber: + type: string + description: Defines the serial number of the device. + roles: + type: array + items: + type: string + description: Role a device can provide to the Margo environment. + enum: + - Standalone Cluster + - Standalone Device + - Cluster Leader + - Gateway + description: 'Element that defines the device role it can provide to the Margo + environment. MUST be one of the following: Standalone Cluster, Cluster Leader, + Standalone Device, or Gateway.' + resources: + $ref: '#/components/schemas/Resources' + description: Element that defines the device's resources available to the application + deployed on the device. See the [Resources Attributes](#resources-attributes) + section below. + required: + - id + - vendor + - modelNumber + - serialNumber + - roles + - resources ComponentStatus: type: object - required: [name, state] + additionalProperties: true + description: Status of a component deployment. properties: name: type: string - state: + state: &id001 type: string - enum: [pending, installing, installed, failed, removing, removed] + description: Permissible deployment states. + enum: + - pending + - installing + - installed + - failed + - removing + - removed error: - type: object - properties: - code: - type: string - source: - type: string - message: - type: string - - DeploymentStatusManifest: + $ref: '#/components/schemas/Error' + description: Optional error details when the state is `failed`. + required: + - name + - state + Error: type: object - required: [apiVersion, kind, deploymentId, status, components] + additionalProperties: true + description: Error details associated with a failed deployment state. properties: - apiVersion: + code: type: string - kind: + source: type: string - enum: [DeploymentStatusManifest] - deploymentId: + description: Identifies the source of the error. It is set to the device id, + with its full hierarchy if applicable, of the device generating the error, + or to the component name of the component generating the error. + message: type: string - deviceId: - $ref: '#/components/schemas/DeviceId' - status: - type: object - required: [state] - properties: - state: - type: string - enum: [pending, installing, installed, failed, removing, removed] - error: - type: object - properties: - code: - type: string - source: - type: string - message: - type: string - components: - type: array - items: - $ref: '#/components/schemas/ComponentStatus' - - DevicePeripheral: + Status: type: object - required: [type] + additionalProperties: true + description: Overall deployment state and optional error details. + properties: + state: *id001 + error: + $ref: '#/components/schemas/Error' + description: Optional error details when the state is `failed`. + required: + - state + HelmDeploymentProfile: + type: object + additionalProperties: true + description: '' properties: type: type: string - enum: [gpu, display, camera, microphone, speaker] - manufacturer: - type: string - model: - type: string - - DeviceCommunicationInterface: + description: Defines the type of this deployment configuration for the application. The + allowed values are `helm.v3`, to indicate the deployment profile's format + is Helm version 3, and `compose` to indicate the deployment profile's format + is a Compose file. When installing the application on a device supporting + the Kubernetes platform, all `helm.v3` components, and only `helm.v3` components, + will be provided to the device in same order they are listed in the application + description file. When installing the application on a device supporting + Compose, all `compose` components, and only `compose` components, will be + provided to the device in the same order they are listed in the application + description file. The device will install the components in the same order + they are listed in the application description file. + pattern: ^(helm\.v3|compose)$ + const: helm.v3 + components: + type: array + items: + $ref: '#/components/schemas/helmApplicationDeploymentProfileComponent' + description: Component element indicating the components to deploy when installing + the application. See the [Component](#component-attributes) section below. + required: + - type + - components + ComposeDeploymentProfile: type: object - required: [type] + additionalProperties: true + description: '' properties: type: type: string - enum: [ethernet, wifi, cellular, bluetooth, usb, canbus, rs232] - - # app deployment struct added here for ease of programming, the code generators will generate the structs - # for the actual app deployment yaml and parsing would be easy to do + description: Defines the type of this deployment configuration for the application. The + allowed values are `helm.v3`, to indicate the deployment profile's format + is Helm version 3, and `compose` to indicate the deployment profile's format + is a Compose file. When installing the application on a device supporting + the Kubernetes platform, all `helm.v3` components, and only `helm.v3` components, + will be provided to the device in same order they are listed in the application + description file. When installing the application on a device supporting + Compose, all `compose` components, and only `compose` components, will be + provided to the device in the same order they are listed in the application + description file. The device will install the components in the same order + they are listed in the application description file. + pattern: ^(helm\.v3|compose)$ + const: compose + components: + type: array + items: + $ref: '#/components/schemas/composeApplicationDeploymentProfileComponent' + description: Component element indicating the components to deploy when installing + the application. See the [Component](#component-attributes) section below. + required: + - type + - components appDeploymentManifest: type: object - description: Application Deployment manifest - required: [apiVersion, kind, metadata, spec] + additionalProperties: true + description: A class representing the desired state of an entity. properties: apiVersion: type: string - default: margo.org - description: API version + description: Identifier of the version of the API the object definition follows. kind: type: string - default: ApplicationDeployment - description: Resource kind - id: - type: string - description: Unique identifier for the application deployment + description: Must be `ApplicationDeployment`. + const: ApplicationDeployment + enum: + - ApplicationDeployment metadata: $ref: '#/components/schemas/appDeploymentMetadata' + description: Metadata element specifying characteristics about the application + deployment. See the [Metadata Attributes](#metadata-attributes) section below. spec: $ref: '#/components/schemas/appDeploymentSpec' - appDeploymentMetadata: + description: Spec element that defines deployment profile and parameters associated + with the application deployment. See the [Spec Attributes](#spec-attributes) + section below. + required: + - apiVersion + - kind + - metadata + - spec + Parameter__identifier_optional: type: object - required: [annotations, name, namespace, deviceId] + additionalProperties: true + description: Defines a configurable parameter for the application. properties: name: type: string - description: Name of the resource - namespace: - type: string - description: Namespace of the resource - deviceId: - $ref: '#/components/schemas/DeviceId_with_asterisk' - description: Device ID of the target device for the deployment - labels: - type: object - additionalProperties: { type: string } - description: Labels for the resource - helmApplicationDeploymentProfileComponent: + description: Name of the parameter. + value: + description: The parameter's default value. Accepted data types are string, + integer, double, boolean, array[string], array[integer], array[double], array[boolean]. + anyOf: + - type: boolean + - type: integer + - type: number + - type: string + targets: + type: array + items: + $ref: '#/components/schemas/appParameterTarget' + description: Used to indicate which component the value should be applied to + when installing, or updating, the application. See the [Target](#target-attributes) + section below. + required: + - targets + ApplicationDescription: type: object - description: Helm Application Deployment Profile Component - required: [name, properties] - patternProperties: - '^x-[a-z][a-z0-9-]*-extensions$': - type: object - description: >- - Vendor-specific extension. Keys MUST match x--extensions - where matches [a-z][a-z0-9-]*. - additionalProperties: false + additionalProperties: true + description: Root class for an application description. properties: - name: + apiVersion: type: string - description: Name of the component - properties: + description: Identifier of the version of the API the object definition follows. + kind: + type: string + description: Specifies the object type; must be `ApplicationDescription`. + const: ApplicationDescription + enum: + - ApplicationDescription + metadata: + $ref: '#/components/schemas/ApplicationMetadata' + description: Metadata element specifying characteristics about the application + deployment. See the [Metadata Attributes](#metadata-attributes) section below. + deploymentProfiles: + type: array + items: + $ref: '#/components/schemas/DeploymentProfileDescription' + description: Deployment profiles element specifying the types of deployments + the application supports. See the [Deployment](#deploymentprofile-attributes) + section below. + parameters: type: object - required: [repository] - properties: - repository: - type: string - description: Repository of the component - revision: - type: string - description: Revision of the component - timeout: - type: string - description: Timeout for the component - wait: - type: boolean - description: Wait for the component to be ready - composeApplicationDeploymentProfileComponent: + additionalProperties: + anyOf: + - $ref: '#/components/schemas/Parameter__identifier_optional' + - type: array + items: + $ref: '#/components/schemas/appParameterTarget' + description: Used to indicate which component the value should be applied + to when installing, or updating, the application. See the [Target](#target-attributes) + section below. + description: Parameters element specifying the configurable parameters to use + when installing, or updating, the application. See the [Parameter](#parameter-attributes) + section below. + configuration: + $ref: '#/components/schemas/Configuration' + description: Configuration element specifying how parameters should be displayed + to the user for setting the value as well as the rules to use to validate + the user's input. See the [Configuration](#configuration-attributes) section + below. + required: + - apiVersion + - kind + - metadata + - deploymentProfiles + HelmDeploymentProfileDescription: type: object - description: Compose Application Deployment Profile Component - required: [name, properties] - patternProperties: - '^x-[a-z][a-z0-9-]*-extensions$': - type: object - description: >- - Vendor-specific extension. Keys MUST match x--extensions - where matches [a-z][a-z0-9-]*. - additionalProperties: false + additionalProperties: true + description: '' properties: - name: + id: type: string - description: Name of the component - properties: - type: object - required: [packageLocation] - properties: - packageLocation: - type: string - description: The URL indicating the Compose package's location. It should be a direct path to the compose.yaml or compose.yaml file archived in tar.gz - keyLocation: - type: string - description: Key location of the component - timeout: - type: string - description: Timeout for the component - wait: - type: boolean - description: Wait for the component to be ready - appDeploymentProfile: + description: An identifier for the deployment profile, given by the application + developer, used to uniquely identify this deployment profile from others within + this application description's scope. + description: + type: string + description: This human-readable description of a deployment profile allows + for providing additional context about the deployment profile. E.g., the application + developer can use this to describe the deployment profile's purpose, such + as the intended use case. Additionally, the application developer can use + this to provide further details about the resources, peripherals, and interfaces + required to run the application. + requiredResources: + $ref: '#/components/schemas/Resources' + description: Required resources element specifying the resources required to + install the application. See the [Required Resources](#requiredresources-attributes) + section below. The consequences (e.g., aborting / blocking the installation + or execution of the application) of not meeting these required resources are + not defined (yet) by margo. + type: + type: string + description: Defines the type of this deployment configuration for the application. The + allowed values are `helm.v3`, to indicate the deployment profile's format + is Helm version 3, and `compose` to indicate the deployment profile's format + is a Compose file. When installing the application on a device supporting + the Kubernetes platform, all `helm.v3` components, and only `helm.v3` components, + will be provided to the device in same order they are listed in the application + description file. When installing the application on a device supporting + Compose, all `compose` components, and only `compose` components, will be + provided to the device in the same order they are listed in the application + description file. The device will install the components in the same order + they are listed in the application description file. + pattern: ^(helm\.v3|compose)$ + const: helm.v3 + components: + type: array + items: + $ref: '#/components/schemas/helmApplicationDeploymentProfileComponent' + description: Component element indicating the components to deploy when installing + the application. See the [Component](#component-attributes) section below. + required: + - id + - type + - components + ComposeDeploymentProfileDescription: type: object - description: Application Deployment Profile - required: [type, components] - patternProperties: - '^x-[a-z][a-z0-9-]*-extensions$': - type: object - description: >- - Vendor-specific extension. Keys MUST match x--extensions - where matches [a-z][a-z0-9-]*. - additionalProperties: false + additionalProperties: true + description: '' properties: + id: + type: string + description: An identifier for the deployment profile, given by the application + developer, used to uniquely identify this deployment profile from others within + this application description's scope. + description: + type: string + description: This human-readable description of a deployment profile allows + for providing additional context about the deployment profile. E.g., the application + developer can use this to describe the deployment profile's purpose, such + as the intended use case. Additionally, the application developer can use + this to provide further details about the resources, peripherals, and interfaces + required to run the application. + requiredResources: + $ref: '#/components/schemas/Resources' + description: Required resources element specifying the resources required to + install the application. See the [Required Resources](#requiredresources-attributes) + section below. The consequences (e.g., aborting / blocking the installation + or execution of the application) of not meeting these required resources are + not defined (yet) by margo. type: type: string - enum: ["helm", "compose"] - description: Type of deployment profile + description: Defines the type of this deployment configuration for the application. The + allowed values are `helm.v3`, to indicate the deployment profile's format + is Helm version 3, and `compose` to indicate the deployment profile's format + is a Compose file. When installing the application on a device supporting + the Kubernetes platform, all `helm.v3` components, and only `helm.v3` components, + will be provided to the device in same order they are listed in the application + description file. When installing the application on a device supporting + Compose, all `compose` components, and only `compose` components, will be + provided to the device in the same order they are listed in the application + description file. The device will install the components in the same order + they are listed in the application description file. + pattern: ^(helm\.v3|compose)$ + const: compose components: type: array items: - oneOf: - - $ref: '#/components/schemas/helmApplicationDeploymentProfileComponent' - - $ref: '#/components/schemas/composeApplicationDeploymentProfileComponent' - description: Components of the deployment profile - appParameterTarget: + $ref: '#/components/schemas/composeApplicationDeploymentProfileComponent' + description: Component element indicating the components to deploy when installing + the application. See the [Component](#component-attributes) section below. + required: + - id + - type + - components + TextValidationSchema: type: object - description: Application Parameter Target - required: [pointer, components] + additionalProperties: true + description: Extends schema to define a string/text-specific set of validation rules + that can be used. properties: - pointer: + allowEmpty: + type: boolean + description: If true, indicates a value must be provided. Default is false if + not provided. + minLength: + type: integer + description: If set, indicates the minimum number of characters the value must + have to be considered valid. + maxLength: + type: integer + description: If set, indicates the maximum number of characters the value must + have to be considered valid. + regexMatch: type: string - description: Pointer to the parameter - components: + description: If set, indicates a regular expression to use to validate the value. + name: + type: string + description: The name of the schema rule. This used in the [setting](#setting-attributes) + to link the setting to the schema rule. + dataType: + type: string + description: Indicates the expected data type for the user provided value. Accepted + values are string, integer, double, boolean, array[string], array[integer], + array[double], array[boolean]. At a minimum, the provided parameter value + MUST match the schema's data type if no other validation rules are provided. + required: + - name + - dataType + BooleanValidationSchema: + type: object + additionalProperties: true + description: Extends schema to define a boolean-specific set of validation rules + that can be used. + properties: + allowEmpty: + type: boolean + description: If true, indicates a value must be provided. Default is false if + not provided. + name: + type: string + description: The name of the schema rule. This used in the [setting](#setting-attributes) + to link the setting to the schema rule. + dataType: + type: string + description: Indicates the expected data type for the user provided value. Accepted + values are string, integer, double, boolean, array[string], array[integer], + array[double], array[boolean]. At a minimum, the provided parameter value + MUST match the schema's data type if no other validation rules are provided. + required: + - name + - dataType + NumericIntegerValidationSchema: + type: object + additionalProperties: true + description: Extends schema to define a integer-specific set of validation rules + that can be used. + properties: + allowEmpty: + type: boolean + description: If true, indicates a value must be provided. Default is false if + not provided. + minValue: + type: integer + description: If set, indicates the minimum allowed integer value the value must + have to be considered valid. + maxValue: + type: integer + description: If set, indicates the maximum allowed integer value the value must + have to be considered valid. + name: + type: string + description: The name of the schema rule. This used in the [setting](#setting-attributes) + to link the setting to the schema rule. + dataType: + type: string + description: Indicates the expected data type for the user provided value. Accepted + values are string, integer, double, boolean, array[string], array[integer], + array[double], array[boolean]. At a minimum, the provided parameter value + MUST match the schema's data type if no other validation rules are provided. + required: + - name + - dataType + NumericDoubleValidationSchema: + type: object + additionalProperties: true + description: Extends schema to define a double-specific set of validation rules + that can be used. + properties: + allowEmpty: + type: boolean + description: If true, indicates a value must be provided. Default is false if + not provided. + minValue: + type: number + description: If set, indicates the minimum value to be considered valid. + maxValue: + type: number + description: If set, indicates the maximum value to be considered valid. + minPrecision: + type: integer + description: If set, indicates the minimum level of precision the value must + have to be considered valid. + maxPrecision: + type: integer + description: If set, indicates the maximum level of precision the value must + have to be considered valid. + name: + type: string + description: The name of the schema rule. This used in the [setting](#setting-attributes) + to link the setting to the schema rule. + dataType: + type: string + description: Indicates the expected data type for the user provided value. Accepted + values are string, integer, double, boolean, array[string], array[integer], + array[double], array[boolean]. At a minimum, the provided parameter value + MUST match the schema's data type if no other validation rules are provided. + required: + - name + - dataType + SelectValidationSchema: + type: object + additionalProperties: true + description: Extends schema to define a specific set of validation rules that can + be used for select options. + properties: + allowEmpty: + type: boolean + description: If true, indicates a value must be provided. Default is false if + not provided. + multiselect: + type: boolean + description: If true, indicates multiple values can be selected. If multiple + values can be selected the resulting value is an array of the selected values. + The default is false if not provided. + options: type: array items: type: string - description: Components of the parameter - appParameterValue: + description: "This provides the list of acceptable options the user can select\ + \ from. The data type for each option must match the parameter setting\u2019\ + s data type." + name: + type: string + description: The name of the schema rule. This used in the [setting](#setting-attributes) + to link the setting to the schema rule. + dataType: + type: string + description: Indicates the expected data type for the user provided value. Accepted + values are string, integer, double, boolean, array[string], array[integer], + array[double], array[boolean]. At a minimum, the provided parameter value + MUST match the schema's data type if no other validation rules are provided. + required: + - options + - name + - dataType + UnsignedAppStateManifest: type: object - description: Application Parameter Value - required: [value, targets] + additionalProperties: true + description: Manifest from the Workload Fleet Manager, representing the complete + desired workload configuration assigned to the device. properties: - value: - # type: object - description: Value of the parameter - additionalProperties: true - x-go-type: interface{} - targets: + manifestVersion: + type: integer + description: Monotonically increasing unsigned 64-bit integer in the inclusive + range [1, 2^64-1]. Prevents rollback attacks. + minimum: 1 + maximum: 18446744073709551615 + bundle: + $ref: '#/components/schemas/DeploymentBundleRef' + description: Package optimization containing multiple ApplicationDeployment + YAMLs. + deployments: type: array items: - $ref: '#/components/schemas/appParameterTarget' - description: Targets of the parameter - appDeploymentParams: + $ref: '#/components/schemas/DeploymentManifestRef' + description: List of deployment objects describing each workload. + required: + - manifestVersion + - bundle + - deployments + DeviceCapabilitiesManifest: type: object - description: Application Parameters - additionalProperties: - $ref: '#/components/schemas/appParameterValue' - appDeploymentSpec: + additionalProperties: true + description: Capabilities of a device on which applications can be deployed. + properties: + apiVersion: + type: string + description: Identifier of the version the API resource follows. + kind: + type: string + description: Must be `DeviceCapabilitiesManifest`. + const: DeviceCapabilitiesManifest + enum: + - DeviceCapabilitiesManifest + properties: + $ref: '#/components/schemas/Properties' + description: Element that defines characteristics about the device. See the + [Properties Attributes](#properties-attributes) section below. + required: + - apiVersion + - kind + - properties + DeviceResources: type: object - description: Application Deployment specification - required: [applicationId, deploymentProfile] - patternProperties: - '^x-[a-z][a-z0-9-]*-extensions$': - type: object - description: >- - Vendor-specific extension. Keys MUST match x--extensions - where matches [a-z][a-z0-9-]*. - additionalProperties: false + additionalProperties: true + description: '' properties: - applicationId: + cpu: + $ref: '#/components/schemas/CPU' + description: CPU element specifying the CPU requirements for the application. + See the [CPU](#cpu-attributes) section below. + memory: type: string - description: >- - An identifier for the application. - The id is used to help create unique identifiers where required, such as namespaces. - The id must be lower case letters and numbers and MAY contain dashes. - Uppercase letters, underscores and periods MUST NOT be used. - The id MUST NOT be more than 200 characters. - The applicationId MUST match the associated application description's top-level "id" attribute. - pattern: "^[-a-z0-9]{1,200}$" - deploymentProfile: - $ref: '#/components/schemas/appDeploymentProfile' - description: Deployment profile - parameters: - $ref: '#/components/schemas/appDeploymentParams' - description: Parameters for the deployment \ No newline at end of file + description: The minimum amount of memory required. The value is given in binary + units (`Ki` = Kibibytes, `Mi` = Mebibytes, `Gi` = Gibibytes). This is defined + by the application developer. After deployment of the application, the device + MUST provide this amount of memory for the application. + pattern: ^[0-9]+(Mi|Gi|Ki)$ + storage: + type: string + description: The amount of storage required for the application to run. This + encompasses the installed application and the data it needs to store. The + value is given in binary units (`Ki` = Kibibytes, `Mi` = Mebibytes, `Gi` = + Gibibytes, `Ti` Tebibytes, `Pi` = Pebibytes, `Ei` = Exbibytes). This is defined + by the application developer. After deployment of the application, the device + MUST provide this amount of storage for the application + pattern: ^[0-9]+(Mi|Gi|Ki|Ti|Pi|Ei)$ + peripherals: + type: array + items: + $ref: '#/components/schemas/DevicePeripheral' + description: Peripherals element specifying the peripherals required to run + the application. See the [Peripheral](#peripheral-attributes) section below. + interfaces: + type: array + items: + $ref: '#/components/schemas/DeviceCommunicationInterface' + description: Interfaces element specifying the communication interfaces required + to run the application. See the [Communication Interfaces](#communicationinterface-attributes) + section below. + required: + - cpu + - memory + - storage + - peripherals + - interfaces + DeploymentStatusManifest: + type: object + additionalProperties: true + description: Manifest sent by the device client to report the deployment status + of a workload. + properties: + apiVersion: + type: string + kind: + type: string + const: DeploymentStatusManifest + enum: + - DeploymentStatusManifest + deploymentId: + type: string + description: The unique identifier of the deployment whose status is being reported. + deviceId: + type: string + description: Id of the device hosting the deployment. Includes the full device + hierarchy if applicable. This attribute is required when reporting on behalf + of a child-device. + pattern: ^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*(\/\*)?$ + status: + $ref: '#/components/schemas/Status' + description: Overall status of the deployment. + components: + type: array + items: + $ref: '#/components/schemas/ComponentStatus' + description: Per-component status list. + required: + - apiVersion + - kind + - deploymentId + - status + - components + DeviceId_with_asterisk: + type: string + pattern: ^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*(\/\*)?$ + DeviceId: + type: string + pattern: ^[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*$ + description: 'Unique identifier of a device or device hierarchy. Format: "{id}[/{id}[/{id}...]]". + The top-level id is required and must include only unreserved characters as specified + in RFC3986. Subsequent ids indicate child devices in a gateway hierarchy and must + also use only unreserved characters.' diff --git a/tools/check-examples.bash b/tools/check-examples.bash new file mode 100755 index 00000000..40007832 --- /dev/null +++ b/tools/check-examples.bash @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +set -eu + +THIS_SCRIPT="$(readlink -f "${0}")" +THIS_DIR="$(dirname "${THIS_SCRIPT}")" +DOCS_GEN="${THIS_DIR}" +CONFIGS="${DOCS_GEN}/configurations" + +ROOT_DIR="$(dirname "${THIS_DIR}")" + +VERBOSITY="${1:-info}" + +debug() { + if [[ "$VERBOSITY" == "debug" ]]; then + echo "$1" + fi +} + +trace() { + if [[ "$VERBOSITY" == "debug" ]] || + [[ "$VERBOSITY" == "trace" ]]; then + echo "$1" + fi +} + +info() { + if [[ "$VERBOSITY" == "debug" ]] || + [[ "$VERBOSITY" == "trace" ]] || + [[ "$VERBOSITY" == "info" ]]; then + echo "$1" + fi +} + +if command -v poetry 2>&1 >/dev/null; then + RUN="poetry run" +else + if ! command -v linkml 2>&1 >/dev/null; then + echo "The command 'linkml' is missing" + exit 1 + fi + if ! command -v mkdocs 2>&1 >/dev/null; then + echo "The command 'mkdocs' is missing" + exit 1 + fi + RUN="" +fi + +check_spec() { + trace "********************************" + SPEC_ROOT="${ROOT_DIR}/$(jq -r '.root' "${CONFIGS}/$1")" + trace "Spec root folder: ${SPEC_ROOT}" + + if [[ ! -d "${SPEC_ROOT}" ]]; then + echo "🚨 Spec root folder does not exist: ${SPEC_ROOT}" + return 1 + fi + + TARGET_CLASS="$(jq -r '.targetclass' "${CONFIGS}/$1")" + info "Target class: ${TARGET_CLASS}" + + SCHEMA_FILE="$(jq -r '.schemafile' "${CONFIGS}/$1")" + trace "Schema file: ${SCHEMA_FILE}" + + EXAMPLES_DIR="${SPEC_ROOT}/examples/valid" + trace "Examples folder: ${EXAMPLES_DIR}" + FOUND_VALID=0 + for EXAMPLE in $(ls "${EXAMPLES_DIR}"/${TARGET_CLASS}-*.{yaml,json} 2>/dev/null); do + FOUND_VALID=1 + if result=$(${RUN} linkml validate \ + --schema "${SPEC_ROOT}/${SCHEMA_FILE}" \ + --target-class "${TARGET_CLASS}" \ + "${EXAMPLE}") && + [[ "${result}" == "No issues found" ]]; then + echo "✅ Valid example (${EXAMPLE})" + else + echo "🚨 Not valid example expected to be valid! (${EXAMPLE})" + echo " ERROR: $result" + return 1 + fi + done + + if [[ "${FOUND_VALID}" -eq 0 ]]; then + echo "⚠️ No valid examples found for ${TARGET_CLASS} in ${EXAMPLES_DIR}" + return 1 + fi + + COUNTEREXAMPLES_DIR="${SPEC_ROOT}/examples/invalid" + FOUND_INVALID=0 + for COUNTEREXAMPLE in $(ls "${COUNTEREXAMPLES_DIR}"/${TARGET_CLASS}-*.{yaml,json} 2>/dev/null); do + FOUND_INVALID=1 + if ! result=$(${RUN} linkml validate \ + --schema "${SPEC_ROOT}/${SCHEMA_FILE}" \ + --target-class "${TARGET_CLASS}" \ + "${COUNTEREXAMPLE}"); then + echo "✅ Validation of invalid example failed, as expected (${COUNTEREXAMPLE})" + debug "${result}" + else + echo "🚨 Validation of invalid example '${COUNTEREXAMPLE}' was expected to fail, but has succeeded." + echo "${result}" + return 1 + fi + done + + if [[ "${FOUND_INVALID}" -eq 0 ]]; then + echo "⚠️ No invalid examples found for ${TARGET_CLASS} in ${COUNTEREXAMPLES_DIR}" + return 1 + fi + + if false; then + # does not work due to following LinkML bugs: + # https://github.com/linkml/linkml/issues/2423 + # https://github.com/linkml/linkml/issues/2425 + ${RUN} linkml examples \ + --schema "${SPEC_ROOT}/application-description.linkml.yaml" \ + --input-directory "${SPEC_ROOT}/examples/valid" \ + --counter-example-input-directory "${SPEC_ROOT}/examples/invalid" \ + --output-directory "${SPEC_ROOT}/output" + fi +} + +OVERALL_RESULT=0 + +for spec in $(ls "${CONFIGS}"); do + if ! check_spec "${spec}"; then + OVERALL_RESULT=1 + fi +done + +exit "${OVERALL_RESULT}" diff --git a/tools/configurations/application-deployment.json b/tools/configurations/application-deployment.json new file mode 100644 index 00000000..96602d7a --- /dev/null +++ b/tools/configurations/application-deployment.json @@ -0,0 +1,6 @@ +{ + "root": "model", + "targetclass": "ApplicationDeployment", + "schemafile": "application-deployment.linkml.yaml", + "markdowndoc": "application-deployment.md" +} diff --git a/tools/configurations/application-description.json b/tools/configurations/application-description.json new file mode 100644 index 00000000..e72066b1 --- /dev/null +++ b/tools/configurations/application-description.json @@ -0,0 +1,6 @@ +{ + "root": "model", + "targetclass": "ApplicationDescription", + "schemafile": "application-description.linkml.yaml", + "markdowndoc": "application-description.md" +} diff --git a/tools/configurations/deployment-status.json b/tools/configurations/deployment-status.json new file mode 100644 index 00000000..c3cecd41 --- /dev/null +++ b/tools/configurations/deployment-status.json @@ -0,0 +1,6 @@ +{ + "root": "model", + "targetclass": "DeploymentStatusManifest", + "schemafile": "deployment-status.linkml.yaml", + "markdowndoc": "deployment-status.md" +} diff --git a/tools/configurations/desired-state-manifest.json b/tools/configurations/desired-state-manifest.json new file mode 100644 index 00000000..a25f5c33 --- /dev/null +++ b/tools/configurations/desired-state-manifest.json @@ -0,0 +1,6 @@ +{ + "root": "model", + "targetclass": "DesiredStateManifest", + "schemafile": "desired-state-manifest.linkml.yaml", + "markdowndoc": "desired-state-manifest.md" +} diff --git a/tools/configurations/device-capabilities.json b/tools/configurations/device-capabilities.json new file mode 100644 index 00000000..e1d10814 --- /dev/null +++ b/tools/configurations/device-capabilities.json @@ -0,0 +1,6 @@ +{ + "root": "model", + "targetclass": "DeviceCapabilitiesManifest", + "schemafile": "device-capabilities.linkml.yaml", + "markdowndoc": "device-capabilities.md" +} diff --git a/tools/generate-all.bash b/tools/generate-all.bash new file mode 100755 index 00000000..8caec966 --- /dev/null +++ b/tools/generate-all.bash @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -eu + +THIS_SCRIPT="$(readlink -f "${0}")" +THIS_DIR="$(dirname "${THIS_SCRIPT}")" + +echo "Generate Class Diagrams" +"${THIS_DIR}/generate-class-diagram.bash" +echo "Generate JSON Schemas" +"${THIS_DIR}/generate-json-schemas.bash" +echo "Generate OpenAPI specification" +"${THIS_DIR}/generate-openapi.bash" +echo "Generate Documentation" +"${THIS_DIR}/generate-docs.bash" +echo "Done" diff --git a/tools/generate-class-diagram.bash b/tools/generate-class-diagram.bash new file mode 100755 index 00000000..ed31b11b --- /dev/null +++ b/tools/generate-class-diagram.bash @@ -0,0 +1,108 @@ +#!/usr/bin/env bash + +TMP_PLANTUML_FILE=$(mktemp) + +cleanup() { + rm "${TMP_PLANTUML_FILE}" +} + +trap cleanup EXIT + +set -eu + +THIS_SCRIPT="$(readlink -f "${0}")" +THIS_DIR="$(dirname "${THIS_SCRIPT}")" + +ROOT_DIR="$(dirname "${THIS_DIR}")" + +TGT_DIR="${ROOT_DIR}/build/artifacts/diagrams" + +if command -v poetry &>/dev/null; then + RUN="poetry run" +else + if ! command -v linkml &>/dev/null; then + echo "The command 'linkml' is missing" + exit 1 + fi + if ! command -v curl &>/dev/null; then + echo "The command 'curl' is missing" + exit 1 + fi + RUN="" +fi + +mkdir -p "${TGT_DIR}" + +# Holistic class diagram +${RUN} linkml generate plantuml "${ROOT_DIR}/model/margo-data-model.linkml.yaml" | + sed "s/@enduml/DeploymentAnnotations ..> ApplicationDescription\n@enduml/" | + sed "s/@enduml/DeploymentStatusManifest ..> ApplicationDeployment\n@enduml/" | + sed "s/@enduml/DesiredStateManifest ..> ApplicationDeployment\n@enduml/" >"${TMP_PLANTUML_FILE}" + +curl -H "Content-Type: test/plain" --silent --data-binary @"${TMP_PLANTUML_FILE}" https://kroki.io/plantuml/svg -o "${TGT_DIR}/DataModel-ClassDiagram.svg" +curl -H "Content-Type: test/plain" --silent --data-binary @"${TMP_PLANTUML_FILE}" https://kroki.io/plantuml/png -o "${TGT_DIR}/DataModel-ClassDiagram.png" + +if [ "$#" -lt 1 ]; then + exit +fi + +# Class diagram focused on ApplicationDescription +${RUN} linkml generate plantuml \ + --classes ApplicationDescription \ + --classes ApplicationMetadata \ + --classes Parameter \ + --classes Configuration \ + --classes DeploymentProfileDescription \ + "${ROOT_DIR}/model/application-description.linkml.yaml" | + sed "/Component.*{$/,/}/d" \ + >"${TMP_PLANTUML_FILE}" + +curl -H "Content-Type: test/plain" --silent --data-binary @"${TMP_PLANTUML_FILE}" https://kroki.io/plantuml/svg -o "${TGT_DIR}/ApplicationDescription-ClassDiagram.svg" + +# Class diagram focused on DeploymentStatusManifest +${RUN} linkml generate plantuml \ + --classes DeploymentStatusManifest \ + --classes ApplicationDeployment \ + --classes DeploymentProfile \ + "${ROOT_DIR}/model/margo-data-model.linkml.yaml" | + sed "s/@enduml/DeploymentStatusManifest ..> ApplicationDeployment\n@enduml/" | + sed "s/@enduml/ComponentStatus ..> Component\n@enduml/" \ + >"${TMP_PLANTUML_FILE}" + +curl -H "Content-Type: test/plain" --silent --data-binary @"${TMP_PLANTUML_FILE}" https://kroki.io/plantuml/svg -o "${TGT_DIR}/DeploymentStatusManifest-ClassDiagram.svg" + +# Class diagram focused on DesiredStateManifest +${RUN} linkml generate plantuml \ + --classes DesiredStateManifest \ + "${ROOT_DIR}/model/margo-data-model.linkml.yaml" | + sed "s/@enduml/DesiredStateManifest ..> ApplicationDeployment\n@enduml/" \ + >"${TMP_PLANTUML_FILE}" + +curl -H "Content-Type: test/plain" --silent --data-binary @"${TMP_PLANTUML_FILE}" https://kroki.io/plantuml/svg -o "${TGT_DIR}/DesiredStateManifest-ClassDiagram.svg" + +# Class diagram focused on DeviceCapabilities +${RUN} linkml generate plantuml \ + --classes DeviceCapabilitiesManifest \ + --classes Properties \ + --classes Resources \ + "${ROOT_DIR}/model/margo-data-model.linkml.yaml" \ + >"${TMP_PLANTUML_FILE}" + +curl -H "Content-Type: test/plain" --silent --data-binary @"${TMP_PLANTUML_FILE}" https://kroki.io/plantuml/svg -o "${TGT_DIR}/DeviceCapabilities-ClassDiagram.svg" + +# Class diagram focused on ApplicationDeployment +${RUN} linkml generate plantuml \ + --classes ApplicationDeployment \ + --classes DeploymentMetadata \ + --classes DeploymentAnnotations \ + --classes Spec \ + --classes Parameter \ + --classes DeploymentProfile \ + --classes ComposeDeploymentProfile \ + --classes HelmDeploymentProfile \ + --classes Component \ + "${ROOT_DIR}/model/application-deployment.linkml.yaml" | + sed "s/@enduml/DeploymentAnnotations ..> ApplicationDescription\n@enduml/" \ + >"${TMP_PLANTUML_FILE}" + +curl -H "Content-Type: test/plain" --silent --data-binary @"${TMP_PLANTUML_FILE}" https://kroki.io/plantuml/svg -o "${TGT_DIR}/ApplicationDeployment-ClassDiagram.svg" diff --git a/tools/generate-docs.bash b/tools/generate-docs.bash new file mode 100755 index 00000000..091661b3 --- /dev/null +++ b/tools/generate-docs.bash @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +set -eu + +THIS_SCRIPT="$(readlink -f "${0}")" +THIS_DIR="$(dirname "${THIS_SCRIPT}")" + +ROOT_DIR="$(dirname "${THIS_DIR}")" + +TMP_DIR="$(mktemp -d)" + +cleanup() { + rm -r "${TMP_DIR}" +} + +trap cleanup EXIT + +if command -v poetry &>/dev/null; then + RUN="poetry run" +else + if ! command -v linkml &>/dev/null; then + echo "The command 'linkml' is missing" + exit 1 + fi + if ! command -v mkdocs &>/dev/null; then + echo "The command 'mkdocs' is missing" + exit 1 + fi + RUN="" +fi + +TGT_DIR="${ROOT_DIR}/build/artifacts/main-classes" +MERGED_DIR="${ROOT_DIR}/build/site" + +mkdir -p "${TGT_DIR}" "${MERGED_DIR}" + +cp -R -H "${ROOT_DIR}/docs/"* "${MERGED_DIR}/" +cp -R -H "${ROOT_DIR}/system-design/"* "${MERGED_DIR}/" + +# Main classes + +for schema_name in "application-description" "application-deployment" "deployment-status" "desired-state-manifest" "device-capabilities"; do + ${RUN} linkml generate doc \ + --directory="${TMP_DIR}" \ + --template-directory="${ROOT_DIR}/tools/templates/main-classes" \ + --preserve-names \ + --stacktrace \ + --example-directory="${ROOT_DIR}/model/examples/valid" \ + "${ROOT_DIR}/model/${schema_name}.linkml.yaml" >/dev/null + + mv "${TMP_DIR}/index.md" "${TGT_DIR}/${schema_name}.md" + rm -rf "${TMP_DIR:?}/*" +done + +mv "${TGT_DIR}/deployment-status.md" "${MERGED_DIR}/specification/margo-management-interface/" +mv "${TGT_DIR}/device-capabilities.md" "${MERGED_DIR}/specification/margo-management-interface/" +mv "${TGT_DIR}/application-description.md" "${MERGED_DIR}/specification/applications/" + +# Whole Data Model + +TGT_DIR="${ROOT_DIR}/build/artifacts/markdown" + +mkdir -p "${TGT_DIR}" + +${RUN} linkml generate doc \ + --directory="${TGT_DIR}" \ + --template-directory="${ROOT_DIR}/tools/templates/model" \ + --preserve-names \ + --stacktrace \ + --example-directory="${ROOT_DIR}/model/examples/valid" \ + "${ROOT_DIR}/model/margo-data-model.linkml.yaml" >/dev/null + +mkdir -p "${MERGED_DIR}/data-model" +mv "${TGT_DIR}"/* "${MERGED_DIR}/data-model/" + +"${THIS_DIR}/generate-class-diagram.bash" + +mkdir -p "${MERGED_DIR}/figures" +cp "${ROOT_DIR}/build/artifacts/diagrams/DataModel-ClassDiagram.svg" "${MERGED_DIR}/figures/" +cp "${ROOT_DIR}/build/artifacts/diagrams/DataModel-ClassDiagram.png" "${MERGED_DIR}/figures/" + +"${THIS_DIR}/generate-openapi.bash" +# generate-openapi.bash writes to both build/artifacts/openapi/ and system-design/. +# Copy the spec into build/site/ so mkdocs build can find it. +cp "${ROOT_DIR}/build/artifacts/openapi/workload-management-api-1.0.0.openapi.yaml" \ + "${MERGED_DIR}/specification/margo-management-interface/workload-management-api-1.0.0.yaml" + +# JSON Schemas: copy for download +JSON_SCHEMA_DIR="${ROOT_DIR}/build/artifacts/json-schemas" +MERGED_JSON_SCHEMA_DIR="${MERGED_DIR}/json-schemas" + +mkdir -p "${MERGED_JSON_SCHEMA_DIR}" + +for schema_file in "${JSON_SCHEMA_DIR}"/*.schema.json; do + if [ -f "${schema_file}" ]; then + cp "${schema_file}" "${MERGED_JSON_SCHEMA_DIR}/" + fi +done diff --git a/tools/generate-json-schemas.bash b/tools/generate-json-schemas.bash new file mode 100755 index 00000000..14ff6e5b --- /dev/null +++ b/tools/generate-json-schemas.bash @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -eu + +THIS_SCRIPT="$(readlink -f "${0}")" +THIS_DIR="$(dirname "${THIS_SCRIPT}")" + +ROOT_DIR="$(dirname "${THIS_DIR}")" + +TGT_DIR="${ROOT_DIR}/build/artifacts/json-schemas" + +if command -v poetry &>/dev/null; then + RUN="poetry run" +else + if ! command -v linkml &>/dev/null; then + echo "The command 'linkml' is missing" + exit 1 + fi + RUN="" +fi + +mkdir -p "${TGT_DIR}" + +for schema in "application-deployment" "application-description" "deployment-status" "desired-state-manifest" "device-capabilities"; do + ${RUN} linkml generate json-schema "${ROOT_DIR}/model/${schema}.linkml.yaml" >"${TGT_DIR}/${schema}.schema.json" +done diff --git a/tools/generate-openapi.bash b/tools/generate-openapi.bash new file mode 100755 index 00000000..3764e3c5 --- /dev/null +++ b/tools/generate-openapi.bash @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -eu + +THIS_SCRIPT="$(readlink -f "${0}")" +THIS_DIR="$(dirname "${THIS_SCRIPT}")" + +ROOT_DIR="$(dirname "${THIS_DIR}")" + +TGT_DIR="${ROOT_DIR}/build/artifacts/openapi" +TGT_FILE="${TGT_DIR}/workload-management-api-1.0.0.openapi.yaml" + +# Tracked location in system-design/ — this is what the repo uses +# generate-docs.bash copies system-design/ into build/site/ for mkdocs +SYSTEM_DESIGN_FILE="${ROOT_DIR}/system-design/specification/margo-management-interface/workload-management-api-1.0.0.yaml" + +if command -v poetry &>/dev/null; then + RUN="poetry run" +else + if ! command -v linkml &>/dev/null; then + echo "The command 'linkml' is missing" + exit 1 + fi + RUN="" +fi + +mkdir -p "${TGT_DIR}" + +# TODO: remove following block after LinkML release >v1.11.1 +( + cd "${THIS_DIR}" + ${RUN} python openapigen.py --keep-unreferenced --inline-enums -t templates/openapi/workload-management-api-1.0.0.openapi.yaml ../model/margo-data-model.linkml.yaml \ + >"${TGT_FILE}" + cp "${TGT_FILE}" "${SYSTEM_DESIGN_FILE}" +) +exit + +${RUN} linkml generate openapi \ + --template "${THIS_DIR}/templates/openapi/workload-management-api-1.0.0.openapi.yaml" \ + --inline-enums \ + --keep-unreferenced \ + "${ROOT_DIR}/model/margo-data-model.linkml.yaml" \ + >"${TGT_FILE}" + +# Copy to the tracked location in system-design/ so it is picked up by +# generate-docs.bash (which copies system-design/ → build/site/) and by git +cp "${TGT_FILE}" "${SYSTEM_DESIGN_FILE}" diff --git a/tools/openapigen.py b/tools/openapigen.py new file mode 100644 index 00000000..b05e83f0 --- /dev/null +++ b/tools/openapigen.py @@ -0,0 +1,623 @@ +"""Generate OpenAPI YAML files.""" + +import json +import os +import re +import textwrap +from dataclasses import dataclass, field +from typing import cast + +import click +import yaml +from openapi_spec_validator import OpenAPIV30SpecValidator, OpenAPIV31SpecValidator +from openapi_spec_validator import validate as openapi_validate +from openapi_spec_validator.validation.validators import SpecValidator as OaSpecValidator +from pydantic import BaseModel +from referencing import Registry, Resource +from referencing.jsonschema import DRAFT4 +from yaml import MappingNode, ScalarNode + +from linkml._version import __version__ +from linkml.generators.jsonschemagen import JsonSchemaGenerator, json_schema_types +from linkml.generators.pydanticgen import PydanticGenerator +from linkml.utils.generator import Generator, shared_arguments + +SUPPORTED_OPENAPI_VERSIONS = ["3.0.3", "3.1.0"] + +openapi_generic_template = """# TODO: remove this whole comment block after processing +# This is a valid OpenAPI template to be used by the LinkML OpenAPI generator. +# Make sure to set the right OpenAPI version in the `openapi` top-level attribute. +# These are the supported OpenAPI versions: {openapi_version_list} +# It adds one (random) class or type of the LinkML schema as an example. +# Please adapt it to your needs. +# See more information in the online documentation: +# https://linkml.io/linkml/generators/openapi.html +openapi: x.y.z +info: + title: Generic example referring in LinkML-modelled resources + version: 0.1.0 +servers: + - url: https://example.org/ +security: + - PayloadSignature: [] +paths: + /api/endpoint: + get: + responses: + '200': + description: Endpoint example involving random data schema + content: + application/json: + schema: + # TODO: remove this whole comment block after processing + # any broken reference will cause template instantiation to fail + # OpenAPI editors typically also report them + $ref: '#/components/schemas/{data_schema}' +components: + # TODO: remove this whole comment block after processing + # any data schema provided here that is not used by at least + # one endpoint will be eliminated from the template instantiation + # OpenAPI editors typically also report them + schemas: + # TODO: remove this whole comment block after processing + # this resource name can differ from the name in the LinkML schema + # it must only match the corresponding endpoint `$ref` references + # it creates a mapping between names in OpenAPI and LinkML + {data_schema}: + type: object + description: Resource schema to be generated from the LinkML data model. + # TODO: remove this whole comment block after processing + # schema ID mismatching with provided schema will cause template + # instantiation to fail + x-linkml-schema: {linkml_schema_id} + x-linkml-source: {data_schema} +""" + + +@dataclass +class OpenApiGenerator(Generator): + """ + Generates OpenAPI YAML from a LinkML schema. + + The generator composes a user-provided OpenAPI template (containing the API header, + paths/endpoints, and security schemes) with JSON Schema components generated from + the LinkML schema via :class:`.JsonSchemaGenerator`. Only data schemas referenced + by the template's endpoints (and their transitive dependencies) are included in + the ``components/schemas`` section. + + Currently following generation paths are supported (others might follow): + + * **v3.0.3** — uses :class:`.JsonSchemaGenerator` and applies post-processing + transforms (``const`` → ``enum``, nullable ``type`` lists → ``anyOf``, + ``$defs`` → ``components/schemas``) required by OpenAPI 3.0.3. + * **v3.1.0** — uses :class:`.PydanticGenerator` to compile a Python module, + then calls :meth:`pydantic.BaseModel.model_json_schema` on each class. + Because OpenAPI 3.1.0 is fully aligned with JSON Schema 2020-12, no + post-processing transforms are needed beyond rewriting ``$defs`` references + and stripping ``linkml_meta`` annotations. + + The OpenAPI version to be generated is obtained from the template's top-level + attribute `openapi`. + """ + + generatorname = os.path.basename(__file__) + generatorversion = "0.2.0" + valid_formats = ["openapi"] + file_extension = "yaml" + uses_schemaloader = False + + _template: dict = field(default_factory=dict, init=False, repr=False) + keep_unreferenced: bool = False + inline_enums: bool = False + # Mapping of valid_formats entries to OpenAPI version strings. + # Extend this dict when adding support for additional OpenAPI versions. + _openapi_versions: list[str] = field( + default_factory=lambda: SUPPORTED_OPENAPI_VERSIONS, + init=False, + repr=False, + ) + # Mapping of OpenAPI version strings to validators from openapi-spec-validator. + # Extend this dict when adding support for additional OpenAPI versions. + _openapi_validators: dict[str, type[OaSpecValidator]] = field( + default_factory=lambda: {"3.0.3": OpenAPIV30SpecValidator, "3.1.0": OpenAPIV31SpecValidator}, + init=False, + repr=False, + ) + + _openapi_version = "" # OpenAPI version declared in the template + + def _validate_oa_template(self, oa_validator_class: type[OaSpecValidator], expected_version: str): + """Validate the OpenAPI template""" + # Validate the input template against the OpenAPI specification. + # This also catches dangling $ref targets in endpoints. + openapi_validate(self._template, cls=oa_validator_class) + # Validation: every template schema must declare this LinkML schema. + if "components" in self._template and "schemas" in self._template["components"]: + for name, schema in self._template["components"]["schemas"].items(): + if schema["x-linkml-schema"] != self.schemaview.schema.id: + raise ValueError( + f"Template data schema '{name}' declares " + f"x-linkml-schema '{schema['x-linkml-schema']}' " + f"but the loaded schema has id '{self.schemaview.schema.id}'" + ) + + def _find_referenced_schemas(self) -> set[str]: + """Return the set of resource names referenced by the template's endpoints.""" + result = set() + for endp_spec in self._template["paths"].values(): + for req_spec in endp_spec.values(): + if "requestBody" in req_spec and "content" in req_spec["requestBody"]: + for content_spec in req_spec["requestBody"]["content"].values(): + if "$ref" in content_spec["schema"]: + resource_name = content_spec["schema"]["$ref"].removeprefix("#/components/schemas/") + result.add(resource_name) + if "parameters" in req_spec: + for param_spec in req_spec["parameters"]: + if "$ref" in param_spec["schema"]: + resource_name = param_spec["schema"]["$ref"].removeprefix("#/components/schemas/") + result.add(resource_name) + if "responses" in req_spec: + for response in req_spec["responses"].values(): + if "content" in response: + for content_spec in response["content"].values(): + if "$ref" in content_spec["schema"]: + resource_name = content_spec["schema"]["$ref"].removeprefix("#/components/schemas/") + result.add(resource_name) + return result + + def _generate_type_schema(self, type_name: str) -> dict: + """Build an OpenAPI-compatible JSON Schema for a LinkML TypeDefinition.""" + type_def = self.schemaview.get_type(type_name) + typ, fmt = json_schema_types.get(type_def.base.lower(), ("string", None)) + schema: dict = {} + if typ: + schema["type"] = str(typ) + if fmt: + schema["format"] = str(fmt) + if type_def.pattern: + schema["pattern"] = str(type_def.pattern) + if type_def.minimum_value is not None: + schema["minimum"] = str(type_def.minimum_value) + if type_def.maximum_value is not None: + schema["maximum"] = str(type_def.maximum_value) + if type_def.equals_string is not None: + schema["const"] = str(type_def.equals_string) + if type_def.equals_number is not None: + schema["const"] = str(type_def.equals_number) + if type_def.description: + schema["description"] = str(type_def.description) + return schema + + def _find_references(self, element: dict | list, referenced_schemas: set[str]) -> set[str]: + """Recursively collect all ``$ref`` target names from ``element`` into ``referenced_data_schemas``.""" + refd_schemas = referenced_schemas.copy() + if isinstance(element, dict): + if "$ref" in element: + refd_schemas.add(element["$ref"].replace("#/$defs/", "")) + for value in element.values(): + refd_schemas = self._find_references(value, refd_schemas) + elif isinstance(element, list): + for item in element: + refd_schemas = self._find_references(item, refd_schemas) + return refd_schemas + + def _fix_openapi_spec_v303(self, element: dict | list) -> dict | list | None: + """ + Transform JSON Schema constructs into OpenAPI v3.0.3 compatible forms: + + - ``const`` becomes ``enum`` with a single value + - ``type`` as a list (e.g. nullable ``["string", "null"]``) becomes ``anyOf`` + - ``$ref`` paths are rewritten from ``#/$defs/`` to ``#/components/schemas/`` + """ + fixed_element = None + if isinstance(element, dict): + fixed_element = {} + for key, value in element.items(): + if key == "const": + fixed_element["enum"] = [value] + elif key == "type" and isinstance(value, list): + fixed_element["anyOf"] = [{"type": item} for item in value if item != "null"] + else: + if isinstance(value, dict | list): + value = self._fix_openapi_spec_v303(value) + elif isinstance(value, str) and value.startswith("#/$defs/"): + value = value.replace("#/$defs/", "#/components/schemas/") + fixed_element[key] = value + elif isinstance(element, list): + fixed_element = [] + for item in element: + if isinstance(item, dict | list): + item = self._fix_openapi_spec_v303(item) + elif isinstance(item, str) and item.startswith("#/$defs/"): + item = item.replace("#/$defs/", "#/components/schemas/") + fixed_element.append(item) + return fixed_element + + def _rename(self, name_map: dict[str, str], element: dict | list) -> dict | list: + """ + If the resource names do not correspond the data schema names, + then some renaming is needed so that OpenAPI resource names + are properly referenced throughout the whole OpenAPI file. + """ + if isinstance(element, dict): + renamed_element: dict | list = {} + for key, value in element.items(): + if key in name_map: + key = name_map[key] + if isinstance(value, dict | list): + value = self._rename(name_map, value) + elif isinstance(value, str) and value.startswith("#/components/schemas/"): + data_schema_name = value[len("#/components/schemas/") :] + if data_schema_name in name_map: + value = value.replace(data_schema_name, name_map[data_schema_name]) + renamed_element[key] = value + elif isinstance(element, list): + renamed_element: dict | list = [] + for item in element: + if isinstance(item, dict | list): + item = self._rename(name_map, item) + elif isinstance(item, str) and item.startswith("#/components/schemas/"): + data_schema_name = item[len("#/components/schemas/") :] + if data_schema_name in name_map: + item = item.replace(data_schema_name, name_map[data_schema_name]) + renamed_element.append(item) + else: + raise TypeError(f"Unexpected type '{type(element)}', only 'dict' and 'list' supported.") + return renamed_element + + def _strip_linkml_meta(self, element: dict | list) -> dict | list: + """Remove ``linkml_meta`` annotations recursively from Pydantic JSON Schema output.""" + if isinstance(element, dict): + element.pop("linkml_meta", None) + for value in element.values(): + if isinstance(value, dict) or isinstance(value, list): + self._strip_linkml_meta(value) + elif isinstance(element, list): + for item in element: + if isinstance(item, dict) or isinstance(item, list): + self._strip_linkml_meta(item) + return element + + def _rewrite_defs_refs(self, element: dict | list) -> dict | list: + """ + Rewrite ``#/$defs/`` references to ``#/components/schemas/`` in-place. + + This is the only structural transformation needed for OpenAPI 3.1.0, + since it is fully aligned with JSON Schema 2020-12. + """ + if isinstance(element, dict): + keys_to_update = [] + for key, value in element.items(): + if isinstance(value, str) and value.startswith("#/$defs/"): + keys_to_update.append((key, value.replace("#/$defs/", "#/components/schemas/"))) + elif isinstance(value, dict) or isinstance(value, list): + self._rewrite_defs_refs(value) + for key, new_value in keys_to_update: + element[key] = new_value + elif isinstance(element, list): + for i, item in enumerate(element): + if isinstance(item, str) and item.startswith("#/$defs/"): + element[i] = item.replace("#/$defs/", "#/components/schemas/") + elif isinstance(item, dict) or isinstance(item, list): + self._rewrite_defs_refs(item) + return element + + def _sanitize_schemas(self, name_map: dict[str, str], openapi_schemas: dict, req_linkml_names: set[str]) -> dict: + """ + Prune unreachable schemas, remove redundant metadata, convert JSON Schema constructs + to OpenAPI 3.0.3 compat, and apply any OpenAPI<->LinkML name renames. + """ + + referenced_schemas = req_linkml_names.copy() + for openapi_schema in openapi_schemas.values(): + referenced_schemas = self._find_references(openapi_schema, referenced_schemas) + if not self.keep_unreferenced: + openapi_schema_names = list(openapi_schemas.keys()) + for openapi_schema_name in openapi_schema_names: + if openapi_schema_name not in referenced_schemas: + del openapi_schemas[openapi_schema_name] + # title always duplicates the schema dict key, so it is redundant in components/schemas + for openapi_schema in openapi_schemas.values(): + openapi_schema.pop("title", None) + if self._openapi_version == "3.0.3": + openapi_schemas = cast(dict, self._fix_openapi_spec_v303(openapi_schemas)) + elif self._openapi_version == "3.1.0": + openapi_schemas = cast(dict, self._strip_linkml_meta(openapi_schemas)) + openapi_schemas = cast(dict, self._rewrite_defs_refs(openapi_schemas)) + # OpenAPI 3.1 restricts components/schemas keys to ^[a-zA-Z0-9._-]+$ + # (no spaces). Sanitize offending schema names and rewrite every $ref. + sanitize_map = self._sanitize_schema_names(openapi_schemas, reserved=set(name_map.values())) + if sanitize_map: + openapi_schemas = cast(dict, self._rename(sanitize_map, openapi_schemas)) + else: + raise ValueError(f"OpenAPI version '{self._openapi_version}' is not supported") + if name_map: + openapi_schemas = cast(dict, self._rename(name_map, openapi_schemas)) + if self.inline_enums: + openapi_schemas = self._inline_enum_schemas(openapi_schemas) + return openapi_schemas + + # OpenAPI 3.1 schema-name pattern; keys under components/schemas must match it. + _OPENAPI_31_NAME_RE = re.compile(r"^[a-zA-Z0-9._-]+$") + + def _sanitize_schema_names(self, openapi_schemas: dict, reserved: set[str]) -> dict[str, str]: + """Return a map of schema names invalid under OpenAPI 3.1 to sanitized equivalents. + + OpenAPI 3.1 constrains ``components/schemas`` keys to ``^[a-zA-Z0-9._-]+$``, + so LinkML names containing spaces (or other disallowed characters) must be + rewritten. Any run of invalid characters collapses to a single underscore; + uniqueness is ensured against existing and already-reserved names. + """ + existing = set(openapi_schemas.keys()) | reserved + name_map: dict[str, str] = {} + for name in openapi_schemas: + if self._OPENAPI_31_NAME_RE.match(name): + continue + base = re.sub(r"[^a-zA-Z0-9._-]+", "_", name).strip("_") or "schema" + candidate = base + suffix = 1 + while candidate in existing or candidate in name_map.values(): + candidate = f"{base}_{suffix}" + suffix += 1 + name_map[name] = candidate + existing.add(candidate) + return name_map + + def _inline_enum_schemas(self, data_schemas: dict) -> dict: + """Inline enum subschemas into their parents instead of separate entries.""" + enum_schemas = { + name: schema + for name, schema in data_schemas.items() + if isinstance(schema, dict) and "enum" in schema and "properties" not in schema + } + if not enum_schemas: + return data_schemas + + def _replace_refs(obj): + if isinstance(obj, dict): + if "$ref" in obj: + ref_name = obj["$ref"].split("/")[-1] + if ref_name in enum_schemas: + return enum_schemas[ref_name] + return {k: _replace_refs(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [_replace_refs(item) for item in obj] + return obj + + return {k: _replace_refs(v) for k, v in data_schemas.items() if k not in enum_schemas} + + def _find_schemas_line(self, template_text: str) -> int: + """Return the 0-indexed line number of the ``schemas`` key under ``components``.""" + doc = yaml.compose(template_text) + if not isinstance(doc, MappingNode): + raise ValueError("OpenAPI template is not a YAML mapping") + components_node = None + for key, value in doc.value: + if isinstance(key, ScalarNode) and key.value == "components": + components_node = value + break + if not isinstance(components_node, MappingNode): + raise ValueError("OpenAPI template is missing a valid 'components' section") + for key, _ in components_node.value: + if isinstance(key, ScalarNode) and key.value == "schemas": + return key.start_mark.line + raise ValueError("OpenAPI template is missing 'schemas' section under 'components'") + + def _generate_schemas_v303(self, endpoint_ref_schema_names: set[str]) -> dict: + """Generate component schemas for OpenAPI v3.0.3 via :class:`.JsonSchemaGenerator`.""" + # JsonSchemaGenerator.generate() emits every class/enum of the LinkML schema into + # $defs. LinkML types are not part of $defs and are generated separately. + # all_req_schemas contains all directly or transitively required schemas from + # LinkML classes and types + json_schema = JsonSchemaGenerator(self.schemaview.schema, include_null=False, preserve_names=True).generate() + all_req_schemas: dict[str, dict] = json.loads(json_schema.to_json())["$defs"] + for linkml_name in endpoint_ref_schema_names: + if linkml_name in self.schemaview.all_types(): + all_req_schemas[linkml_name] = self._generate_type_schema(linkml_name) + return all_req_schemas + + def _generate_schemas_v310(self, endpoint_ref_schema_names: set[str]) -> dict: + """Generate component schemas for OpenAPI v3.1.0 via :class:`.PydanticGenerator`.""" + if not endpoint_ref_schema_names: + return {} + materialized_schema = self.schemaview.materialize_derived_schema() + module = PydanticGenerator(materialized_schema, extra_fields="allow").compile_module() + pydantic_classes = { + name: obj + for name, obj in vars(module).items() + if isinstance(obj, type) and issubclass(obj, BaseModel) and obj is not BaseModel + } + defined_types = {name: obj for name, obj in vars(module)["linkml_meta"]["types"].items()} + + all_schemas = {} + for name, cls in pydantic_classes.items(): + schema = cls.model_json_schema() + if "$defs" in schema: + all_schemas |= cls.model_json_schema()["$defs"] + if defined_types: + json_schema = JsonSchemaGenerator( + self.schemaview.schema, include_null=False, preserve_names=True + ).generate() + all_schemas |= json.loads(json_schema.to_json())["$defs"] + + # LinkML types are not emitted as standalone Pydantic classes nor reliably as + # JSON Schema $defs (their constraints are inlined into referencing slots). + # Endpoint-referenced types must therefore be generated explicitly, mirroring + # the v3.0.3 path. + for linkml_name in endpoint_ref_schema_names: + if linkml_name not in all_schemas and linkml_name in self.schemaview.all_types(): + all_schemas[linkml_name] = self._generate_type_schema(linkml_name) + + return all_schemas + + def _generate_schemas(self, endpoint_ref_schema_names: set[str]) -> dict: + if self._openapi_version == "3.1.0": + all_req_schemas = self._generate_schemas_v310(endpoint_ref_schema_names) + else: + all_req_schemas = self._generate_schemas_v303(endpoint_ref_schema_names) + return all_req_schemas + + def serialize(self, template_file: str = "", **kwargs) -> str: + """Generate OpenAPI YAML from ``template_file`` and the loaded LinkML schema.""" + # load the template + if not template_file: + raise ValueError("An OpenAPI template file is required") + with open(template_file) as tf: + template_text = tf.read() + self._template = yaml.safe_load(template_text) + # determine the OpenAPI version from the provided template + self._openapi_version = self._template["openapi"] + if self._openapi_version not in SUPPORTED_OPENAPI_VERSIONS: + raise ValueError( + f"Unsupported OpenAPI version {self._openapi_version}. " + + f"Only supported versions are {','.join(self._openapi_versions)}" + ) + + # get the corresponding OpenAPI validator + oa_validator_class = self._openapi_validators.get(self._openapi_version) + if oa_validator_class is None: + raise ValueError(f"No validator available for OpenAPI version {self._openapi_version}") + # validate the OpenAPI template before further processing + self._validate_oa_template(oa_validator_class, self._openapi_version) + # if no schemas to instantiate, return the template itself + if ( + "components" not in self._template + or "schemas" not in self._template["components"] + or not self._template["components"]["schemas"] + ): + return template_text + + # Two namespaces exist: OpenAPI schema names (from the template's + # components/schemas keys) and LinkML element names (from the LinkML schema). + # Every schema has a name in both namespaces and the template declares the + # mapping between them in the x-linkml-schema values; they may be identical or differ. + # When they differ, name_map records the synonym (LinkML element name -> OpenAPI schema name). + endpoint_ref_openapi_names = self._find_referenced_schemas() # OpenAPI names referenced by endpoints + openapi_schemas = self._template["components"]["schemas"] # schemas provided by the OpenAPI template + # collect the LinkML names referenced by endpoints (seed for sanitizing below) + if self.keep_unreferenced: + req_linkml_names: set[str] = {openapi_schemas[n]["x-linkml-source"] for n in openapi_schemas.keys()} + else: + req_linkml_names: set[str] = {openapi_schemas[n]["x-linkml-source"] for n in endpoint_ref_openapi_names} + # when OpenAPI and LinkML names differ, record the synonym for later renaming. + # The template may declare a resource name (x-linkml-source mapping) for schemas + # referenced only by other schemas, not just those referenced directly by + # endpoints; every declared mapping must be honoured throughout the spec. + name_map: dict[str, str] = { + openapi_schemas[n]["x-linkml-source"]: n + for n in openapi_schemas + if n != openapi_schemas[n]["x-linkml-source"] + } + + all_req_schemas = self._generate_schemas(req_linkml_names) + + # sanitize schemas not transitively reachable from any endpoint-referenced schema + sanitized_data_schemas = self._sanitize_schemas(name_map, all_req_schemas, req_linkml_names) + + # instantiate the real OpenAPI YAML replacing the schema placeholders + lines = template_text.splitlines(keepends=True) + schemas_line_idx = self._find_schemas_line(template_text) + text_before_schemas = "".join(lines[:schemas_line_idx]) + schemas_yaml = yaml.dump(sanitized_data_schemas, sort_keys=False) + indented_schemas = textwrap.indent(schemas_yaml, " ") + result = text_before_schemas + " schemas:\n" + indented_schemas + + # Check for dangling $ref references using the referencing library + result_obj = yaml.safe_load(result) + schemas = result_obj.get("components", {}).get("schemas", {}) + registry = Registry().with_resources( + ( + f"#/components/schemas/{name}", + Resource.from_contents(schema, default_specification=DRAFT4), + ) + for name, schema in schemas.items() + ) + + def _collect_refs(obj, refs): + if isinstance(obj, dict): + if "$ref" in obj and isinstance(obj["$ref"], str) and obj["$ref"].startswith("#/"): + refs.append(obj["$ref"]) + for v in obj.values(): + _collect_refs(v, refs) + elif isinstance(obj, list): + for item in obj: + _collect_refs(item, refs) + + all_refs = [] + _collect_refs(result_obj, all_refs) + dangling = [] + for ref in all_refs: + try: + registry.get_or_retrieve(ref) + except Exception: + dangling.append(ref) + if dangling: + raise ValueError(f"Dangling $ref in generated OpenAPI spec: {','.join(dangling)}") + + # validate the generated output against the OpenAPI specification before returning + openapi_validate(yaml.safe_load(result), cls=oa_validator_class) + return result + + def printout_template(self) -> str: + """Return a generic OpenAPI template pre-filled with the first class/type of the LinkML schema.""" + element_names = self.schemaview.all_classes().keys() + if not element_names: + element_names = self.schemaview.all_types().keys() + if not element_names: + # if no realistic schema and data can be used, put some placeholders + return openapi_generic_template.format( + linkml_schema_id="", data_schema="" + ) + first_element = next(iter(element_names)) + if re.search(r"[ :\d]", first_element): + first_element = f'"{first_element}"' + return openapi_generic_template.format( + linkml_schema_id=self.schemaview.schema.id, + data_schema=first_element, + openapi_version_list=",".join(self._openapi_versions), + ) + + +@shared_arguments(OpenApiGenerator) +@click.command(name="openapi") +@click.option( + "--template", + "-t", + help="OpenAPI template - includes the header, the endpoints and the security schemes", +) +@click.option( + "--keep-unreferenced", + "-k", + is_flag=True, + default=False, + help="Keep schemas listed in the template even if not referenced by any endpoint", +) +@click.option( + "--inline-enums", + "-e", + is_flag=True, + default=False, + help="Inline enum subschemas into their parent schemas instead of generating separate schema entries", +) +@click.version_option(__version__, "-V", "--version") +def cli(yamlfile, template, keep_unreferenced, inline_enums, **args): + """Generate an OpenAPI YAML with resources modelled with LinkML. + If no OpenAPI template is provided, + a generic one with one exemplary class/type schema is printed out.""" + # if no template provided, print out a generic one + if not template: + print(OpenApiGenerator(yamlfile, **args).printout_template()) + return + print( + OpenApiGenerator( + yamlfile, + keep_unreferenced=keep_unreferenced, + inline_enums=inline_enums, + **args, + ).serialize(template_file=template, **args), + end="", + ) + + +if __name__ == "__main__": + cli() diff --git a/tools/templates/main-classes/class.md.jinja2 b/tools/templates/main-classes/class.md.jinja2 new file mode 100644 index 00000000..e69de29b diff --git a/tools/templates/main-classes/index.md.jinja2 b/tools/templates/main-classes/index.md.jinja2 new file mode 100644 index 00000000..cd033865 --- /dev/null +++ b/tools/templates/main-classes/index.md.jinja2 @@ -0,0 +1,66 @@ + +{%- macro format_range(slot) -%} + {%- if slot.multivalued -%} + {%- if slot.inlined -%} + {%- if slot.inlined_as_list -%} + []{{ slot.range }} + {%- else -%} + {%- if slot.name == "properties" -%} + map[string][string] + {%- else -%} + map[string][{{ slot.range }}] + {%- endif -%} + {%- endif -%} + {%- else -%} + []string + {%- endif -%} + {%- else -%} + {{ slot.range }} + {%- endif -%} +{%- endmacro -%} + +{%- set schema_basename = schema.source_file.split('/')[-1].replace('.linkml.yaml', '') -%} + +# {%- if schema.title %}{{ schema.title }}{% else %}{{ schema.name }}{% endif %} + +{% if schema.description %}{{ schema.description }}{% endif %} + +## Object structure + +{% for cls in schemaview.all_classes() -%} +{%- if not cls.startswith("ComponentProperties") and not cls.startswith("Helm") and not cls.startswith("Compose") +%} +### {{ cls }} Attributes + +| Attribute | Type | Required? | Description | +| --- | --- | --- | --- | +{% for slot in schemaview.class_slots(cls)|sort(attribute='rank') -%} +| {{ slot }} | {{ format_range(schemaview.get_slot(slot)) }} | {% if schemaview.get_slot(slot).required == True %} Y {% else %} N {% endif %} | {{ schemaview.get_slot(slot).description }}| +{% endfor %} + +Detailed class view: [{{ cls }}](../../data-model/{{ cls }}.md) +{% endif -%} +{%- endfor -%} + +{% if gen.example_object_blobs(schema.name) -%} +## Examples + +Following examples have been automatically validated against the schema of this class. + +{% for name, blob in gen.example_object_blobs(schema.name) -%} +??? example "{{ name }}" + + ```yaml + {{ blob | indent(4) }} + ``` +{% endfor %} +{% endif %} + +## JSON Schema + +

View JSON Schema in new tab or download

+ diff --git a/tools/templates/model/class.md.jinja2 b/tools/templates/model/class.md.jinja2 new file mode 100644 index 00000000..ce23fbc6 --- /dev/null +++ b/tools/templates/model/class.md.jinja2 @@ -0,0 +1,288 @@ +{%- if element.title %} + {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%} +{%- else %} + {%- if gen.use_class_uris -%} + {%- set title = element.name -%} + {%- else -%} + {%- set title = gen.name(element) -%} + {%- endif -%} +{%- endif -%} + +{% macro compute_range(slot) -%} + {%- if slot.any_of or slot.exactly_one_of -%} + {%- for subslot_range in schemaview.slot_range_as_union(slot) -%} + {{ gen.link(subslot_range) }} + {%- if not loop.last -%} +  or 
+ {%- endif -%} + {%- endfor -%} + {%- else -%} + {{ gen.link(slot.range) }} + {%- endif -%} +{% endmacro %} + +# Class: {{ title }} {% if element.deprecated %} (DEPRECATED) {% endif %} + +{%- if header -%} +{{ header }} +{%- endif -%} + +{% if element.description %} +{% set element_description_lines = element.description.split('\n') %} +{% for element_description_line in element_description_lines %} +_{{ element_description_line }}_ +{% endfor %} +{% endif %} + +{% if element.abstract %} +* __NOTE__: this is an abstract class and should not be instantiated directly +{% endif %} + +{# +URI: {{ gen.uri_link(element) }} +#} + +{% if diagram_type == "er_diagram" %} +```{{ gen.mermaid_directive() }} +{{ gen.mermaid_diagram([element.name]) }} +``` +{% elif diagram_type == "plantuml_class_diagram" %} +```puml +{{ gen.mermaid_diagram([element.name]) }} +``` +{% else %} +{% include "class_diagram.md.jinja2" %} +{% endif %} + +{% if schemaview.class_parents(element.name) or schemaview.class_children(element.name, mixins=False) %} + +## Inheritance +{{ gen.inheritance_tree(element, mixins=True) }} +{% else %} + +{% endif %} + +{%- set has_class_props = element.class_uri or element.tree_root or element.mixin + or element.subclass_of or element.union_of or element.disjoint_with + or element.slot_names_unique or element.represents_relationship + or element.children_are_mutually_disjoint %} + +{%- if has_class_props %} +## Class Properties + +| Property | Value | +| --- | --- | +{%- if element.class_uri %} +| Class URI | {{ gen.uri_link(element.class_uri) }} | +{%- endif %} +{%- if element.mixin %} +| Mixin | Yes | +{%- endif %} +{%- if element.tree_root %} +| Tree Root | Yes | +{%- endif %} +{%- if element.slot_names_unique %} +| Slot Names Unique | Yes | +{%- endif %} +{%- if element.represents_relationship %} +| Represents Relationship | Yes | +{%- endif %} +{%- if element.subclass_of %} +| Subclass Of | {{ gen.links(element.subclass_of) | join(', ') }} | +{%- endif %} +{%- if element.union_of %} +| Union Of | {{ gen.links(element.union_of) | join(', ') }} | +{%- endif %} +{%- if element.disjoint_with %} +| Disjoint With | {{ gen.links(element.disjoint_with) | join(', ') }} | +{%- endif %} +{%- if element.children_are_mutually_disjoint %} +| Children Are Mutually Disjoint | Yes | +{%- endif %} + +{% endif %} +## Attributes + +| Name | Cardinality and Range | Description | Inheritance | +| --- | --- | --- | --- | +{% if gen.get_direct_slots(element)|length > 0 %} +{%- for slot in gen.get_direct_slots(element) -%} +| {{ gen.link(slot) }} | {{ gen.cardinality(slot) }}
{{ compute_range(slot) }} | {{ slot.description|enshorten }} | direct | +{% endfor -%} +{% endif -%} +{% if gen.get_indirect_slots(element)|length > 0 %} +{%- for slot in gen.get_indirect_slots(element) -%} +| {{ gen.link(slot) }} | {{ gen.cardinality(slot) }}
{{ compute_range(slot) }} | {{ slot.description|enshorten }} | {{ gen.links(gen.get_slot_inherited_from(element.name, slot.name))|join(', ') }} | +{% endfor -%} +{% endif %} + +{%- if element.unique_keys %} +## Unique Keys + +{% for uk in element.unique_keys.values() %} +### {{ uk.unique_key_name }} + +**Unique key slots:** {{ uk.unique_key_slots | join(', ') }} +{%- if uk.consider_nulls_inequal %} + +Considers null values as inequal +{%- endif %} +{% endfor %} +{% endif %} + +{%- if element.defining_slots %} +## Defining Slots + +This class is defined by the following slots: + +{% for slot_name in element.defining_slots %} +* {{ gen.link(slot_name) }} +{%- endfor %} +{% endif %} + +{%- set has_expressions = element.any_of or element.all_of or element.exactly_one_of + or element.none_of or element.slot_conditions %} + +{%- if has_expressions %} +
+Expressions & Logic + +{%- if element.any_of %} +#### Any Of + +The class must satisfy at least one of: + +{%- for expr in element.any_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.all_of %} +#### All Of + +The class must satisfy all of: + +{%- for expr in element.all_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.exactly_one_of %} +#### Exactly One Of + +The class must satisfy exactly one of: + +{%- for expr in element.exactly_one_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.none_of %} +#### None Of + +The class must not satisfy any of: + +{%- for expr in element.none_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.slot_conditions %} +#### Slot Conditions + +{%- for slot_name, conditions in element.slot_conditions.items() %} +- **{{ gen.link(slot_name) }}**: {{ conditions }} +{%- endfor %} +{%- endif %} + +
+{% endif %} + +{% if schemaview.is_mixin(element.name) %} +## Mixin Usage + +| mixed into | description | +| --- | --- | +{% for c in schemaview.class_children(element.name, is_a=False) -%} +| {{ gen.link(c) }} | {{ schemaview.get_class(c).description|enshorten }} | +{% endfor %} +{% endif %} + +{% if schemaview.usage_index().get(element.name) %} +## Usages + +| used by | used in | type | used | +| --- | --- | --- | --- | +{% for usage in schemaview.usage_index().get(element.name) -%} +| {{ gen.link(usage.used_by) }} | {{ gen.link(usage.slot) }} | {{ usage.metaslot }} | {{ gen.link(usage.used) }} | +{% endfor %} +{% endif %} + +{% if element.rules %} +## Rules + +{% for rule in gen.classrule_to_dict_view(element) %} +### {{ rule.title }} + +| Rule Applied | Preconditions | Postconditions | Elseconditions | +|--------------|---------------|----------------|----------------| +{% for key in rule.preconditions -%} +| {{ key }} | +{%- if rule.preconditions[key] is defined -%} +```{{ rule.preconditions[key] }}``` +{%- else -%} +{% endif %} | +{%- if rule.postconditions and rule.postconditions[key] is defined -%} +```{{ rule.postconditions[key] }}``` +{%- else -%} +{% endif %} | +{%- if rule.elseconditions and rule.elseconditions[key] is defined -%} +```{{ rule.elseconditions[key] }}``` +{%- else -%} +{% endif %} | +{% endfor %} + +{% endfor %} +{% endif %} + +{% include "common_metadata.md.jinja2" %} + +{% if gen.example_object_blobs(element.name) -%} +## Examples + +Following examples have been automatically validated against the schema of this class. + +{% for name, blob in gen.example_object_blobs(element.name) -%} +??? example "{{ name }}" + + ```yaml + {{ blob | indent(4) }} + ``` +{% endfor %} +{% endif %} + +--- + +??? note "This section is only relevant for contributors of the specification" + + ## LinkML Source + + + + ### Direct + + ??? note "Details" + ```yaml +{{ gen.yaml(element) | indent(8, first=True) }} + ``` + + ### Induced + + ??? note "Details" + ```yaml +{{ gen.yaml(element, inferred=True) | indent(8, first=True) }} + ``` + +{%- if footer -%} +{{ footer }} +{%- endif -%} diff --git a/tools/templates/model/class_diagram.md.jinja2 b/tools/templates/model/class_diagram.md.jinja2 new file mode 100644 index 00000000..95c2e980 --- /dev/null +++ b/tools/templates/model/class_diagram.md.jinja2 @@ -0,0 +1,76 @@ +{% macro slot_relationship(element, slot) %} + {% if slot.range is not none %} + {% set range_element = gen.name(schemaview.get_element(slot.range)) %} + {% set relation_label = gen.name(slot) %} + {{ gen.name(element) }} --> "{{ gen.cardinality(slot) }}" {{ range_element }} : {{ relation_label }} + click {{ range_element }} href "{{ gen.link_mermaid(schemaview.get_element(slot.range)) }}" + {% endif %} +{% endmacro %} + +{% if schemaview.class_parents(element.name) and schemaview.class_children(element.name) %} +```{{ gen.mermaid_directive() }} + classDiagram + class {{ gen.name(element) }} + click {{ gen.name(element) }} href "{{ gen.link_mermaid(element) }}" + {% for s in schemaview.class_parents(element.name)|sort(attribute='name') -%} + {{ gen.name(schemaview.get_element(s)) }} <|-- {{ gen.name(element) }} + click {{ gen.name(schemaview.get_element(s)) }} href "{{ gen.link_mermaid(schemaview.get_element(s)) }}" + {% endfor %} + + {% for s in schemaview.class_children(element.name)|sort(attribute='name') -%} + {{ gen.name(element) }} <|-- {{ gen.name(schemaview.get_element(s)) }} + click {{ gen.name(schemaview.get_element(s)) }} href "{{ gen.link_mermaid(schemaview.get_element(s)) }}" + {% endfor %} + + {% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%} + {{ gen.name(element) }} : {{ gen.name(s) }} + {% if s.range is not none and s.range not in gen.all_type_object_names() %} + {{ slot_relationship(element, s) }} + {% endif %} + {% endfor %} +``` +{% elif schemaview.class_parents(element.name) %} +```{{ gen.mermaid_directive() }} + classDiagram + class {{ gen.name(element) }} + click {{ gen.name(element) }} href "{{ gen.link_mermaid(element) }}" + {% for s in schemaview.class_parents(element.name)|sort(attribute='name') -%} + {{ gen.name(schemaview.get_element(s)) }} <|-- {{ gen.name(element) }} + click {{ gen.name(schemaview.get_element(s)) }} href "{{ gen.link_mermaid(schemaview.get_element(s)) }}" + {% endfor %} + {% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%} + {{ gen.name(element) }} : {{ gen.name(s) }} + {% if s.range is not none and s.range not in gen.all_type_object_names() %} + {{ slot_relationship(element, s) }} + {% endif %} + {% endfor %} +``` +{% elif schemaview.class_children(element.name) %} +```{{ gen.mermaid_directive() }} + classDiagram + class {{ gen.name(element) }} + click {{ gen.name(element) }} href "{{ gen.link_mermaid(element) }}" + {% for s in schemaview.class_children(element.name)|sort(attribute='name') -%} + {{ gen.name(element) }} <|-- {{ gen.name(schemaview.get_element(s)) }} + click {{ gen.name(schemaview.get_element(s)) }} href "{{ gen.link_mermaid(schemaview.get_element(s)) }}" + {% endfor %} + {% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%} + {{ gen.name(element) }} : {{ gen.name(s) }} + {% if s.range is not none and s.range not in gen.all_type_object_names() %} + {{ slot_relationship(element, s) }} + {% endif %} + {% endfor %} +``` +{% else %} +```{{ gen.mermaid_directive() }} + classDiagram + class {{ gen.name(element) }} + click {{ gen.name(element) }} href "{{ gen.link_mermaid(element) }}" + {% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%} + {{ gen.name(element) }} : {{ gen.name(s) }} + {% if s.range is not none and s.range not in gen.all_type_object_names() %} + {{ slot_relationship(element, s) }} + {% endif %} + {% endfor %} +``` +{% endif %} diff --git a/tools/templates/model/common_metadata.md.jinja2 b/tools/templates/model/common_metadata.md.jinja2 new file mode 100644 index 00000000..9fb2c181 --- /dev/null +++ b/tools/templates/model/common_metadata.md.jinja2 @@ -0,0 +1,144 @@ +{% if element.categories %} +## Categories + +{% for cat in element.categories %} +* {{ cat }} +{%- endfor %} + +{% endif %} +{% if element.keywords %} +## Keywords + +{% for kw in element.keywords %} +* {{ kw }} +{%- endfor %} + +{% endif %} +{% if element.in_subset %} +## In Subsets + +{% for subset in element.in_subset %} +* {{ gen.link(subset) }} +{%- endfor %} + +{% endif %} +{% if element.aliases %} +## Aliases + +{% for alias in element.aliases %} +* {{ alias }} +{%- endfor %} +{% endif %} + +{% if element.examples %} +## Examples + +| Value | +| --- | +{% for x in element.examples -%} +| {{ x.value }} | +{% endfor %} +{% endif -%} + +{% if element.comments -%} +## Comments + +{% for x in element.comments -%} +* {{ x }} +{% endfor %} +{% endif -%} + +{% if element.todos -%} +## TODOs + +{% for x in element.todos -%} +* {{ x }} +{% endfor %} +{% endif -%} + +{% if element.see_also -%} +## See Also + +{% for x in element.see_also -%} +* {{ gen.uri_link(x) }} +{% endfor %} +{% endif -%} + +{% if element.notes -%} +## Notes + +{% for note in element.notes -%} +* {{ note }} +{% endfor %} +{% endif -%} + +{% if element.alt_descriptions %} +## Alternative Descriptions + +{% for source, alt_desc in element.alt_descriptions.items() %} +* **{{ source }}**: {{ alt_desc.description }} +{%- endfor %} +{% endif %} + +{# +## Identifier and Mapping Information + +{%- set has_admin_metadata = element.status or (element.rank is not none and element.rank != 1000) %} + +{%- if has_admin_metadata %} +### Administrative Metadata + +{% if element.status -%} +**Status:** {{ element.status }} +{% endif -%} +{% if element.rank is not none and element.rank != 1000 -%} +**Rank:** {{ element.rank }} +{% endif -%} + +{% endif %} +{% if element.id_prefixes %} +### Valid ID Prefixes + +Instances of this class *should* have identifiers with one of the following prefixes: +{% for p in element.id_prefixes %} +* {{ p }} +{% endfor %} + +{% endif %} + +{% if element.annotations %} +### Annotations + +| property | value | +| --- | --- | +{% for a in element.annotations -%} +{%- if a|string|first != '_' -%} +| {{ a }} | {{ element.annotations[a].value }} | +{% endif -%} +{% endfor %} +{% endif %} + +{% if element.from_schema or element.imported_from %} +### Schema Source + +{% if element.from_schema %} +* from schema: {{ element.from_schema }} +{% endif %} +{% if element.imported_from %} +* imported from: {{ element.imported_from }} +{% endif %} +{% endif %} + +{% if schemaview.get_mappings(element.name).items() -%} +## Mappings + +| Mapping Type | Mapped Value | +| --- | --- | +{% for m, mt in schemaview.get_mappings(element.name).items() -%} +{% if mt|length > 0 -%} +| {{ m }} | {{ mt|join(', ') }} | +{% endif -%} +{% endfor %} + +{% endif -%} +#} diff --git a/tools/templates/model/enum.md.jinja2 b/tools/templates/model/enum.md.jinja2 new file mode 100644 index 00000000..e840fae2 --- /dev/null +++ b/tools/templates/model/enum.md.jinja2 @@ -0,0 +1,139 @@ +{%- if element.title and element.title != element.name %} + {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%} +{%- else %} + {%- set title = gen.name(element) -%} +{%- endif -%} + +# Enum: {{ title }} {% if element.deprecated %} (DEPRECATED) {% endif %} + +{% if element.description %} +{% set element_description_lines = element.description.split('\n') %} +{% for element_description_line in element_description_lines %} +_{{ element_description_line }}_ +{% endfor %} +{% endif %} + +URI: {{ gen.uri_link(element) }} + +{%- if element.enum_uri %} + +**Enum URI:** {{ gen.uri_link(element.enum_uri) }} +{% endif %} + +{%- set has_enum_source = element.code_set or element.pv_formula or element.reachable_from + or element.matches or element.concepts %} + +{%- if has_enum_source %} +## Enumeration Source + +{%- if element.code_set %} +**Code Set:** {{ gen.uri_link(element.code_set) }} + +{%- if element.code_set_tag %} +- **Tag:** {{ element.code_set_tag }} +{%- endif %} +{%- if element.code_set_version %} +- **Version:** {{ element.code_set_version }} +{%- endif %} +{%- endif %} + +{%- if element.pv_formula %} +**Permissible Value Formula:** {{ element.pv_formula }} +{%- endif %} + +{%- if element.reachable_from %} +**Reachable From:** + +{%- if element.reachable_from.source_ontology %} +- **Source:** {{ gen.link(element.reachable_from.source_ontology) }} +{%- endif %} +{%- if element.reachable_from.source_nodes %} +- **Nodes:** {{ element.reachable_from.source_nodes | join(', ') }} +{%- endif %} +{%- if element.reachable_from.relationship_types %} +- **Via:** {{ element.reachable_from.relationship_types | join(', ') }} +{%- endif %} +{%- endif %} + +{%- if element.matches %} +**Matches:** + +- **Expression:** `{{ element.matches.string_expression }}` +{%- endif %} + +{%- if element.concepts %} +**Concepts:** {{ gen.uri_links(element.concepts) | join(', ') }} +{%- endif %} + +{% endif %} + +{% if element.permissible_values -%} +{%- set has_pv_extras = namespace(found=false) -%} +{%- for pv in element.permissible_values.values() -%} + {%- if pv.title or pv.is_a or pv.mixins or pv.deprecated -%} + {%- set has_pv_extras.found = true -%} + {%- endif -%} +{%- endfor -%} +## Permissible Values + +{%- if has_pv_extras.found %} +| Value | Meaning | Description | Additional Info | +| --- | --- | --- | --- | +{% for pv in element.permissible_values.values() -%} +| {{ pv.text }} | {{ pv.meaning }} | {{ pv.description|enshorten }} | +{%- if pv.title %} Title: {{ pv.title }}
{% endif -%} +{%- if pv.is_a %} Is-A: {{ gen.link(pv.is_a) }}
{% endif -%} +{%- if pv.mixins %} Mixins: {{ gen.links(pv.mixins) | join(', ') }}
{% endif -%} +{%- if pv.deprecated %} **DEPRECATED**{% if pv.deprecated_element_has_exact_replacement %} (use {{ gen.link(pv.deprecated_element_has_exact_replacement) }}){% endif %}{% endif -%} + | +{% endfor %} +{%- else %} +| Value | Meaning | Description | +| --- | --- | --- | +{% for pv in element.permissible_values.values() -%} +| {{ pv.text }} | {{ pv.meaning }} | {{ pv.description|enshorten }} | +{% endfor %} +{%- endif %} +{% else %} +_This is a dynamic enum_ +{% endif %} + +{%- set has_enum_ops = element.inherits or element.include or element.minus %} + +{%- if has_enum_ops %} +## Enumeration Operations + +{%- if element.inherits %} +**Inherits From:** {{ gen.links(element.inherits) | join(', ') }} +{%- endif %} + +{%- if element.include %} +**Includes:** {{ gen.links(element.include) | join(', ') }} +{%- endif %} + +{%- if element.minus %} +**Excludes:** {{ gen.links(element.minus) | join(', ') }} +{%- endif %} + +{% endif %} + +{% set slots_for_enum = schemaview.get_slots_by_enum(element.name) %} +{% if slots_for_enum is defined and slots_for_enum|length > 0 -%} +## Slots + +| Name | Description | +| --- | --- | +{% for s in schemaview.get_slots_by_enum(element.name) -%} +| {{ gen.link(s) }} | {{ s.description|enshorten }} | +{% endfor %} +{% endif %} + +{% include "common_metadata.md.jinja2" %} + +## LinkML Source + +
+```yaml +{{ gen.yaml(element) }} +``` +
diff --git a/tools/templates/model/index.md.jinja2 b/tools/templates/model/index.md.jinja2 new file mode 100644 index 00000000..a6894b19 --- /dev/null +++ b/tools/templates/model/index.md.jinja2 @@ -0,0 +1,94 @@ + +# {% if schema.title %}{{ schema.title }}{% else %}{{ schema.name }}{% endif %} + +{% if schema.description %}{{ schema.description }}{% endif %} + +{# +URI: {{ schema.id }} + +Name: {{ schema.name }} +#} + +{% if include_top_level_diagram %} + +## Schema Diagram + +```{{ gen.mermaid_directive() }} +{{ gen.mermaid_diagram() }} +``` +{% endif %} + +## Interfacing Classes + +These are the classes directly participating in Margo interfaces (e.g. APIs): + +| Class | Description | +| --- | --- | +{% for cn in ["ApplicationDescription", "ApplicationDeployment", "DeviceCapabilitiesManifest", "DesiredStateManifest", "DeploymentStatusManifest"] -%} +| {{ gen.link(schemaview.get_class(cn), True) }} | {{ schemaview.get_class(cn).description|enshorten }} | +{% endfor %} + +??? note "Complete Margo data model" + + ## Detailed Class Diagram + + This is a class diagram showing all the classes involved in the Margo data model. + Use the mouse wheel to zoom and click-drag to pan. The controls in the bottom-right corner allow zooming in, resetting, and zooming out. + +
+
Scroll to zoom · Drag to pan · Use controls at bottom-right to zoom in/out/reset
+ + ## All Classes + + These are all the classes involved in the Margo data model: + + | Class | Description | + | --- | --- | + {% if gen.hierarchical_class_view -%} + {% for u, v in gen.class_hierarchy_as_tuples() -%} + | {{ " "|safe*u*8 }}{{ gen.link(schemaview.get_class(v), True) }} | {{ schemaview.get_class(v).description|enshorten }} | + {% endfor %} + {% else -%} + {% for c in gen.all_class_objects()|sort(attribute=sort_by) -%} + | {{ gen.link(c, True) }} | {{ c.description|enshorten }} | + {% endfor %} + {% endif %} + +{# +## Slots + +| Slot | Description | +| --- | --- | +{% for s in gen.all_slot_objects()|sort(attribute=sort_by) -%} +| {{ gen.link(s, True) }} | {{ s.description|enshorten }} | +{% endfor %} + +## Enumerations + +| Enumeration | Description | +| --- | --- | +{% for e in gen.all_enum_objects()|sort(attribute=sort_by) -%} +| {{ gen.link(e, True) }} | {{ e.description|enshorten }} | +{% endfor %} + +## Types + +| Type | Description | +| --- | --- | +{% for t in gen.all_type_objects()|sort(attribute=sort_by) -%} +| {{ gen.link(t, True) }} | {{ t.description|enshorten }} | +{% endfor %} + +## Subsets + +| Subset | Description | +| --- | --- | +{% for ss in schemaview.all_subsets().values()|sort(attribute='name') -%} +| {{ gen.link(ss, True) }} | {{ ss.description|enshorten }} | +{% endfor %} +#} diff --git a/tools/templates/model/slot.md.jinja2 b/tools/templates/model/slot.md.jinja2 new file mode 100644 index 00000000..75344cf5 --- /dev/null +++ b/tools/templates/model/slot.md.jinja2 @@ -0,0 +1,454 @@ +{%- if element.title %} + {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%} +{%- else %} + {%- if gen.use_slot_uris -%} + {%- set title = element.name -%} + {%- else -%} + {%- set title = gen.name(element) -%} + {%- endif -%} +{%- endif -%} + +{% macro compute_range(slot) -%} + {%- if slot.any_of or slot.exactly_one_of -%} + {%- for subslot_range in schemaview.slot_range_as_union(slot) -%} + {{ gen.link(subslot_range) }} + {%- if not loop.last -%} +  or 
+ {%- endif -%} + {%- endfor -%} + {%- else -%} + {{ gen.link(slot.range) }} + {%- endif -%} +{% endmacro %} + +# Slot: {{ title }} {% if element.deprecated %} (DEPRECATED) {% endif %} + +{%- if header -%} +{{ header }} +{%- endif -%} + +{% if element.description %} +{% set element_description_lines = element.description.split('\n') %} +{% for element_description_line in element_description_lines %} +_{{ element_description_line }}_ +{% endfor %} +{% endif %} + +{% if element.abstract %} +* __NOTE__: this is an abstract slot and should not be populated directly +{% endif %} + +URI: {{ gen.uri_link(element) }} + +{%- if element.alias %} +Alias: {{ element.alias }} +{% endif -%} + +{% if schemaview.slot_parents(element.name) or schemaview.slot_children(element.name, mixins=False) %} + +## Inheritance + +{{ gen.inheritance_tree(element, mixins=True) }} +{% else %} + +{% endif %} + +{% set classes_by_slot = schemaview.get_classes_by_slot(element, include_induced=True) %} +{% if classes_by_slot %} + +## Applicable Classes + +| Name | Description | Modifies Slot | +| --- | --- | --- | +{% for c in classes_by_slot -%} +| {{ gen.link(c) }} | {{ schemaview.get_class(c).description|enshorten }} | {% if c in schemaview.get_classes_modifying_slot(element) %} yes {% else %} no {% endif %} | +{% endfor %} + +{% endif %} + +{% if schemaview.is_mixin(element.name) %} +## Mixin Usage + +| mixed into | description | range | domain | +| --- | --- | --- | --- | +{% for s in schemaview.slot_children(element.name, is_a=False) -%} +| {{ gen.link(s) }} | {{ schemaview.get_slot(s).description|enshorten }} | {{ schemaview.get_slot(s).range }} | {{ schemaview.get_classes_by_slot(schemaview.get_slot(s))|join(', ') }} | +{% endfor %} +{% endif %} + +## Properties + +### Type and Range + +| Property | Value | +| --- | --- | +| Range | {{ compute_range(element) }} | +{%- if element.domain %} +| Domain | {{ gen.link(element.domain) }} | +{%- endif %} +{%- if element.domain_of %} +| Domain Of | {{ gen.links(element.domain_of) | join(', ') }} | +{%- endif %} +{%- if element.slot_uri %} +| Slot URI | {{ gen.uri_link(element.slot_uri) }} | +{%- endif %} +{%- if element.slot_group %} +| Slot Group | {{ gen.link(element.slot_group) }} | +{%- endif %} +{%- if element.is_grouping_slot %} +| Is Grouping Slot | Yes | +{%- endif %} + +### Cardinality and Requirements + +| Property | Value | +| --- | --- | +{%- if element.required %} +| Required | Yes | +{%- elif element.recommended %} +| Recommended | Yes | +{%- endif %} +{%- if element.multivalued %} +| Multivalued | Yes | +{%- endif %} +{%- if element.minimum_cardinality is not none %} +| Minimum Cardinality | {{ element.minimum_cardinality }} | +{%- endif %} +{%- if element.maximum_cardinality is not none %} +| Maximum Cardinality | {{ element.maximum_cardinality }} | +{%- endif %} +{%- if element.exact_cardinality is not none %} +| Exact Cardinality | {{ element.exact_cardinality }} | +{%- endif %} + +{%- if element.multivalued and (element.list_elements_unique is not none or element.list_elements_ordered is not none) %} +### List/Collection Properties + +| Property | Value | +| --- | --- | +{%- if element.list_elements_unique is not none %} +| Elements Must Be Unique | {% if element.list_elements_unique %}Yes{% else %}No{% endif %} | +{%- endif %} +{%- if element.list_elements_ordered is not none %} +| Elements Are Ordered | {% if element.list_elements_ordered %}Yes{% else %}No{% endif %} | +{%- endif %} + +{% endif %} +{%- set has_slot_chars = element.key or element.identifier or element.designates_type + or element.inherited or element.readonly or element.ifabsent or element.owner + or element.shared or element.is_class_field or element.is_usage_slot + or element.usage_slot_name or element.singular_name or schemaview.is_mixin(element.name) %} + +{%- if has_slot_chars %} +### Slot Characteristics + +| Property | Value | +| --- | --- | +{%- if element.singular_name %} +| Singular Name | {{ element.singular_name }} | +{%- endif %} +{%- if element.key %} +| Key | Yes | +{%- endif %} +{%- if element.identifier %} +| Identifier | Yes | +{%- endif %} +{%- if element.designates_type %} +| Designates Type | Yes | +{%- endif %} +{%- if element.inherited %} +| Inherited | Yes | +{%- endif %} +{%- if element.readonly %} +| Readonly | Yes | +{%- endif %} +{%- if element.ifabsent %} +| If Absent | `{{ element.ifabsent }}` | +{%- endif %} +{%- if element.owner %} +| Owner | {{ gen.link(element.owner) }} | +{%- endif %} +{%- if element.shared %} +| Shared | Yes | +{%- endif %} +{%- if element.is_class_field %} +| Is Class Field | Yes | +{%- endif %} +{%- if element.is_usage_slot %} +| Is Usage Slot | Yes | +{%- endif %} +{%- if element.usage_slot_name %} +| Usage Slot Name | {{ element.usage_slot_name }} | +{%- endif %} +{%- if schemaview.is_mixin(element.name) %} +| Mixin | Yes | +{%- endif %} + +{% endif %} +{%- set has_basic_constraints = element.minimum_value is not none or element.maximum_value is not none or element.pattern %} + +{%- if has_basic_constraints %} +### Value Constraints + +| Property | Value | +| --- | --- | +{%- if element.minimum_value is not none %} +| Minimum Value | {{ element.minimum_value|int }} | +{%- endif %} +{%- if element.maximum_value is not none %} +| Maximum Value | {{ element.maximum_value|int }} | +{%- endif %} +{%- if element.pattern %} +| Regex Pattern | `{{ element.pattern }}` | +{%- endif %} + +{% endif %} +{%- set has_advanced_constraints = element.structured_pattern or element.equals_string + or element.equals_string_in or element.equals_number or element.enum_range + or element.unit or element.implicit_prefix %} + +{%- if has_advanced_constraints %} +
+Additional Constraints + +{%- if element.structured_pattern %} +**Structured Pattern:** + +- **Syntax:** `{{ element.structured_pattern.syntax }}` +- **Interpolated:** {{ element.structured_pattern.interpolated }} +{%- if element.structured_pattern.partial_match %} +- **Partial Match:** Yes +{%- endif %} +{%- endif %} + +{%- if element.equals_string %} +**Must Equal:** `{{ element.equals_string }}` +{%- endif %} + +{%- if element.equals_string_in %} +**Must Be One Of:** {{ element.equals_string_in | join(', ') }} +{%- endif %} + +{%- if element.equals_number %} +**Must Equal:** {{ element.equals_number }} +{%- endif %} + +{%- if element.enum_range %} +**Enumeration Range:** {{ gen.link(element.enum_range) }} +{%- endif %} + +{%- if element.unit %} +**Unit:** {{ gen.uri_link(element.unit) }} +{%- endif %} + +{%- if element.implicit_prefix %} +**Implicit Prefix:** {{ element.implicit_prefix }} +{%- endif %} + +
+{% endif %} + +{%- set has_rel_props = element.symmetric or element.asymmetric or element.reflexive + or element.locally_reflexive or element.irreflexive or element.transitive + or element.inverse or element.transitive_form_of or element.reflexive_transitive_form_of + or element.role or element.relational_role %} + +{%- if has_rel_props %} +
+Relationship Properties + +| Property | Value | +| --- | --- | +{%- if element.symmetric %} +| Symmetric | Yes | +{%- endif %} +{%- if element.asymmetric %} +| Asymmetric | Yes | +{%- endif %} +{%- if element.reflexive %} +| Reflexive | Yes | +{%- endif %} +{%- if element.locally_reflexive %} +| Locally Reflexive | Yes | +{%- endif %} +{%- if element.irreflexive %} +| Irreflexive | Yes | +{%- endif %} +{%- if element.transitive %} +| Transitive | Yes | +{%- endif %} +{%- if element.inverse %} +| Inverse | {{ gen.link(element.inverse) }} | +{%- endif %} +{%- if element.transitive_form_of %} +| Transitive Form Of | {{ gen.link(element.transitive_form_of) }} | +{%- endif %} +{%- if element.reflexive_transitive_form_of %} +| Reflexive Transitive Form Of | {{ gen.link(element.reflexive_transitive_form_of) }} | +{%- endif %} +{%- if element.role %} +| Role | {{ element.role }} | +{%- endif %} +{%- if element.relational_role %} +| Relational Role | {{ element.relational_role }} | +{%- endif %} + +
+{% endif %} + +{%- set has_advanced = element.path_rule or element.disjoint_with + or element.children_are_mutually_disjoint or element.subproperty_of + or element.array or element.bindings or element.type_mappings + or element.value_presence or element.range_expression %} + +{%- if has_advanced %} +
+Advanced Properties + +{%- if element.subproperty_of %} +**Subproperty Of:** {{ gen.link(element.subproperty_of) }} +{%- endif %} + +{%- if element.path_rule %} +**Path Rule:** + +``` +{{ element.path_rule }} +``` +{%- endif %} + +{%- if element.disjoint_with %} +**Disjoint With:** {{ gen.links(element.disjoint_with) | join(', ') }} +{%- endif %} + +{%- if element.children_are_mutually_disjoint %} +**Children Are Mutually Disjoint:** Yes +{%- endif %} + +{%- if element.array %} +**Array Configuration:** + +- **Dimensions:** {{ element.array.dimensions | join(' x ') }} +{%- if element.array.exact_number_dimensions %} +- **Exact Dimensions Required:** Yes +{%- endif %} +{%- endif %} + +{%- if element.range_expression %} +**Range Expression:** {{ element.range_expression }} +{%- endif %} + +{%- if element.value_presence %} +**Value Presence:** {{ element.value_presence }} +{%- endif %} + +{%- if element.bindings %} +**Term Bindings:** + +{%- for binding in element.bindings %} +- {{ binding }} +{%- endfor %} +{%- endif %} + +{%- if element.type_mappings %} +**Type Mappings:** + +{%- for tm in element.type_mappings %} +- **Framework:** {{ tm.framework }}, **Mapping:** {{ tm.mapping }} +{%- endfor %} +{%- endif %} + +
+{% endif %} + +{%- set has_expressions = element.any_of or element.all_of or element.exactly_one_of + or element.none_of or element.equals_expression or element.has_member or element.all_members %} + +{%- if has_expressions %} +
+Expressions & Logic + +{%- if element.any_of %} +#### Any Of + +Value must satisfy at least one of: + +{%- for expr in element.any_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.all_of %} +#### All Of + +Value must satisfy all of: + +{%- for expr in element.all_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.exactly_one_of %} +#### Exactly One Of + +Value must satisfy exactly one of: + +{%- for expr in element.exactly_one_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.none_of %} +#### None Of + +Value must not satisfy any of: + +{%- for expr in element.none_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.equals_expression %} +#### Equals Expression + +`{{ element.equals_expression }}` +{%- endif %} + +{%- if element.has_member %} +#### Has Member + +{{ element.has_member }} +{%- endif %} + +{%- if element.all_members %} +#### All Members + +{{ element.all_members }} +{%- endif %} + +
+{% endif %} + +{% if schemaview.usage_index().get(element.name) %} +## Usages + +| used by | used in | type | used | +| --- | --- | --- | --- | +{% for usage in schemaview.usage_index().get(element.name) -%} +| {{ gen.link(usage.used_by) }} | {{ gen.link(usage.slot) }} | {{ usage.metaslot }} | {{ gen.link(usage.used) }} | +{% endfor %} +{% endif %} + +{% include "common_metadata.md.jinja2" %} + +## LinkML Source + +
+```yaml +{{ gen.yaml(element) }} +``` +
+ +{%- if footer -%} +{{ footer }} +{%- endif -%} diff --git a/tools/templates/model/subset.md.jinja2 b/tools/templates/model/subset.md.jinja2 new file mode 100644 index 00000000..91e75e2b --- /dev/null +++ b/tools/templates/model/subset.md.jinja2 @@ -0,0 +1,108 @@ +{# +# Subset: {{ gen.name(element) }} {% if element.deprecated %} (DEPRECATED) {% endif %} +#} +# Subset: {{ element.title }} {% if element.deprecated %} (DEPRECATED) {% endif %} + +{%- if header -%} +{{ header }} +{%- endif -%} + +{% if element.description %} +{% set element_description_lines = element.description.split('\n') %} +{% for element_description_line in element_description_lines %} +_{{ element_description_line }}_ +{% endfor %} +{% endif %} + +{# +URI: {{ gen.link(element) }} +#} + +{% include "common_metadata.md.jinja2" %} + +{% set classes_in_subset = [] %} +{% set slots_in_subset = [] %} +{% set enums_in_subset = [] %} + +{# Collect classes, slots, and enumerations in subset #} +{% for c in gen.all_class_objects()|sort(attribute=sort_by) %} + {%- if element.name in c.in_subset %} + {% set _ = classes_in_subset.append(c) %} + {%- endif %} +{% endfor %} + +{% for s in gen.all_slot_objects()|sort(attribute=sort_by) %} + {%- if element.name in s.in_subset %} + {% set _ = slots_in_subset.append(s) %} + {%- endif %} +{% endfor %} + +{% for e in schemaview.all_enums().values() %} + {%- if element.name in e.in_subset %} + {% set _ = enums_in_subset.append(e) %} + {%- endif %} +{% endfor %} + +{% if classes_in_subset %} +## Classes in subset + +| Class | Description | +| --- | --- | +{% for c in classes_in_subset -%} +{%- if element.name in c.in_subset -%} +| {{ gen.link(c) }} | {{ c.description|enshorten }} | +{% endif -%} +{% endfor %} + +{% for c in classes_in_subset -%} +{%- if element.name in c.in_subset -%} + +{% set induced_slots = gen.class_induced_slots(c.name)|sort(attribute=sort_by) %} + +{%- set filtered_slots = [] -%} +{%- for s in induced_slots|sort(attribute=sort_by) -%} + {%- if element.name in s.in_subset or element.name in schemaview.get_slot(s.name).in_subset -%} + {% set _ = filtered_slots.append(s) %} + {%- endif -%} +{%- endfor %} + +{%- if filtered_slots|length > 0 -%} +### Slots from {{ gen.link(c) }} also in _{{ element.name }}_ + +| Name | Cardinality and Range | Description | +| --- | --- | --- | +{% for s in filtered_slots -%} +| {{ gen.link(s) }} | {{ gen.cardinality(s) }}
{{ gen.link(s.range) }} | {{ s.description|enshorten }} {% if s.identifier %}**identifier**{% endif %} | +{% endfor %} +{%- endif %} + +{%- endif %} +{% endfor %} + +{%- endif %} + +{% if slots_in_subset %} +## Slots in subset + +| Slot | Description | +| --- | --- | +{% for s in slots_in_subset|sort(attribute=sort_by) -%} +{%- if element.name in s.in_subset -%} +| {{ gen.link(s) }} | {{ s.description|enshorten }} | +{%- endif %} +{% endfor %} + +{%- endif %} + +{% if enums_in_subset %} +## Enumerations in subset + +| Enumeration | Description | +| --- | --- | +{% for e in enums_in_subset|sort(attribute='name') -%} +{% if element.name in e.in_subset -%} +| {{ gen.link(e) }} | {{ e.description|enshorten }} | +{%- endif %} +{% endfor %} + +{%- endif %} diff --git a/tools/templates/model/type.md.jinja2 b/tools/templates/model/type.md.jinja2 new file mode 100644 index 00000000..e109b47a --- /dev/null +++ b/tools/templates/model/type.md.jinja2 @@ -0,0 +1,134 @@ +{%- if element.title and element.title != element.name %} + {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%} +{%- else %} + {%- set title = gen.name(element) -%} +{%- endif -%} + +# Type: {{ title }} {% if element.deprecated %} (DEPRECATED) {% endif %} + +{% if element.description %} +{% set element_description_lines = element.description.split('\n') %} +{% for element_description_line in element_description_lines %} +_{{ element_description_line }}_ +{% endfor %} +{% endif %} + +URI: {{ gen.uri_link(element) }} + +## Type Properties + +| Property | Value | +| --- | --- | +{%- if element.typeof %} +| Type Of | {{ gen.link(element.typeof) }} | +{%- endif %} +{%- if element.base %} +| Base | `{{ element.base }}` | +{%- endif %} +{%- if element.uri %} +| Type URI | {{ gen.uri_link(element.uri) }} | +{%- endif %} +{%- if element.repr %} +| Representation | `{{ element.repr }}` | +{%- endif %} +{%- if element.union_of %} +| Union Of | {{ gen.links(element.union_of) | join(', ') }} | +{%- endif %} + +{%- set has_basic_constraints = element.minimum_value is not none or element.maximum_value is not none or element.pattern %} + +{%- if has_basic_constraints %} +## Value Constraints + +| Property | Value | +| --- | --- | +{%- if element.minimum_value is not none or element.maximum_value is not none %} +| Numeric Range | {{ gen.number_value_range(element) }} | +{%- endif %} +{%- if element.pattern %} +| Regex Pattern | `{{ element.pattern }}` | +{%- endif %} + +{% endif %} +{%- set has_advanced_constraints = element.structured_pattern or element.equals_string + or element.equals_string_in or element.equals_number or element.unit or element.implicit_prefix %} + +{%- if has_advanced_constraints %} +
+Additional Constraints + +{%- if element.structured_pattern %} +**Structured Pattern:** + +- **Syntax:** `{{ element.structured_pattern.syntax }}` +- **Interpolated:** {{ element.structured_pattern.interpolated }} +{%- if element.structured_pattern.partial_match %} +- **Partial Match:** Yes +{%- endif %} +{%- endif %} + +{%- if element.equals_string %} +**Must Equal:** `{{ element.equals_string }}` +{%- endif %} + +{%- if element.equals_string_in %} +**Must Be One Of:** {{ element.equals_string_in | join(', ') }} +{%- endif %} + +{%- if element.equals_number %} +**Must Equal:** {{ element.equals_number }} +{%- endif %} + +{%- if element.unit %} +**Unit:** {{ gen.uri_link(element.unit) }} +{%- endif %} + +{%- if element.implicit_prefix %} +**Implicit Prefix:** {{ element.implicit_prefix }} +{%- endif %} + +
+{% endif %} + +{%- set has_expressions = element.any_of or element.all_of or element.exactly_one_of or element.none_of %} + +{%- if has_expressions %} +
+Type Expressions + +{%- if element.any_of %} +**Any Of:** Value must satisfy at least one of these expressions + +{%- for expr in element.any_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.all_of %} +**All Of:** Value must satisfy all of these expressions + +{%- for expr in element.all_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.exactly_one_of %} +**Exactly One Of:** Value must satisfy exactly one of these expressions + +{%- for expr in element.exactly_one_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +{%- if element.none_of %} +**None Of:** Value must not satisfy any of these expressions + +{%- for expr in element.none_of %} +- {{ expr }} +{%- endfor %} +{%- endif %} + +
+{% endif %} + +{% include "common_metadata.md.jinja2" %} diff --git a/tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml b/tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml new file mode 100644 index 00000000..127568ec --- /dev/null +++ b/tools/templates/openapi/workload-management-api-1.0.0.openapi.yaml @@ -0,0 +1,473 @@ +openapi: 3.1.0 +info: + title: Margo Workload Management API + version: 1.0.0 + description: + API for managing workloads on Margo-compliant edge devices. + Includes the APIs for exchanging desired state and current state. + Communication is secured using server-side TLS (TLS 1.3 preferred), + and payloads are signed using X.509 certificates. + +servers: + - url: https://wfm.margo.org/ + description: Workload Fleet Manager API + +security: + - PayloadSignature: [] + +paths: + /api/v1/onboarding/certificate: + get: + summary: Download Root CA certificate + security: [] + responses: + '200': + description: Root CA certificate + content: + application/json: + schema: + type: object + properties: + certificate: + type: string + description: Base64-encoded certificate text + /api/v1/onboarding: + post: + requestBody: + content: + application/json: + schema: + type: object + required: [apiVersion, kind, certificate] + properties: + apiVersion: + type: string + description: API version identifier + kind: + type: string + enum: [OnboardingRequest] + description: Resource kind + certificate: + description: Base64-encoded client certificate + type: string + required: true + responses: + '201': + content: + application/json: + schema: + properties: + clientId: + type: string + type: object + description: New client onboarded successfully. + '400': + content: + application/json: + schema: + properties: + error: + example: Invalid certificate + type: string + type: object + description: Invalid certificate format or structure. + '403': + content: + application/json: + schema: + properties: + error: + example: Client rejected + type: string + type: object + description: Client certificate not trusted or client rejected. + security: + - PayloadSignature: [] + summary: Complete onboarding with client certificate + + /api/v1/clients/{clientId}/capabilities/{deviceId}: + post: + summary: Report device capabilities + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + - name: deviceId + in: path + required: true + schema: + $ref: '#/components/schemas/DeviceId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceCapabilitiesManifest' + responses: + '201': + description: Capabilities reported successfully + '400': + description: Missing or invalid content-digest header. Ensure the SHA256 hash of the payload is included. + '401': + description: Signature verification failed. Ensure you are signing with the correct X.509 private key. + '403': + description: Client certificate is not trusted or has been revoked. + '404': + description: No client with the given `clientID` was found. + '422': + description: Request body includes a semantic error. + put: + summary: Update device capabilities (Update) + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + - name: deviceId + in: path + required: true + schema: + $ref: '#/components/schemas/DeviceId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceCapabilitiesManifest' + responses: + '201': + description: Capabilities reported successfully + '400': + description: Missing or invalid content-digest header. Ensure the SHA256 hash of the payload is included. + '401': + description: Signature verification failed. Ensure you are signing with the correct X.509 private key. + '403': + description: Client certificate is not trusted or has been revoked. + '404': + description: No client with the given `clientID` was found. + '422': + description: Request body includes a semantic error. + delete: + summary: Remove device (Unregister) + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + - name: deviceId + in: path + required: true + schema: + $ref: '#/components/schemas/DeviceId' + responses: + '204': + description: Device capabilities removed successfully + '400': + description: Missing or invalid content-digest header. Ensure the SHA256 hash of the payload is included. + '401': + description: Signature verification failed. Ensure you are signing with the correct X.509 private key. + '403': + description: Client certificate is not trusted or has been revoked. + '404': + description: Client or device not found. + /api/v1/clients/{clientId}/bundles/{digest}: + get: + summary: Retrieve bundle information for a specific device and digest + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + description: Unique identifier of the device-client + - name: digest + in: path + required: true + schema: + type: string + description: Content-addressable digest of the bundle archive. MUST conform to the 'digest' attribute in the Digest Specification and MUST equal the digest computed over the exact sequence of bytes (per Exact Bytes Rule) in the HTTP 200 OK response body. If the server cannot produce content whose digest matches this value it MUST return 404 Not Found. + - in: header + name: If-None-Match + required: false + schema: + type: string + description: Quoted ETag (same as digest) previously returned for this bundle. + responses: + '200': + description: Bundle archive (immutable) + headers: + ETag: + schema: + type: string + description: New ETag for the returned manifest + Cache-Control: + schema: + type: string + description: public, max-age=31536000, immutable + content: + application/vnd.margo.bundle.v1+tar+gzip: + schema: + type: string + format: binary + description: Gzip-compressed tar containing one YAML file per deployment. + '304': + description: Representation not modified + '404': + description: Bundle not found for the given digest + '400': + description: Invalid request. + # TBD + # '500': + # $ref: '#/components/responses/ErrorResponse' + + /api/v1/clients/{clientId}/deployments: + get: + summary: Retrieve the complete desired state for all workloads assigned to a device + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + description: The unique identifier of the Edge Compute Device making the request + - name: If-None-Match + in: header + required: false + schema: + type: string + description: > + ETag value of the last successfully synced manifest. The ETag is returned to the client from the /deployments endpoint, it is the digest of the state manifest. + - name: Accept + in: header + required: false + schema: + type: string + description: > + Indicates which manifest formats the client supports. + Supported values: application/vnd.margo.manifest.v1+json. + responses: + '200': + description: Manifest returned in the negotiated format + headers: + Content-Type: + schema: + type: string + description: Format of the returned manifest + ETag: + schema: + type: string + description: New ETag for the returned manifest + content: + application/vnd.margo.manifest.v1+json: + schema: + $ref: '#/components/schemas/UnsignedAppStateManifest' + '304': + description: Not Modified - Manifest has not changed + '406': + description: Not Acceptable - Server cannot generate a response matching the Accept header + + + /api/v1/clients/{clientId}/deployments/{deploymentId}/{digest}: + get: + summary: Retrieve an individual ApplicationDeployment YAML file + security: + - PayloadSignature: [] + description: > + This endpoint is used by the client to fetch the YAML for a single ApplicationDeployment after it has processed a new State Manifest and identified a small number of new or updated deployments. This allows for highly efficient, incremental updates without needing to download the full bundle. + To make individual workload retrievals race-free and cache-friendly, this endpoint is content-addressable: the digest of the expected YAML is part of the URL. This guarantees immutability of the fetched resource and prevents a time-of-check / time-of-use race where a deployment changes between manifest retrieval and content fetch. + parameters: + - name: clientId + in: path + required: true + schema: + type: string + description: Unique identifier of the Edge Compute Device + - name: deploymentId + in: path + required: true + schema: + type: string + description: Unique identifier for the application deployment + - name: digest + in: path + required: true + schema: + type: string + description: > + Content-addressable digest of the ApplicationDeployment YAML file. MUST conform to the Digest Specification and MUST equal the digest computed over the exact sequence of bytes (per Exact Bytes Rule) in the HTTP 200 OK response body. If the server cannot produce content whose digest matches this value it MUST return 404 Not Found. + - name: If-None-Match + in: header + required: false + schema: + type: string + description: > + Optional ETag for caching. The ETag is returned to the client from the /deployments endpoint, it is the digest of the state manifest. + - name: Accept-Encoding + in: header + required: false + schema: + type: string + description: Indicates supported compression formats (e.g., gzip, br) + responses: + '200': + description: > + The response body is the raw ApplicationDeployment YAML file (Content-Type: application/yaml). The content MUST match the {digest} path segment; the server MUST return 404 if it does not have the exact digest referenced. + headers: + Content-Type: + schema: + type: string + description: application/yaml + ETag: + schema: + type: string + description: > + The ETag is returned to the client from the /deployments endpoint, it is the digest of the state manifest. + Cache-Control: + schema: + type: string + description: public, max-age=31536000, immutable + Vary: + schema: + type: string + description: Accept-Encoding + content: + application/yaml: + schema: + type: string + description: Raw YAML content of the ApplicationDeployment + '404': + description: Deployment not found for the given digest + + /api/v1/clients/{clientId}/deployments/{deploymentId}/status: + post: + summary: Report deployment status + security: + - PayloadSignature: [] + parameters: + - name: clientId + in: path + required: true + schema: + type: string + - name: deploymentId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeploymentStatusManifest' + responses: + '200': + description: The deployment status was added, or updated, successfully. + '400': + description: Missing or invalid content-digest header. Ensure the SHA256 hash of the base64-encoded payload is included. + '401': + description: Signature verification failed. Ensure you are signing with the correct X.509 private key. + '403': + description: Client certificate is not trusted or has been revoked. + '422': + description: Request body includes a semantic error. + +components: + securitySchemes: + # TODO: fix this as we are following RFC 9421, instead of a custom signature header field + PayloadSignature: + type: apiKey + in: header + name: X-Payload-Signature + description: > + Base64-encoded payload signature using SHA-256 and device certificate. + Format: public_key;digital_signature + + # Modifications compared to pre-data-model stage: + # - ManifestVersion INLINED into UnsignedAppStateManifest(OpenAPI)/DesiredStateManifest(LinkML) + # - appDeploymentParams INLINED into appDeploymentSpec(OpenAPI)/Spec(LinkML) + schemas: + UnsignedAppStateManifest: + type: object + description: >- + Manifest from the Workload Fleet Manager, representing the complete desired workload configuration assigned to the device. + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: DesiredStateManifest + DeviceCapabilitiesManifest: + type: object + description: >- + Capabilities of a device on which applications can be deployed. + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: DeviceCapabilitiesManifest + DeploymentStatusManifest: + description: >- + Manifest sent by the device client to report the deployment status of a workload. + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: DeploymentStatusManifest + DeviceId: + description: >- + Device properties reported to the WFM. + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: FlatDeviceId + DeviceId_with_asterisk: + description: >- + Device properties reported to the WFM. + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: HierarchicalDeviceId + appDeploymentManifest: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: ApplicationDeployment + appDeploymentMetadata: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: DeploymentMetadata + appDeploymentProfile: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: DeploymentProfile + appDeploymentSpec: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: Spec + appParameterTarget: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: Target + DeploymentBundleRef: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: Bundle + DeploymentManifestRef: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: Deployment + helmApplicationDeploymentProfileComponent: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: HelmComponent + composeApplicationDeploymentProfileComponent: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: ComposeComponent + appParameterValue: + type: object + x-linkml-schema: https://specification.margo.org/data-model + x-linkml-source: Parameter.identifier