diff --git a/.github/scripts/check_links.py b/.github/scripts/check_links.py
index 351549a85c..91d06622e0 100644
--- a/.github/scripts/check_links.py
+++ b/.github/scripts/check_links.py
@@ -52,6 +52,8 @@
"snomed.info",
# LinkML metamodel - w3id redirects to linkml.io/linkml-model which may have issues
"w3id.org",
+ # Certificate expired Sep/24/2026
+ "datashapes.org",
}
diff --git a/.github/workflows/check-external-links.yaml b/.github/workflows/check-external-links.yaml
index a99ad80708..fe2ad217d0 100644
--- a/.github/workflows/check-external-links.yaml
+++ b/.github/workflows/check-external-links.yaml
@@ -28,7 +28,7 @@ jobs:
python-version: "3.12"
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
diff --git a/.github/workflows/dependency-audit.yaml b/.github/workflows/dependency-audit.yaml
index 1f6916dc13..a58832496c 100644
--- a/.github/workflows/dependency-audit.yaml
+++ b/.github/workflows/dependency-audit.yaml
@@ -38,7 +38,7 @@ jobs:
# Pin uv to a known-good, recent release.
- name: Install uv and setup uv caching
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
@@ -56,7 +56,10 @@ jobs:
#
# So gate the audit on whether the change actually altered dependencies,
# relative to each event's natural base:
- # * pull_request -> the PR base
+ # * pull_request -> the base the checked-out merge commit was built on
+ # (HEAD^1). Not github.event.pull_request.base.sha: that can be an
+ # older main commit, so the diff would include main's own uv.lock
+ # bumps and audit a PR that changed no dependencies.
# * push (main) -> the commit before the push (github.event.before)
# * merge_group -> the queue base
# * otherwise (workflow_dispatch, first/force push) -> audit
@@ -78,7 +81,7 @@ jobs:
run: |
set -euo pipefail
case "${{ github.event_name }}" in
- pull_request) base="${{ github.event.pull_request.base.sha }}" ;;
+ pull_request) base="$(git rev-parse HEAD^1)" ;;
merge_group) base="${{ github.event.merge_group.base_sha }}" ;;
push) base="${{ github.event.before }}" ;;
*) base="" ;;
diff --git a/.github/workflows/doc-pages.yaml b/.github/workflows/doc-pages.yaml
index 27ffbe7353..627b248dc8 100644
--- a/.github/workflows/doc-pages.yaml
+++ b/.github/workflows/doc-pages.yaml
@@ -24,7 +24,7 @@ jobs:
git fetch upstream --tags
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml
index 7bd585d539..231b39a45e 100644
--- a/.github/workflows/docker-build.yaml
+++ b/.github/workflows/docker-build.yaml
@@ -48,10 +48,10 @@ jobs:
echo "Ref: ${{ github.ref }}"
- name: Set up QEMU
- uses: docker/setup-qemu-action@v4.2.0
+ uses: docker/setup-qemu-action@v4.4.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v4.3.0
+ uses: docker/setup-buildx-action@v4.4.1
- name: Login to DockerHub
if: startsWith(github.ref, 'refs/tags/v')
@@ -61,7 +61,7 @@ jobs:
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push
- uses: docker/build-push-action@v7.3.0
+ uses: docker/build-push-action@v7.4.0
with:
context: .
platforms: linux/amd64,linux/arm64/v8
diff --git a/.github/workflows/docs-test.yaml b/.github/workflows/docs-test.yaml
index fd1cbd69e7..7f2f0a757b 100644
--- a/.github/workflows/docs-test.yaml
+++ b/.github/workflows/docs-test.yaml
@@ -36,7 +36,7 @@ jobs:
git fetch upstream --tags
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 85c7aa330d..a8c122e1bb 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -24,7 +24,7 @@ jobs:
steps:
- uses: actions/checkout@v7
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
@@ -76,7 +76,7 @@ jobs:
git fetch upstream --tags
- name: Install uv and setup uv caching
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
@@ -105,7 +105,7 @@ jobs:
shell: bash
- name: Upload linkml coverage report
if: github.repository == 'linkml/linkml' && github.actor != 'dependabot[bot]'
- uses: codecov/codecov-action@v7.0.0
+ uses: codecov/codecov-action@v7.1.1
with:
name: codecov-linkml-${{ matrix.os }}-${{ matrix.python-version }}
token: ${{ secrets.CODECOV_TOKEN }}
@@ -114,7 +114,7 @@ jobs:
fail_ci_if_error: true
- name: Upload runtime coverage report
if: github.repository == 'linkml/linkml' && github.actor != 'dependabot[bot]'
- uses: codecov/codecov-action@v7.0.0
+ uses: codecov/codecov-action@v7.1.1
with:
name: codecov-runtime-${{ matrix.os }}-${{ matrix.python-version }}
token: ${{ secrets.CODECOV_TOKEN }}
@@ -139,7 +139,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@v7
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
@@ -160,7 +160,7 @@ jobs:
uv run coverage report -m
- name: Upload linkml slow test coverage
if: github.repository == 'linkml/linkml' && github.actor != 'dependabot[bot]'
- uses: codecov/codecov-action@v7.0.0
+ uses: codecov/codecov-action@v7.1.1
with:
name: codecov-linkml-slow-${{ matrix.os }}-${{ matrix.python-version }}
token: ${{ secrets.CODECOV_TOKEN }}
@@ -189,7 +189,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@v7
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
@@ -207,7 +207,7 @@ jobs:
uv run pytest tests/linkml/test_notebooks/ -m "not kroki" --cov --cov-report xml:coverage-notebooks.xml --cov-report term-missing
- name: Upload notebook coverage
if: github.repository == 'linkml/linkml' && github.actor != 'dependabot[bot]'
- uses: codecov/codecov-action@v7.0.0
+ uses: codecov/codecov-action@v7.1.1
with:
name: codecov-notebooks-${{ matrix.os }}-${{ matrix.python-version }}
token: ${{ secrets.CODECOV_TOKEN }}
@@ -232,7 +232,7 @@ jobs:
python-version: 3.13
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
- name: Build source and wheel archives
diff --git a/.github/workflows/metamodel-compat.yaml b/.github/workflows/metamodel-compat.yaml
index 86917fbcb8..b9e7396eca 100644
--- a/.github/workflows/metamodel-compat.yaml
+++ b/.github/workflows/metamodel-compat.yaml
@@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@v7
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
diff --git a/.github/workflows/pypi-publish.yaml b/.github/workflows/pypi-publish.yaml
index d906594493..d5337b167c 100644
--- a/.github/workflows/pypi-publish.yaml
+++ b/.github/workflows/pypi-publish.yaml
@@ -23,7 +23,7 @@ jobs:
python-version: 3.13
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
diff --git a/.github/workflows/rustgen.yaml b/.github/workflows/rustgen.yaml
index b53f1a8ec5..14f62a1234 100644
--- a/.github/workflows/rustgen.yaml
+++ b/.github/workflows/rustgen.yaml
@@ -34,7 +34,7 @@ jobs:
git fetch upstream --tags
- name: Install uv and setup uv caching
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
@@ -63,7 +63,7 @@ jobs:
- name: Upload coverage report
if: github.repository == 'linkml/linkml' && github.actor != 'dependabot[bot]'
- uses: codecov/codecov-action@v7.0.0
+ uses: codecov/codecov-action@v7.1.1
with:
name: codecov-results-rustgen
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/typedb-integration.yaml b/.github/workflows/typedb-integration.yaml
index a8de2511de..c812050fa6 100644
--- a/.github/workflows/typedb-integration.yaml
+++ b/.github/workflows/typedb-integration.yaml
@@ -31,7 +31,7 @@ jobs:
--health-start-period 30s
steps:
- uses: actions/checkout@v7
- - uses: astral-sh/setup-uv@v10.0.1
+ - uses: astral-sh/setup-uv@v10.2.0
- uses: actions/setup-python@v7.0.0
with:
python-version: "3.12"
diff --git a/.github/workflows/update-feature-dashboard.yaml b/.github/workflows/update-feature-dashboard.yaml
index 02fb8fba59..cfde207543 100644
--- a/.github/workflows/update-feature-dashboard.yaml
+++ b/.github/workflows/update-feature-dashboard.yaml
@@ -32,7 +32,7 @@ jobs:
git fetch upstream --tags
- name: Install uv
- uses: astral-sh/setup-uv@v10.0.1
+ uses: astral-sh/setup-uv@v10.2.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
diff --git a/AGENTS.md b/AGENTS.md
index 772f7c59ab..c9a178e6c2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -23,6 +23,8 @@ All commands use `uv run` prefix (e.g., `uv run pytest`).
* Do not "fix" issues by changing or weakening test conditions. Try harder, or ask questions if a test fails.
* Avoid try/except blocks, these can mask bugs
* Failing fast is a good principle
+* Optional dependencies are the documented exception to the two rules above. Guard the import, not the spec: `try: import x` / `except ImportError as exc: raise ImportError("x is required. Install with: pip install 'linkml[extra]'") from exc` - see `generators/bigquerygen.py`. `importlib.util.find_spec` raises under the test fixture below, which hides your message.
+* Testing that an optional dependency is absent is not a mock test: use the `mock_missing_import` fixture in `tests/conftest.py`. Do not build isolated environments in CI for this.
* Follow the DRY principle
* Avoid repeating chunks of code, but also avoid premature over-abstraction
* Declarative principles are favored
diff --git a/docs/generators/openapi.rst b/docs/generators/openapi.rst
index d120e70296..dc5010999b 100644
--- a/docs/generators/openapi.rst
+++ b/docs/generators/openapi.rst
@@ -2,8 +2,9 @@ OpenAPI
=======
`OpenAPI `_ is a specification for describing
-RESTful HTTP APIs. The OpenAPI generator produces an OpenAPI v3.0.3
-specification in YAML from a LinkML schema.
+RESTful HTTP APIs. The OpenAPI generator produces an OpenAPI
+specification in YAML from a LinkML schema. As of now it supports OpenAPI
+specification versions v3.0.3 and v3.1.0.
.. note:: This generator produces a complete OpenAPI spec by combining a
user-provided *template* (containing the API header, endpoints, and
@@ -15,15 +16,16 @@ Overview
The generator works in two stages:
-1. The user provides an **OpenAPI template** — a valid OpenAPI v3.0.3 YAML
+1. The user provides an **OpenAPI template** — a valid OpenAPI YAML
file that defines the API metadata (title, version, servers), paths
- (endpoints), and security schemes.
+ (endpoints), and security schemes. It also specifies the version of
+ the OpenAPI specification in its top-level attribute `openapi`.
2. The generator fills the ``components/schemas`` section with JSON Schema
definitions generated from the LinkML schema, keeping only those classes
that are transitively reachable from the endpoints.
Both the input template and the final output are automatically validated
-against the OpenAPI 3.0.3 specification using
+against the corresponding OpenAPI specification version using
`openapi-spec-validator `_.
To run:
@@ -32,6 +34,9 @@ To run:
gen-openapi personinfo.yaml --template api-template.yaml > personinfo.openapi.yaml
+The **template's top-level attribute `openapi` MUST specify the supported OpenAPI
+version**.
+
The ``--template`` / ``-t`` option is required when generating a concrete
specification. If omitted, the generator prints a generic template that can
be used as a starting point:
@@ -47,14 +52,8 @@ The generator validates both the input template and the final output against
the OpenAPI specification using
`openapi-spec-validator `_.
-The ``openapi`` field in the template is checked against the expected
-version for the chosen output format (currently ``openapi303`` →
-``3.0.3``). If the versions do not match, the generator raises a
-``ValueError``.
-
-Additionally, the template's ``components/schemas`` section must declare each
-resource that is referenced by an endpoint, using two custom extension
-fields:
+The template's ``components/schemas`` section must declare each resource
+that is referenced by an endpoint, using two custom extension fields:
``x-linkml-schema``
The ``id`` of the LinkML schema being used. Must match exactly.
diff --git a/docs/howtos/collaborative-development.md b/docs/howtos/collaborative-development.md
index 94515df2d1..221337e843 100644
--- a/docs/howtos/collaborative-development.md
+++ b/docs/howtos/collaborative-development.md
@@ -40,6 +40,36 @@ Some projects may opt to manage their data alongside the schema in the same repo
for "registry" style projects, but in other cases the data may be best managed separately in a dedicated
store or a repository like Zenodo or Figshare.
+(rdf-in-version-control)=
+### Keep generated RDF diffs small
+
+When you track generated RDF in Git, adding a class or property can change many
+blank-node labels. The default RDFC-1.0 numbering is repeatable for the same graph,
+but an insertion can renumber existing nodes and obscure the actual schema edit.
+
+The optional `--diff-stable` flag uses
+[diffable-rdf](https://github.com/ASCS-eV/diffable-rdf) to reduce this label churn.
+In a uv project containing your schema, install LinkML with the extra and generate
+SHACL as follows (using a release that includes this option):
+
+```bash
+uv add 'linkml[diff-stable]'
+uv run gen-shacl --diff-stable schema.yaml > schema.shacl.ttl
+```
+
+The flag also works with `gen-owl`, `gen-rdf`, and `gen-shex --format rdf`.
+Python callers of `canonicalize_rdf_graph(..., diff_stable=True)` can install
+`linkml-runtime[diff-stable]`. The feature is disabled by default; enabling it
+changes existing labels once, so keep that regeneration separate from schema edits.
+
+Labels depend on blank-node neighborhoods. Edits within connected structures or
+among symmetric nodes can still affect other labels; minimum diffs are not
+guaranteed. Keep tool versions consistent when comparing generated files.
+The relabeled output is not the standard RDFC canonical representation.
+ShExC, ShEx JSON and instance-data conversion are outside this option's scope.
+Non-standard RDF takes the existing fallback path, which warns that it cannot
+apply diff-stable labels.
+
### Permissively License Your Code and Data
(adapted from [O3 guidelines](https://osf.io/vuzt3/))
diff --git a/examples/tutorial/tutorial01/personinfo.json b/examples/tutorial/tutorial01/personinfo.json
index b4f0bc63f3..f83931ec6a 100644
--- a/examples/tutorial/tutorial01/personinfo.json
+++ b/examples/tutorial/tutorial01/personinfo.json
@@ -1,7 +1,7 @@
{
"$defs": {
"Person": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"age": {
diff --git a/packages/linkml/pyproject.toml b/packages/linkml/pyproject.toml
index df18ca8f70..183cf3f706 100644
--- a/packages/linkml/pyproject.toml
+++ b/packages/linkml/pyproject.toml
@@ -59,15 +59,18 @@ dependencies = [ # Specifier syntax: https://peps.python.org/pep-0631/
"pyyaml",
"rdflib>=7.6.0",
"requests >= 2.22",
- "sqlalchemy>=2.0.53",
+ "sqlalchemy>=2.0.54",
"watchdog >= 0.9.0",
"typing-extensions >= 4.6.0; python_version < '3.12'",
"sphinx-click (>=6.0.0)",
"openapi-spec-validator >= 0.8.4",
"pydantic-settings>=2.15.0",
- "platformdirs>=4.11.8",
+ "platformdirs>=4.11.12",
]
+[project.optional-dependencies]
+diff-stable = ["linkml-runtime[diff-stable]>=1.10.0,<2.0.0"]
+
[dependency-groups]
lint = [
"black >= 24.0.0",
@@ -79,6 +82,7 @@ shacl = [
"pyshacl>=0.40.1",
]
tests = [
+ "linkml-runtime[diff-stable]",
{ include-group = "lint" },
{ include-group = "typing" },
{ include-group = "shacl" },
@@ -97,7 +101,7 @@ dev = [
"nbconvert",
"nbformat",
"coverage>=7.16.0",
- "tox>=4.61.2",
+ "tox>=4.64.1",
"tox-uv",
"myst-nb>=1.4.0; python_version >= '3.10'",
"sphinx-design >= 0.5.0",
@@ -114,7 +118,7 @@ tests-rustgen = [
"maturin>=1.15.0"
]
typedb = [
- "typedb-driver>=3.12.3,<4.0",
+ "typedb-driver>=3.13.6,<4.0",
]
bigquery = [
"sqlalchemy-bigquery >= 1.9.0",
diff --git a/packages/linkml/src/linkml/generators/jsonschemagen.py b/packages/linkml/src/linkml/generators/jsonschemagen.py
index 82d508345a..586b21dc4b 100644
--- a/packages/linkml/src/linkml/generators/jsonschemagen.py
+++ b/packages/linkml/src/linkml/generators/jsonschemagen.py
@@ -3,7 +3,7 @@
import logging
import os
from copy import deepcopy
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from typing import Any, cast
import click
@@ -414,8 +414,16 @@ class JsonSchemaGenerator(Generator, LifecycleMixin):
# @deprecated("Use top_class")
topClass: str | None = None
- not_closed: bool | None = True
- """If not closed, then an open-ended set of attributes can be instantiated for any object"""
+ not_closed: bool = False
+ """Allow data to include properties that the schema does not declare.
+
+ Defaults to closed, following the metamodel: ``meta.yaml`` documents an absent
+ ``extra_slots`` as "forbid all additional data (default)". An explicit
+ ``extra_slots`` on a class always takes precedence over this.
+
+ This governs classes only. The top-level schema takes ``additionalProperties``
+ from the document's root class -- see :meth:`start_schema`.
+ """
indent: int = 4
@@ -424,6 +432,9 @@ class JsonSchemaGenerator(Generator, LifecycleMixin):
top_class: ClassDefinitionName | str | None = None # JSON object is one instance of this
"""Class instantiated by the root node of the document tree"""
+ _root_class_name: str | None = field(default=None, init=False, repr=False)
+ """Name of the resolved root class, set by :meth:`start_schema`."""
+
include_range_class_descendants: bool = False
"""If set, use an open world assumption and allow the range of a slot to be any descendant of the declared range.
Note that if the range of a slot has a type designator, descendants will always be included.
@@ -472,8 +483,6 @@ def __post_init__(self):
self.top_class = self.topClass
super().__post_init__()
- if self.namespaces is None:
- raise TypeError("Schema text must be supplied to JSON schema generator. Preparsed schema will not work")
# Set the class variable for JsonSchema to use
JsonSchema.PRESERVE_NAMES = self.preserve_names
@@ -485,14 +494,49 @@ def __post_init__(self):
if self.schemaview.get_class(self.top_class) is None:
logger.warning(f"No class in schema named {self.top_class}")
+ def _names_match(self, a: str, b: str) -> bool:
+ """Compare class names the way ``--top-class`` has always been matched.
+
+ ``top_class`` is habitually passed in CamelCase while the schema spells the
+ class out (``top_class="AnyType"`` for ``any type``), so the comparison is
+ on ``camelcase`` unless ``preserve_names`` is set.
+ """
+ return a == b if self.preserve_names else camelcase(a) == camelcase(b)
+
+ def _root_class(self) -> ClassDefinition | None:
+ """The class the root of the document instantiates, or ``None`` if there is none.
+
+ Named by ``--top-class``, or failing that declared with ``tree_root: true``.
+ A schema may carry more than one ``tree_root`` (biolink-model does), so the
+ first is taken; the point is that every use site agrees on which it is.
+ """
+ classes = self.schemaview.all_classes().values()
+ if self.top_class:
+ return next((c for c in classes if self._names_match(self.top_class, c.name)), None)
+ return next((c for c in classes if c.tree_root), None)
+
+ def _is_root_class(self, cls: ClassDefinition) -> bool:
+ """Whether *cls* is the class the root of the document instantiates.
+
+ Compares against the name resolved in :meth:`start_schema`, so the top-level
+ ``additionalProperties`` and the subschema merged beneath it cannot come from
+ two different classes.
+ """
+ return self._root_class_name is not None and self._names_match(self._root_class_name, cls.name)
+
def start_schema(self, inline: bool = False):
self.inline = inline
- top_additional_properties = self.not_closed
- if self.top_class:
- top_class_def = self.schemaview.get_class(self.top_class)
- if top_class_def is not None:
- top_additional_properties = self.get_additional_properties(top_class_def)
+ root_class_def = self._root_class()
+ self._root_class_name = root_class_def.name if root_class_def is not None else None
+
+ if root_class_def is not None:
+ top_additional_properties = self.get_additional_properties(root_class_def)
+ else:
+ # No root class means the top level has no properties of its own, so
+ # closing it would admit nothing but `{}`. Stay open regardless of
+ # `not_closed`, which governs classes.
+ top_additional_properties = True
self.top_level_schema = JsonSchema(
{
@@ -619,13 +663,7 @@ def handle_class(self, cls: ClassDefinition) -> None:
else:
self.top_level_schema.add_def(cls.name, class_subschema)
- if (
- self.top_class is not None
- and (
- (self.preserve_names and self.top_class == cls.name)
- or (not self.preserve_names and camelcase(self.top_class) == camelcase(cls.name))
- )
- ) or (self.top_class is None and cls.tree_root):
+ if self._is_root_class(cls):
for key, value in class_subschema.items():
# check this first to ensure we don't overwrite things like additionalProperties
# or description on the root. But we do want to copy over properties, required,
@@ -1209,10 +1247,12 @@ def _parameterized_dimension(self, dimension: DimensionExpression, dtype: JsonSc
)
@click.option(
"--not-closed/--closed",
- default=True,
+ default=False,
show_default=True,
help="""
-Set additionalProperties=False if closed otherwise true if not closed at the global level
+Allow data to include properties that the schema does not declare. Closed by
+default, following the metamodel; an explicit `extra_slots` on a class always
+wins. The top level takes its value from the document's root class.
""",
)
@click.option(
diff --git a/packages/linkml/src/linkml/generators/openapigen.py b/packages/linkml/src/linkml/generators/openapigen.py
index 5865917287..813506e41f 100644
--- a/packages/linkml/src/linkml/generators/openapigen.py
+++ b/packages/linkml/src/linkml/generators/openapigen.py
@@ -1,4 +1,4 @@
-"""Generate OpenAPI v3.0.3 Specification YAML files."""
+"""Generate OpenAPI YAML files."""
import json
import os
@@ -10,21 +10,28 @@
import click
import yaml
-from openapi_spec_validator import OpenAPIV30SpecValidator
+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 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
-openapi_generic_template = """openapi: 3.0.3
+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
@@ -41,20 +48,24 @@
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}
@@ -65,18 +76,32 @@
@dataclass
class OpenApiGenerator(Generator):
"""
- Generates OpenAPI v3.0.3 specification YAML from a LinkML schema.
+ 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 = ["openapi303"]
+ valid_formats = ["openapi"]
file_extension = "yaml"
uses_schemaloader = False
@@ -85,30 +110,23 @@ class OpenApiGenerator(Generator):
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: dict[str, str] = field(
- default_factory=lambda: {"openapi303": "3.0.3"},
+ _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},
+ default_factory=lambda: {"3.0.3": OpenAPIV30SpecValidator, "3.1.0": OpenAPIV31SpecValidator},
init=False,
repr=False,
)
- def _validate_oad_template(
- self, oad_validator_class: type[OaSpecValidator], expected_version: str, format_name: str
- ):
+ _openapi_version = "" # OpenAPI version declared in the template
+
+ def _validate_oad_template(self, oad_validator_class: type[OaSpecValidator], expected_version: str):
"""Validate the OpenAPI template"""
- # Validate that the template declares the expected OpenAPI version
- declared_version = self._template.get("openapi")
- if declared_version != expected_version:
- raise ValueError(
- f"Template OpenAPI version is '{declared_version}', "
- f"but format '{format_name}' requires version '{expected_version}'"
- )
# Validate the input template against the OpenAPI specification.
# This also catches dangling $ref targets in endpoints.
openapi_validate(self._template, cls=oad_validator_class)
@@ -214,11 +232,11 @@ def _reachable_from_seeds(self, ref_map: dict[str, set[str]], seeds: set[str]) -
stack.append(ref)
return seen
- def _fix_openapi_spec(self, element: dict | list) -> dict | list:
+ 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 (OpenAPI 3.0 doesn't support ``const``)
+ - ``const`` becomes ``enum`` with a single value
- ``type`` as a list (e.g. nullable ``["string", "null"]``) becomes ``anyOf``
- ``examples`` (a list) becomes ``example`` (its first element); OpenAPI 3.0 has
no plural ``examples`` keyword on the Schema Object, only singular ``example``
@@ -241,7 +259,7 @@ def _fix_openapi_spec(self, element: dict | list) -> dict | list:
fixed_element["example"] = value[0]
else:
if isinstance(value, dict | list):
- value = self._fix_openapi_spec(value)
+ 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
@@ -249,7 +267,7 @@ def _fix_openapi_spec(self, element: dict | list) -> dict | list:
fixed_element = []
for item in element:
if isinstance(item, dict | list):
- item = self._fix_openapi_spec(item)
+ 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)
@@ -287,10 +305,47 @@ def _rename(self, name_map: dict[str, str], element: dict | list) -> dict | list
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], elem_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.
+ to OpenAPI 3.0.3 compat, and apply any OpenAPI<->LinkML name renames.
"""
# Keep only schemas transitively reachable from the endpoint-referenced seeds.
# The reference graph is built once and traversed in a single pass; no fixpoint
@@ -303,7 +358,18 @@ def _sanitize_schemas(self, name_map: dict[str, str], elem_schemas: dict, req_li
# title always duplicates the schema dict key, so it is redundant in components/schemas
for elem_schema in elem_schemas.values():
elem_schema.pop("title", None)
- elem_schemas = cast(dict, self._fix_openapi_spec(elem_schemas))
+ if self._openapi_version == "3.0.3":
+ elem_schemas = cast(dict, self._fix_openapi_spec_v303(elem_schemas))
+ elif self._openapi_version == "3.1.0":
+ elem_schemas = cast(dict, self._strip_linkml_meta(elem_schemas))
+ elem_schemas = cast(dict, self._rewrite_defs_refs(elem_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(elem_schemas, reserved=set(name_map.values()))
+ if sanitize_map:
+ elem_schemas = cast(dict, self._rename(sanitize_map, elem_schemas))
+ else:
+ raise ValueError(f"OpenAPI version '{self._openapi_version}' is not supported")
if self.inline_enums:
# inline before renaming so the enum/type guard matches LinkML names,
# not the (possibly renamed) OpenAPI schema names
@@ -312,6 +378,32 @@ def _sanitize_schemas(self, name_map: dict[str, str], elem_schemas: dict, req_li
elem_schemas = cast(dict, self._rename(name_map, elem_schemas))
return elem_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, endpoint_schemas: set[str] | None = None) -> dict:
"""Inline enum subschemas into their parents instead of separate entries.
@@ -389,26 +481,93 @@ def _collect_refs(obj: dict | list) -> list[str]:
refs.extend(OpenApiGenerator._collect_refs(item))
return refs
+ 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
+ # not_closed=True is deliberate: APIs are extended backwards-compatibly by
+ # adding attributes to existing objects, which additionalProperties=False
+ # blocks. Stated explicitly rather than inherited from the generator default,
+ # which follows the metamodel and closes classes with no `extra_slots`.
+ json_schema = JsonSchemaGenerator(
+ self.schemaview.schema, include_null=False, preserve_names=True, not_closed=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:
+ # not_closed=True is deliberate, mirroring the v3.0.3 path: APIs are extended
+ # backwards-compatibly by adding attributes to existing objects, which
+ # additionalProperties=False blocks. The generated class schemas above are open
+ # (extra_fields="allow"); this JsonSchema merge must not clobber them with the
+ # metamodel-default closed class schemas.
+ json_schema = JsonSchemaGenerator(
+ self.schemaview.schema, include_null=False, preserve_names=True, not_closed=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 an OpenAPI v3.0.3 spec from ``template_file`` and the loaded LinkML schema."""
+ """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 expected OpenAPI version from the active output format
- format_name = getattr(self, "format", self.valid_formats[0]) or self.valid_formats[0]
- expected_version = self._openapi_versions.get(format_name)
- if expected_version is None:
- raise ValueError(f"Unsupported output format '{format_name}'")
+ # 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
- oad_validator_class = self._openapi_validators.get(expected_version)
+ oad_validator_class = self._openapi_validators.get(self._openapi_version)
if oad_validator_class is None:
- raise ValueError(f"No validator available for OpenAPI version {expected_version}")
+ raise ValueError(f"No validator available for OpenAPI version {self._openapi_version}")
# validate the OpenAPI template before further processing
- self._validate_oad_template(oad_validator_class, expected_version, format_name)
+ self._validate_oad_template(oad_validator_class, self._openapi_version)
# if no schemas to instantiate, return the template itself
if (
"components" not in self._template
@@ -429,22 +588,17 @@ def serialize(self, template_file: str = "", **kwargs) -> str:
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
+ # 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 endpoint_ref_openapi_names
+ for n in openapi_schemas
if n != openapi_schemas[n]["x-linkml-source"]
}
- # 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 req_linkml_names:
- if linkml_name in self.schemaview.all_types():
- all_req_schemas[linkml_name] = self._generate_type_schema(linkml_name)
+ 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)
@@ -494,7 +648,11 @@ def printout_template(self) -> str:
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)
+ 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)
@@ -502,7 +660,7 @@ def printout_template(self) -> str:
@click.option(
"--template",
"-t",
- help="OpenAPI v3.0.3 template - includes the header, the endpoints and the security schemes",
+ help="OpenAPI template - includes the header, the endpoints and the security schemes",
)
@click.option(
"--keep-unreferenced",
@@ -520,7 +678,7 @@ def printout_template(self) -> str:
)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile, template, keep_unreferenced, inline_enums, **args):
- """Generate an OpenAPI v3.0.3 spec with resources modelled with LinkML.
+ """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
diff --git a/packages/linkml/src/linkml/generators/owlgen.py b/packages/linkml/src/linkml/generators/owlgen.py
index 50be266df0..3f16c4965c 100644
--- a/packages/linkml/src/linkml/generators/owlgen.py
+++ b/packages/linkml/src/linkml/generators/owlgen.py
@@ -122,6 +122,9 @@ class OwlSchemaGenerator(Generator):
"""Suffix to add to the schema name to create the ontology URI, e.g. .owl.ttl"""
# ObjectVars
+ diff_stable: bool = False
+ """Reduce blank-node label churn. See :ref:`rdf-in-version-control`."""
+
metadata_profile: MetadataProfile | None = None
"""Deprecated - use metadata_profiles."""
@@ -353,7 +356,7 @@ def serialize(self, **kwargs: Any) -> str:
"""
self.as_graph()
fmt = "turtle" if self.format in ["owl", "ttl"] else self.format
- return canonicalize_rdf_graph(self.graph, output_format=fmt)
+ return canonicalize_rdf_graph(self.graph, output_format=fmt, diff_stable=self.diff_stable)
def add_metadata(self, e: Definition | PermissibleValue, uri: URIRef) -> None:
"""
@@ -1556,7 +1559,7 @@ def _range_uri(self, slot: SlotDefinition) -> URIRef:
if self.type_objects:
return self._type_uri(typ.name)
else:
- return self.namespaces.uri_for(typ.uri)
+ return URIRef(self.schemaview.get_uri(typ, expand=True))
elif slot.range in self.schema.enums:
# TODO: enums fill this in
return self._enum_uri(EnumDefinitionName(slot.range))
@@ -1844,6 +1847,15 @@ def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
"specified language tag. Element-level in_language overrides this."
),
)
+@click.option(
+ "--diff-stable/--no-diff-stable",
+ default=False,
+ show_default=True,
+ help=(
+ "Reduce blank-node label churn across edits. See "
+ "https://linkml.io/linkml/howtos/collaborative-development.html#rdf-in-version-control"
+ ),
+)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile: str, metadata_profile: str, **kwargs: Any) -> None:
"""Generate an OWL representation of a LinkML model
diff --git a/packages/linkml/src/linkml/generators/rdfgen.py b/packages/linkml/src/linkml/generators/rdfgen.py
index 2da1701787..5b8f8dc1c7 100644
--- a/packages/linkml/src/linkml/generators/rdfgen.py
+++ b/packages/linkml/src/linkml/generators/rdfgen.py
@@ -78,6 +78,9 @@ class RDFGenerator(Generator):
uses_schemaloader = True
# ObjectVars
+ diff_stable: bool = False
+ """Reduce blank-node label churn. See :ref:`rdf-in-version-control`."""
+
emit_metadata: bool = False
context: list[str] = None
original_schema: SchemaDefinition = None
@@ -89,7 +92,7 @@ def __post_init__(self):
def _data(self, g: Graph) -> str:
fmt = "turtle" if self.format == "ttl" else self.format
- return canonicalize_rdf_graph(g, output_format=fmt)
+ return canonicalize_rdf_graph(g, output_format=fmt, diff_stable=self.diff_stable)
def end_schema(self, output: str | None = None, context: str = None, **_) -> str:
gen = JSONLDGenerator(
@@ -137,6 +140,15 @@ def end_schema(self, output: str | None = None, context: str = None, **_) -> str
multiple=True,
help="JSONLD context file (default: vendored meta.context.jsonld)",
)
+@click.option(
+ "--diff-stable/--no-diff-stable",
+ default=False,
+ show_default=True,
+ help=(
+ "Reduce blank-node label churn across edits. See "
+ "https://linkml.io/linkml/howtos/collaborative-development.html#rdf-in-version-control"
+ ),
+)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile, **kwargs):
"""Generate an RDF representation of a LinkML model"""
diff --git a/packages/linkml/src/linkml/generators/shaclgen.py b/packages/linkml/src/linkml/generators/shaclgen.py
index 4731b9f0b8..1804b1204e 100644
--- a/packages/linkml/src/linkml/generators/shaclgen.py
+++ b/packages/linkml/src/linkml/generators/shaclgen.py
@@ -142,6 +142,9 @@ class ShaclGenerator(Generator):
ignores any per-slot ``in_language``.
"""
+ diff_stable: bool = False
+ """Reduce blank-node label churn. See :ref:`rdf-in-version-control`."""
+
emit_rules: bool = True
"""Emit ``sh:sparql`` constraints from LinkML ``rules:`` blocks.
@@ -196,7 +199,7 @@ def generate_header(self) -> str:
def serialize(self, **args) -> str:
g = self.as_graph()
fmt = "turtle" if self.format in ["owl", "ttl"] else self.format
- return canonicalize_rdf_graph(g, output_format=fmt)
+ return canonicalize_rdf_graph(g, output_format=fmt, diff_stable=self.diff_stable)
def as_graph(self) -> Graph:
sv = self.schemaview
@@ -929,6 +932,15 @@ def add_simple_data_type(func: Callable, r: ElementName) -> None:
"sh:NodeShape. Use --no-emit-rules to suppress rule generation."
),
)
+@click.option(
+ "--diff-stable/--no-diff-stable",
+ default=False,
+ show_default=True,
+ help=(
+ "Reduce blank-node label churn across edits. See "
+ "https://linkml.io/linkml/howtos/collaborative-development.html#rdf-in-version-control"
+ ),
+)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile, **args):
"""Generate SHACL turtle from a LinkML model"""
diff --git a/packages/linkml/src/linkml/generators/shexgen.py b/packages/linkml/src/linkml/generators/shexgen.py
index 40a93ffbc9..e74e45225e 100644
--- a/packages/linkml/src/linkml/generators/shexgen.py
+++ b/packages/linkml/src/linkml/generators/shexgen.py
@@ -40,6 +40,9 @@ class ShExGenerator(Generator):
uses_schemaloader = True
# ObjectVars
+ diff_stable: bool = False
+ """Reduce blank-node label churn in RDF output. See :ref:`rdf-in-version-control`."""
+
shex: Schema = field(default_factory=lambda: Schema()) # ShEx Schema being generated
shapes: list = field(default_factory=lambda: [])
shape: Shape | None = None # Current shape being defined
@@ -177,7 +180,7 @@ def end_schema(self, output: str | None = None, **_) -> str:
g = Graph()
g.parse(data=shex, format="json-ld", version="1.1")
g.bind("owl", OWL)
- shex = canonicalize_rdf_graph(g, output_format="turtle")
+ shex = canonicalize_rdf_graph(g, output_format="turtle", diff_stable=self.diff_stable)
elif self.format == "shex":
g = Graph()
self.namespaces.load_graph(g)
@@ -258,6 +261,15 @@ def _get_subproperty_values(self, slot: SlotDefinition) -> list:
help="If --expand-subproperty-of (default), slots with subproperty_of will generate NodeConstraint "
"values containing all slot descendants. Use --no-expand-subproperty-of to disable this behavior.",
)
+@click.option(
+ "--diff-stable/--no-diff-stable",
+ default=False,
+ show_default=True,
+ help=(
+ "Reduce blank-node label churn across edits (--format rdf). See "
+ "https://linkml.io/linkml/howtos/collaborative-development.html#rdf-in-version-control"
+ ),
+)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile, **args):
"""Generate a ShEx Schema for a LinkML model"""
diff --git a/packages/linkml/src/linkml/utils/generator.py b/packages/linkml/src/linkml/utils/generator.py
index 6d5df36eb8..dc338941e4 100644
--- a/packages/linkml/src/linkml/utils/generator.py
+++ b/packages/linkml/src/linkml/utils/generator.py
@@ -20,6 +20,7 @@
import os
import re
import sys
+import warnings
from collections.abc import Callable, Mapping
from copy import deepcopy
from dataclasses import dataclass, field
@@ -30,7 +31,6 @@
import click
import yaml
from click import Argument, Command, Option
-from jsonasobj2 import JsonObj
from linkml import LOCAL_METAMODEL_YAML_FILE
from linkml.cli.logging import DEFAULT_LOG_LEVEL_INT, log_level_option
@@ -162,8 +162,67 @@ class Generator(metaclass=abc.ABCMeta):
"""Path to output file. Note all generators may not implement this
uniformly, see https://github.com/linkml/linkml/issues/923"""
- namespaces: Namespaces | None = None
- """All prefix expansions used"""
+ _namespaces: ClassVar[Namespaces | None] = None
+ """Class-level sentinel default for the private backing store of the
+ :attr:`namespaces` property. Declaring it as a ``ClassVar`` keeps it out of
+ the dataclass-generated ``__init__`` while still providing a safe default
+ read (``None``) before the instance attribute is assigned. The public,
+ constructor-visible name is the field ``namespaces``. On ``main`` that was a
+ plain dataclass field, so ``namespaces=`` was an implicit constructor kwarg;
+ it is preserved here so external callers/subclasses relying on it are not
+ silently broken by the switch to a property-backed field.
+ """
+
+ @property
+ def namespaces(self) -> Namespaces | None:
+ """Return the namespace registry.
+
+ On the SchemaLoader path (``uses_schemaloader=True``) this returns the
+ pre-built :class:`~linkml_runtime.utils.namespaces.Namespaces` object
+ populated by SchemaLoader.
+
+ On the SchemaView path (``uses_schemaloader=False``) accessing this
+ property is a sign of a hybrid design anti-pattern. A deprecation
+ warning is emitted and the call is transparently forwarded to
+ ``self.schemaview.namespaces()`` so that existing callers continue to
+ work while being nudged towards the correct API.
+ """
+ # Emit a warning when a SchemaView-based generator reads self.namespaces.
+ if not self.uses_schemaloader and self.schemaview is not None:
+ warnings.warn(
+ f"{type(self).__name__} uses SchemaView (uses_schemaloader=False) but "
+ "self.namespaces was accessed. Use self.schemaview.namespaces() for URI "
+ "resolution instead; self.namespaces is a SchemaLoader-era artifact that "
+ "is not populated on the SchemaView path.",
+ UserWarning,
+ stacklevel=3,
+ )
+
+ # Return the namespace map, preferring an explicitly injected one.
+ if self._namespaces is not None:
+ return self._namespaces
+ if not self.uses_schemaloader and self.schemaview is not None:
+ return self.schemaview.namespaces()
+ return None
+
+ @namespaces.setter
+ def namespaces(self, value: Namespaces | None) -> None:
+ """Save passed namespace registry in the private backing store of the
+ :attr:`namespaces` property."""
+ self._namespaces = value
+
+ namespaces: Namespaces | None = namespaces
+ """Constructor kwarg backing the ``namespaces`` property (see above).
+
+ This does NOT create a second attribute that shadows the property.
+ A dataclass "field" is just an *annotation* plus a *default value*;
+ the only real class attribute named ``namespaces`` remains the
+ ``namespaces`` property object.
+ ``@dataclass`` reads that property object as the field's default and bakes it
+ into the generated ``__init__`` as ``namespaces=``
+ -- it does not overwrite the property, so attribute access still goes through
+ the getter/setter.
+ """
directory_output: bool = False
"""True means output is to a directory, False is to stdout"""
@@ -191,6 +250,13 @@ class Generator(metaclass=abc.ABCMeta):
"""If set, include extra schema outside of the imports mechanism"""
def __post_init__(self) -> None:
+ # The ``namespaces`` dataclass field defaults to the property object
+ # itself (see its declaration). When no ``namespaces=`` kwarg is passed,
+ # the generated __init__ routes that default through the property setter
+ # into ``self._namespaces``; normalise that sentinel back to ``None`` so
+ # the SchemaLoader/SchemaView paths can populate it as usual.
+ if self._namespaces is Generator.__dict__["namespaces"]:
+ self._namespaces = None
if not self.logger:
self.logger = logger
if self.log_level is not None:
@@ -241,8 +307,6 @@ def __post_init__(self) -> None:
if not self.include_generation_date and self.schema is not None:
self.schema.generation_date = None
- self._init_namespaces()
-
def _initialize_using_schemaloader(self, schema: Union[str, TextIO, SchemaDefinition, "Generator"]):
# currently generators are very liberal in what they accept, including
# other generators.
@@ -253,7 +317,7 @@ def _initialize_using_schemaloader(self, schema: Union[str, TextIO, SchemaDefini
self.schema = gen.schema
self.synopsis = gen.synopsis
self.loaded = gen.loaded
- self.namespaces = gen.namespaces
+ self._namespaces = gen.namespaces
self.base_dir = gen.base_dir
self.importmap = gen.importmap
self.source_file_data = gen.source_file_date
@@ -282,7 +346,7 @@ def _initialize_using_schemaloader(self, schema: Union[str, TextIO, SchemaDefini
self.schema = loader.schema
self.synopsis = loader.synopsis
self.loaded = loader.loaded
- self.namespaces = loader.namespaces
+ self._namespaces = loader.namespaces
self.base_dir = loader.base_dir
self.importmap = loader.importmap
self.source_file_data = loader.source_file_date
@@ -290,26 +354,6 @@ def _initialize_using_schemaloader(self, schema: Union[str, TextIO, SchemaDefini
self.schema_location = loader.schema_location
self.schema_defaults = loader.schema_defaults
- def _init_namespaces(self):
- if self.namespaces is None:
- self.namespaces = Namespaces()
- if isinstance(self.schema.prefixes, dict):
- for key, value in self.schema.prefixes.items():
- if hasattr(value, "prefix_reference"):
- self.namespaces[key] = value.prefix_reference
- else:
- self.namespaces[key] = value
- elif isinstance(self.schema.prefixes, JsonObj):
- prefixes = vars(self.schema.prefixes)
- for key, value in prefixes.items():
- if hasattr(value, "prefix_reference"):
- self.namespaces[key] = value.prefix_reference
- else:
- self.namespaces[key] = value
- else:
- for prefix in self.schema.prefixes.values():
- self.namespaces[prefix.prefix_prefix] = prefix.prefix_reference
-
@classmethod
def validate_generator_args(cls, args: Mapping[str, Any]) -> None:
"""Validate ``generator_args`` before any generator is built from them.
diff --git a/packages/linkml_runtime/pyproject.toml b/packages/linkml_runtime/pyproject.toml
index 617a182a31..a6720ee445 100644
--- a/packages/linkml_runtime/pyproject.toml
+++ b/packages/linkml_runtime/pyproject.toml
@@ -62,6 +62,7 @@ dev = [
linkml-normalize = "linkml_runtime.processing.referencevalidator:cli"
[project.optional-dependencies]
+diff-stable = ["diffable-rdf>=0.4.0"]
dev = [
"coverage",
"requests-cache>=1.3.3",
diff --git a/packages/linkml_runtime/src/linkml_runtime/utils/rdf_canonicalize.py b/packages/linkml_runtime/src/linkml_runtime/utils/rdf_canonicalize.py
index 31c5580348..bc06a969b8 100644
--- a/packages/linkml_runtime/src/linkml_runtime/utils/rdf_canonicalize.py
+++ b/packages/linkml_runtime/src/linkml_runtime/utils/rdf_canonicalize.py
@@ -287,6 +287,7 @@ def _is_safe_prefix_iri(iri: str) -> bool:
def canonicalize_rdf_graph(
graph: rdflib.Graph,
output_format: str = "turtle",
+ diff_stable: bool = False,
) -> str:
"""Serialize an rdflib Graph deterministically using RDFC-1.0 canonicalization.
@@ -300,8 +301,22 @@ def canonicalize_rdf_graph(
:param graph: The rdflib Graph to serialize.
:param output_format: Target serialization format (e.g. ``"turtle"``, ``"nt"``).
+ :param diff_stable: Use diffable-rdf labels to reduce blank-node churn across
+ edits. Requires the ``diff-stable`` extra; disabled by default. The
+ non-standard RDF fallback warns when it cannot apply these labels.
+ See https://linkml.io/linkml/howtos/collaborative-development.html#rdf-in-version-control
:return: Deterministic string serialization of the graph.
+ :raises ImportError: If diff-stable labels are requested without diffable-rdf.
"""
+ if diff_stable:
+ try:
+ from diffable_rdf import wl_relabel_quads
+ except ImportError as exc:
+ raise ImportError(
+ "diff_stable=True requires diffable-rdf. Install "
+ "'linkml-runtime[diff-stable]' or 'linkml[diff-stable]'."
+ ) from exc
+
ox_format = _FORMAT_MAP.get(output_format.lower())
if ox_format is None:
warnings.warn(
@@ -330,6 +345,16 @@ def canonicalize_rdf_graph(
RDFCanonicalizationWarning,
stacklevel=2,
)
+ if diff_stable:
+ warnings.warn(
+ "diff_stable=True was requested but this graph took the rdflib fallback, "
+ "which cannot apply Weisfeiler-Lehman blank-node labels. Output is "
+ "deterministic but NOT diff-stable: an unrelated edit may still renumber "
+ "blank nodes. Make the offending terms standard RDF (absolute IRIs, IRI "
+ "predicates) to get diff-stable labels.",
+ RDFCanonicalizationWarning,
+ stacklevel=2,
+ )
return _deterministic_fallback_serialize(graph, output_format)
dataset = ox.Dataset()
@@ -339,13 +364,17 @@ def canonicalize_rdf_graph(
# 3. Canonicalize blank node labels with RDFC-1.0.
dataset.canonicalize(ox.CanonicalizationAlgorithm.RDFC_1_0)
+ quads = list(dataset)
+
+ if diff_stable:
+ quads = wl_relabel_quads(quads)
+
# 4. Sort triples for deterministic ordering.
# RDFC-1.0 stabilizes blank-node labels but pyoxigraph's Dataset
# iteration order is not sorted and varies across processes (verified
# empirically against pyoxigraph 0.5.8). The explicit string-key sort
# is load-bearing for byte-identical output across runs; see
# tests/linkml_runtime/test_utils/test_rdf_canonicalize.py::test_sort_is_load_bearing.
- quads = list(dataset)
sorted_triples = sorted(
(ox.Triple(q.subject, q.predicate, q.object) for q in quads),
key=lambda t: (str(t.subject), str(t.predicate), str(t.object)),
diff --git a/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py b/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py
index 5cb7c6794d..0291617950 100644
--- a/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py
+++ b/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py
@@ -333,6 +333,16 @@ def set_modified(self) -> None:
self._hash = None
self.modifications += 1
+ def _resolved_importmap(self) -> dict[str, Any]:
+ """User-supplied importmap merged over the built-in ``linkml:`` metamodel mapping.
+
+ Values are usually strings, but may be an in-memory schema dict (see ``load_import``).
+ """
+ from linkml_runtime import SCHEMA_DIRECTORY
+
+ # user entries come last so they can override the built-in linkml: entry
+ return {"linkml:": str(SCHEMA_DIRECTORY), **self.importmap}
+
def load_import(self, imp: str, from_schema: SchemaDefinition | None = None) -> SchemaDefinition:
"""Handle import directives.
@@ -361,10 +371,7 @@ def load_import(self, imp: str, from_schema: SchemaDefinition | None = None) ->
"""
if from_schema is None:
from_schema = self.schema
- from linkml_runtime import SCHEMA_DIRECTORY
-
- default_import_map = {"linkml:": str(SCHEMA_DIRECTORY)}
- importmap = {**default_import_map, **self.importmap}
+ importmap = self._resolved_importmap()
# An importmap entry may be an in-memory schema dict, which is loaded
# directly without touching the filesystem or network.
mapped = importmap.get(str(imp))
@@ -437,6 +444,38 @@ def _get_dict(self, element_name: str, imports: bool = True) -> dict:
return d
+ def _load_closure_import(self, sn: str, raw_imp: str | None, importer_sn: str | None) -> SchemaDefinition:
+ """Load one schema for :meth:`imports_closure`.
+
+ ``sn`` is the closure key (possibly normalized as root-relative); ``raw_imp`` is the
+ import exactly as written in the importing schema; ``importer_sn`` is that schema's key.
+
+ When an importmap or CURIE mapping applies to ``sn``, resolution stays relative to the
+ origin schema. Otherwise the raw import is resolved against the *importing* schema's
+ recorded ``source_file``, so a schema an importmap redirected outside the root tree
+ still finds its own relative imports (#3499). For unredirected trees both resolutions
+ denote the same file. Fall back to origin-schema resolution when the importer has no
+ ``source_file`` (e.g. in-memory schemas).
+
+ Pre-existing limitation: distinct files imported under the same un-normalizable name
+ share one closure key; the first one loaded wins.
+ """
+ # raw_imp is None only for the root schema (it has no importer)
+ if raw_imp is None:
+ return self.load_import(sn)
+ importmap = self._resolved_importmap()
+ # an in-memory schema dict has no location of its own, and a mapping hit on sn means an
+ # importmap/CURIE entry governs this key; both resolve against the origin schema
+ if isinstance(importmap.get(str(sn)), dict) or map_import(importmap, self.namespaces, sn) != sn:
+ return self.load_import(sn)
+ importer = self.schema_map.get(importer_sn)
+ # without a source_file there is no directory to resolve the import against
+ if importer is None or not importer.source_file:
+ return self.load_import(sn)
+ # raw_imp, not sn: resolving the root-relative key against the importer's directory
+ # would apply the relative prefix twice (subdir/types against /subdir/)
+ return self.load_import(raw_imp, from_schema=importer)
+
@lru_cache(None)
def imports_closure(
self, imports: bool = True, traverse: bool | None = None, inject_metadata: bool = True
@@ -473,7 +512,9 @@ def imports_closure(
closure = deque()
visited = set()
- todo = [self.schema.name]
+ # (closure key, import name as written in the importing schema, importing schema's key);
+ # the root schema has no importer, hence the Nones
+ todo = [(self.schema.name, None, None)]
if traverse is not None:
warnings.warn(
@@ -482,13 +523,13 @@ def imports_closure(
)
if not imports or (not traverse and traverse is not None):
- return todo
+ return [self.schema.name]
while len(todo) > 0:
# visit item
- sn = todo.pop()
+ sn, raw_imp, importer_sn = todo.pop()
if sn not in self.schema_map:
- self.schema_map[sn] = self.load_import(sn)
+ self.schema_map[sn] = self._load_closure_import(sn, raw_imp, importer_sn)
# resolve item's imports if it has not been visited already
# we will get duplicates, but not cycles this way, and
@@ -499,30 +540,37 @@ def imports_closure(
if i == sn:
continue
- # resolve relative imports relative to the importing schema, rather than the
- # origin schema. Imports can be a URI or Curie, and imports from the same
- # directory don't require a ./, so if the current (sn) import is a relative
- # path, and the target import doesn't have : (as in a curie or a URI)
- # we prepend the relative path. This WILL make the key in the `schema_map` not
- # equal to the literal text specified in the importing schema, but this is
- # essential to sensible deduplication: e.g. for
+ # compute the closure key for this import: when the current (sn) key is a
+ # relative path and the target import has no : (as in a CURIE or URI), the
+ # import is normalised against sn's parent. This makes the key in the
+ # `schema_map` not equal to the literal text specified in the importing
+ # schema, but it is essential to sensible deduplication: e.g. for
# - main.yaml (imports ./types.yaml, ./subdir/subschema.yaml)
# - types.yaml
# - subdir/subschema.yaml (imports ./types.yaml)
# - subdir/types.yaml
# we should treat the two `types.yaml` as separate schemas from the POV of the
- # origin schema.
-
- # if i is not a CURIE and sn looks like a path with at least one parent folder,
- # normalise i with respect to sn
- if "/" in sn and ":" not in i:
+ # origin schema. The key identifies the schema and drives importmap/CURIE
+ # lookups; locating the actual file happens in _load_closure_import, which
+ # uses the raw import name and the importer's own location.
+
+ # if i is not a CURIE and sn looks like a filesystem path with at least one
+ # parent folder, normalise i with respect to sn.
+ #
+ # URLs are excluded: os.path.normpath() collapses the double slash in a
+ # scheme, so file://a/b would become file:/a/b, and the mangled key is then
+ # indistinguishable from a CURIE. A URL-keyed schema keeps the literal
+ # import as its key and is located by _load_closure_import instead, which
+ # resolves it against the URL the importing schema was fetched from.
+ if "/" in sn and "://" not in sn and ":" not in i:
if WINDOWS:
# This cannot be simplified. os.path.normpath() must be called before .as_posix()
- todo.append(PurePath(os.path.normpath(PurePath(sn).parent / i)).as_posix())
+ key = PurePath(os.path.normpath(PurePath(sn).parent / i)).as_posix()
else:
- todo.append(os.path.normpath(str(Path(sn).parent / i)))
+ key = os.path.normpath(str(Path(sn).parent / i))
else:
- todo.append(i)
+ key = i
+ todo.append((key, i, sn))
# add item to closure
# append + pop (above) is FILO queue, which correctly extends tree leaves,
@@ -1439,7 +1487,17 @@ def get_uri(
msg = f"Cannot find {e.from_schema} in schema_map"
raise ValueError(msg)
else:
- schema = self.schema_map[self.in_schema(e.name)]
+ # Two classes reusing an attribute name give a bare placeholder slot with no
+ # from_schema, so the schema has to be looked up by name instead.
+ schema_name = self.in_schema(e.name) # the schema's own name, e.g. 'core'
+ # .get(), not [...]: keys are imports as written, so a relative import misses
+ # here -- a miss is expected and must fall through to the name match below.
+ schema = self.schema_map.get(schema_name)
+ if schema is None:
+ schema = next((sc for sc in self.schema_map.values() if sc.name == schema_name), None)
+ if schema is None:
+ msg = f"Cannot find schema {schema_name} in schema_map"
+ raise ValueError(msg)
if use_element_type:
e_type = e.class_name.split("_", 1)[0] # for example "class_definition"
e_type_path = f"{e_type}/"
diff --git a/pyproject.toml b/pyproject.toml
index 559cf265ac..fd62dc2986 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -115,6 +115,7 @@ markers = [
"yarrrml: End-to-end tests for the YARRRML generator",
"typedbgen: Tests for the TypeDB generator",
"arrays: Array specifications!",
+ "diffable_rdf: tests that exercise the diffable-rdf integration (--diff-stable); run as a canary against linkml in diffable-rdf's own CI",
]
# https://docs.astral.sh/ruff/settings/
diff --git a/tests/conftest.py b/tests/conftest.py
index bcf958b9e5..ba13d6f764 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -422,7 +422,8 @@ class MockImportErrorFinder(MetaPathFinder):
"""
Fake like we don't have a module when we really do.
- see the ``mock_black_import`` fixture for example usage.
+ see the ``mock_missing_import`` fixture to hide any module, or ``mock_black_import``
+ for a single-purpose example.
.. note::
@@ -442,6 +443,40 @@ def find_spec(self, fullname, path, target):
return None
+@pytest.fixture(scope="function")
+def mock_missing_import():
+ """Pretend an optional dependency is not installed, for any module.
+
+ Yields a callable taking a module name, so a test for one optional
+ dependency does not need a fixture of its own::
+
+ def test_requires_the_extra(mock_missing_import):
+ mock_missing_import("diffable_rdf")
+ with pytest.raises(ImportError, match="linkml-runtime[diff-stable]"):
+ ...
+
+ See :class:`MockImportErrorFinder` for the caveat about reimporting
+ modules that already imported the one being hidden.
+ """
+ removed = {}
+ finders = []
+
+ def hide(module: str) -> None:
+ for name, loaded in list(sys.modules.items()):
+ if name.startswith(module):
+ removed[name] = loaded
+ del sys.modules[name]
+ finder = MockImportErrorFinder(module)
+ finders.append(finder)
+ sys.meta_path.insert(0, finder)
+
+ yield hide
+
+ sys.modules.update(removed)
+ for finder in finders:
+ sys.meta_path.remove(finder)
+
+
@pytest.fixture(scope="function")
def mock_black_import():
"""
diff --git a/tests/linkml/test_biolink_model/__snapshots__/biolink.schema.json b/tests/linkml/test_biolink_model/__snapshots__/biolink.schema.json
index f1923d984c..d76f06623b 100644
--- a/tests/linkml/test_biolink_model/__snapshots__/biolink.schema.json
+++ b/tests/linkml/test_biolink_model/__snapshots__/biolink.schema.json
@@ -1,7 +1,7 @@
{
"$defs": {
"AccessibleDnaRegion": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A region (or regions) of a chromatinized genome that has been measured to be more accessible to an enzyme such as DNase-I or Tn5 Transpose",
"properties": {
"category": {
@@ -136,7 +136,7 @@
"type": "object"
},
"Activity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An activity is something that occurs over a period of time and acts upon or with entities; it may include consuming, processing, transforming, modifying, relocating, using, or generating entities.",
"properties": {
"category": {
@@ -247,13 +247,13 @@
"type": "object"
},
"ActivityAndBehavior": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Activity or behavior of any independent integral living, organization or mechanical actor in the world",
"title": "ActivityAndBehavior",
"type": "object"
},
"AdministrativeEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -364,7 +364,7 @@
"type": "object"
},
"Agent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "person, group, organization or project that provides a piece of information (i.e. a knowledge association)",
"properties": {
"address": {
@@ -507,7 +507,7 @@
"type": "string"
},
"AnatomicalEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A subcellular location, cell type or gross anatomical part",
"properties": {
"category": {
@@ -635,7 +635,7 @@
"type": "object"
},
"AnatomicalEntityToAnatomicalEntityAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -1006,7 +1006,7 @@
"type": "object"
},
"AnatomicalEntityToAnatomicalEntityOntogenicAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A relationship between two anatomical entities where the relationship is ontogenic, i.e. the two entities are related by development. A number of different relationship types can be used to specify the precise nature of the relationship.",
"properties": {
"adjusted_p_value": {
@@ -1380,7 +1380,7 @@
"type": "object"
},
"AnatomicalEntityToAnatomicalEntityPartOfAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A relationship between two anatomical entities where the relationship is mereological, i.e the two entities are related by parthood. This includes relationships between cellular components and cells, between cells and tissues, tissues and whole organisms",
"properties": {
"adjusted_p_value": {
@@ -1760,7 +1760,7 @@
"type": "object"
},
"Annotation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Biolink Model root class for entity annotations.",
"title": "Annotation",
"type": "object"
@@ -1788,7 +1788,7 @@
"type": "string"
},
"Article": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a piece of writing on a particular topic presented as a stand-alone section of a larger publication",
"properties": {
"authors": {
@@ -2006,7 +2006,7 @@
"type": "object"
},
"Association": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A typed association between two entities, supported by evidence",
"properties": {
"adjusted_p_value": {
@@ -2377,7 +2377,7 @@
"type": "object"
},
"Attribute": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A property or characteristic of an entity. For example, an apple may have properties such as color, shape, age, crispiness. An environmental sample may have attributes such as depth, lat, long, material.",
"properties": {
"category": {
@@ -2510,7 +2510,7 @@
"type": "object"
},
"Bacterium": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A member of a group of unicellular microorganisms lacking a nuclear membrane, that reproduce by binary fission and are often motile.",
"properties": {
"category": {
@@ -2638,7 +2638,7 @@
"type": "object"
},
"Behavior": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -2796,7 +2796,7 @@
"type": "object"
},
"BehaviorToBehavioralFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between an mixture behavior and a behavioral feature manifested by the individual exhibited or has exhibited the behavior.",
"properties": {
"adjusted_p_value": {
@@ -3304,7 +3304,7 @@
"type": "object"
},
"BehavioralExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A behavioral exposure is a factor relating to behavior impacting an individual.",
"properties": {
"category": {
@@ -3445,7 +3445,7 @@
"type": "object"
},
"BehavioralFeature": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A phenotypic feature which is behavioral in nature.",
"properties": {
"category": {
@@ -3573,13 +3573,13 @@
"type": "object"
},
"BehavioralOutcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An outcome resulting from an exposure event which is the manifestation of human behavior.",
"title": "BehavioralOutcome",
"type": "object"
},
"BiologicalEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -3707,7 +3707,7 @@
"type": "object"
},
"BiologicalProcess": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "One or more causally connected executions of molecular functions",
"properties": {
"category": {
@@ -3865,7 +3865,7 @@
"type": "object"
},
"BiologicalProcessOrActivity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Either an individual molecular activity, or a collection of causally connected molecular activities in a biological system.",
"properties": {
"category": {
@@ -4023,7 +4023,7 @@
"type": "object"
},
"BiologicalSex": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -4156,7 +4156,7 @@
"type": "object"
},
"BioticExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An external biotic exposure is an intake of (sometimes pathological) biological organisms (including viruses).",
"properties": {
"category": {
@@ -4297,7 +4297,7 @@
"type": "object"
},
"Book": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "This class may rarely be instantiated except if use cases of a given knowledge graph support its utility.",
"properties": {
"authors": {
@@ -4490,7 +4490,7 @@
"type": "object"
},
"BookChapter": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"authors": {
@@ -4701,7 +4701,7 @@
"type": "object"
},
"Case": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An individual (human) organism that has a patient role in some clinical context.",
"properties": {
"category": {
@@ -4829,7 +4829,7 @@
"type": "object"
},
"CaseToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An abstract association for use where the case is the subject",
"properties": {
"object": {
@@ -4854,7 +4854,7 @@
"type": "object"
},
"CaseToPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a case (e.g. individual patient) and a phenotypic feature in which the individual has or has had the phenotype.",
"properties": {
"adjusted_p_value": {
@@ -5362,7 +5362,7 @@
"type": "object"
},
"CausalGeneToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -5912,7 +5912,7 @@
"type": "string"
},
"Cell": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -6040,7 +6040,7 @@
"type": "object"
},
"CellLine": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -6168,7 +6168,7 @@
"type": "object"
},
"CellLineAsAModelOfDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -6618,7 +6618,7 @@
"type": "object"
},
"CellLineToDiseaseOrPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An relationship between a cell line and a disease or a phenotype, where the cell line is derived from an individual with that disease or phenotype.",
"properties": {
"adjusted_p_value": {
@@ -6993,7 +6993,7 @@
"type": "object"
},
"CellLineToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An relationship between a cell line and another entity",
"properties": {
"object": {
@@ -7018,7 +7018,7 @@
"type": "object"
},
"CellularComponent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A location in or around a cell",
"properties": {
"category": {
@@ -7146,7 +7146,7 @@
"type": "object"
},
"CellularOrganism": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -7274,7 +7274,7 @@
"type": "object"
},
"ChemicalAffectsGeneAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Describes an effect that a chemical has on a gene or gene product (e.g. an impact of on its abundance, activity,localization, processing, expression, etc.)",
"properties": {
"adjusted_p_value": {
@@ -7840,7 +7840,7 @@
"type": "object"
},
"ChemicalEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A chemical entity is a physical entity that pertains to chemistry or biochemistry.",
"properties": {
"available_from": {
@@ -7992,7 +7992,7 @@
"type": "object"
},
"ChemicalEntityAssessesNamedThingAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -8374,13 +8374,13 @@
"type": "string"
},
"ChemicalEntityOrGeneOrGeneProduct": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A union of chemical entities and children, and gene or gene product. This mixin is helpful to use when searching across chemical entities that must include genes and their children as chemical entities.",
"title": "ChemicalEntityOrGeneOrGeneProduct",
"type": "object"
},
"ChemicalEntityOrGeneOrGeneProductRegulatesGeneAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A regulatory relationship between two genes",
"properties": {
"adjusted_p_value": {
@@ -8765,13 +8765,13 @@
"type": "object"
},
"ChemicalEntityOrProteinOrPolypeptide": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A union of chemical entities and children, and protein and polypeptide. This mixin is helpful to use when searching across chemical entities that must include genes and their children as chemical entities.",
"title": "ChemicalEntityOrProteinOrPolypeptide",
"type": "object"
},
"ChemicalEntityToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction between a chemical entity and another entity",
"properties": {
"object": {
@@ -8796,7 +8796,7 @@
"type": "object"
},
"ChemicalExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A chemical exposure is an intake of a particular chemical entity.",
"properties": {
"category": {
@@ -8937,7 +8937,7 @@
"type": "object"
},
"ChemicalGeneInteractionAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "describes a physical interaction between a chemical entity and a gene or gene product. Any biological or chemical effect resulting from such an interaction are out of scope, and covered by the ChemicalAffectsGeneAssociation type (e.g. impact of a chemical on the abundance, activity, structure, etc, of either participant in the interaction)",
"properties": {
"adjusted_p_value": {
@@ -9409,7 +9409,7 @@
"type": "object"
},
"ChemicalMixture": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A chemical mixture is a chemical entity composed of two or more molecular entities.",
"properties": {
"available_from": {
@@ -9600,13 +9600,13 @@
"type": "object"
},
"ChemicalOrDrugOrTreatment": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"title": "ChemicalOrDrugOrTreatment",
"type": "object"
},
"ChemicalOrDrugOrTreatmentSideEffectDiseaseOrPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "This association defines a relationship between a chemical or treatment (or procedure) and a disease or phenotypic feature where the disesae or phenotypic feature is a secondary, typically (but not always) undesirable effect.",
"properties": {
"FDA_adverse_event_level": {
@@ -10068,7 +10068,7 @@
"type": "object"
},
"ChemicalOrDrugOrTreatmentToDiseaseOrPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "This association defines a relationship between a chemical or treatment (or procedure) and a disease or phenotypic feature where the disease or phenotypic feature is a secondary undesirable effect.",
"properties": {
"FDA_adverse_event_level": {
@@ -10544,7 +10544,7 @@
"type": "string"
},
"ChemicalRole": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A role played by the molecular entity or part thereof within a chemical context.",
"examples": [
"CHEBI:35469"
@@ -10680,7 +10680,7 @@
"type": "object"
},
"ChemicalToChemicalAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A relationship between two chemical entities. This can encompass actual interactions as well as temporal causal edges, e.g. one chemical converted to another.",
"properties": {
"adjusted_p_value": {
@@ -11051,7 +11051,7 @@
"type": "object"
},
"ChemicalToChemicalDerivationAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A causal relationship between two chemical entities, where the subject represents the upstream entity and the object represents the downstream. For any such association there is an implicit reaction: IF R has-input C1 AND R has-output C2 AND R enabled-by P AND R type Reaction THEN C1 derives-into C2 catalyst qualifier P",
"properties": {
"adjusted_p_value": {
@@ -11436,7 +11436,7 @@
"type": "object"
},
"ChemicalToDiseaseOrPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction between a chemical entity and a phenotype or disease, where the presence of the chemical gives rise to or exacerbates the phenotype.",
"properties": {
"adjusted_p_value": {
@@ -11811,7 +11811,7 @@
"type": "object"
},
"ChemicalToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction between a chemical entity and another entity",
"properties": {
"object": {
@@ -11836,7 +11836,7 @@
"type": "object"
},
"ChemicalToPathwayAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction between a chemical entity and a biological process or pathway.",
"properties": {
"adjusted_p_value": {
@@ -12207,7 +12207,7 @@
"type": "object"
},
"ChiSquaredAnalysisResult": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A result of a chi squared analysis.",
"properties": {
"category": {
@@ -12357,7 +12357,7 @@
"type": "string"
},
"ClinicalAttribute": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Attributes relating to a clinical manifestation",
"properties": {
"category": {
@@ -12490,7 +12490,7 @@
"type": "object"
},
"ClinicalCourse": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "The course a disease typically takes from its onset, progression in time, and eventual resolution or death of the affected individual",
"properties": {
"category": {
@@ -12623,7 +12623,7 @@
"type": "object"
},
"ClinicalEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any entity or process that exists in the clinical domain and outside the biological realm. Diseases are placed under biological entities",
"properties": {
"category": {
@@ -12734,7 +12734,7 @@
"type": "object"
},
"ClinicalFinding": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "this category is currently considered broad enough to tag clinical lab measurements and other biological attributes taken as 'clinical traits' with some statistical score, for example, a p value in genetic associations.",
"properties": {
"category": {
@@ -12862,7 +12862,7 @@
"type": "object"
},
"ClinicalIntervention": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -12973,7 +12973,7 @@
"type": "object"
},
"ClinicalMeasurement": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A clinical measurement is a special kind of attribute which results from a laboratory observation from a subject individual or sample. Measurements can be connected to their subject by the 'has attribute' slot.",
"properties": {
"category": {
@@ -13106,7 +13106,7 @@
"type": "object"
},
"ClinicalModifier": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Used to characterize and specify the phenotypic abnormalities defined in the phenotypic abnormality sub-ontology, with respect to severity, laterality, and other aspects",
"properties": {
"category": {
@@ -13239,7 +13239,7 @@
"type": "object"
},
"ClinicalTrial": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -13350,7 +13350,7 @@
"type": "object"
},
"CodingSequence": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -13485,7 +13485,7 @@
"type": "object"
},
"Cohort": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A group of people banded together or treated as a group who share common characteristics. A cohort 'study' is a particular form of longitudinal study that samples a cohort, performing a cross-section at intervals through time.",
"properties": {
"category": {
@@ -13613,7 +13613,7 @@
"type": "object"
},
"CommonDataElement": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A Common Data Element (CDE) is a standardized, precisely defined question, paired with a set of allowable responses, used systematically across different sites, studies, or clinical trials to ensure consistent data collection. Multiple CDEs (from one or more Collections) can be curated into Forms. (https://cde.nlm.nih.gov/home)",
"properties": {
"category": {
@@ -13750,7 +13750,7 @@
"type": "object"
},
"ComplexChemicalExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A complex chemical exposure is an intake of a chemical mixture (e.g. gasoline), other than a drug.",
"properties": {
"category": {
@@ -13883,7 +13883,7 @@
"type": "object"
},
"ComplexMolecularMixture": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A complex molecular mixture is a chemical mixture composed of two or more molecular entities with unknown concentration and stoichiometry.",
"properties": {
"available_from": {
@@ -14074,7 +14074,7 @@
"type": "object"
},
"ConceptCountAnalysisResult": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A result of a concept count analysis.",
"properties": {
"category": {
@@ -14211,7 +14211,7 @@
"type": "object"
},
"ConfidenceLevel": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Level of confidence in a statement",
"properties": {
"category": {
@@ -14348,7 +14348,7 @@
"type": "object"
},
"ContributorAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any association between an entity (such as a publication) and various agents that contribute to its realisation",
"properties": {
"adjusted_p_value": {
@@ -14726,7 +14726,7 @@
"type": "object"
},
"CorrelatedGeneToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -15251,7 +15251,7 @@
"type": "object"
},
"Dataset": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "an item that refers to a collection of data from a data source.",
"properties": {
"category": {
@@ -15388,7 +15388,7 @@
"type": "object"
},
"DatasetDistribution": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "an item that holds distribution level information about a dataset.",
"properties": {
"category": {
@@ -15531,7 +15531,7 @@
"type": "object"
},
"DatasetSummary": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "an item that holds summary level information about a dataset.",
"properties": {
"category": {
@@ -15680,7 +15680,7 @@
"type": "object"
},
"DatasetVersion": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "an item that holds version level information about a dataset.",
"properties": {
"category": {
@@ -15835,7 +15835,7 @@
"type": "object"
},
"Device": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A thing made or adapted for a particular purpose, especially a piece of mechanical or electronic equipment",
"properties": {
"category": {
@@ -15946,7 +15946,7 @@
"type": "object"
},
"DiagnosticAid": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A device or substance used to help diagnose disease or injury",
"properties": {
"category": {
@@ -16068,7 +16068,7 @@
"type": "string"
},
"Disease": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A disorder of structure or function, especially one that produces specific signs, phenotypes or symptoms or that affects a specific location and is not simply a direct result of physical injury. A disposition to undergo pathological processes that exists in an organism because of one or more disorders in that organism.",
"properties": {
"category": {
@@ -16196,7 +16196,7 @@
"type": "object"
},
"DiseaseOrPhenotypicFeature": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Either one of a disease or an individual phenotypic feature. Some knowledge resources such as Monarch treat these as distinct, others such as MESH conflate. Please see definitions of phenotypic feature and disease in this model for their independent descriptions. This class is helpful to enforce domains and ranges that may involve either a disease or a phenotypic feature.",
"properties": {
"category": {
@@ -16324,7 +16324,7 @@
"type": "object"
},
"DiseaseOrPhenotypicFeatureExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A disease or phenotypic feature state, when viewed as an exposure, represents an precondition, leading to or influencing an outcome, e.g. HIV predisposing an individual to infections; a relative deficiency of skin pigmentation predisposing an individual to skin cancer.",
"properties": {
"category": {
@@ -16465,13 +16465,13 @@
"type": "object"
},
"DiseaseOrPhenotypicFeatureOutcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Physiological outcomes resulting from an exposure event which is the manifestation of a disease or other characteristic phenotype.",
"title": "DiseaseOrPhenotypicFeatureOutcome",
"type": "object"
},
"DiseaseOrPhenotypicFeatureToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"object": {
@@ -16500,7 +16500,7 @@
"type": "object"
},
"DiseaseOrPhenotypicFeatureToGeneticInheritanceAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between either a disease or a phenotypic feature and its mode of (genetic) inheritance.",
"properties": {
"adjusted_p_value": {
@@ -16881,7 +16881,7 @@
"type": "object"
},
"DiseaseOrPhenotypicFeatureToLocationAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between either a disease or a phenotypic feature and an anatomical entity, where the disease/feature manifests in that site.",
"properties": {
"adjusted_p_value": {
@@ -17259,7 +17259,7 @@
"type": "object"
},
"DiseaseToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"object": {
@@ -17287,7 +17287,7 @@
"type": "object"
},
"DiseaseToExposureEventAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between an exposure event and a disease.",
"properties": {
"adjusted_p_value": {
@@ -17661,7 +17661,7 @@
"type": "object"
},
"DiseaseToPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a disease and a phenotypic feature in which the phenotypic feature is associated with the disease in some way.",
"properties": {
"adjusted_p_value": {
@@ -18179,7 +18179,7 @@
"type": "object"
},
"Drug": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A substance intended for use in the diagnosis, cure, mitigation, treatment, or prevention of disease",
"properties": {
"available_from": {
@@ -18390,7 +18390,7 @@
"type": "string"
},
"DrugExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A drug exposure is an intake of a particular drug.",
"properties": {
"category": {
@@ -18531,7 +18531,7 @@
"type": "object"
},
"DrugLabel": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a document accompanying a drug or its container that provides written, printed or graphic information about the drug, including drug contents, specific instructions or warnings for administration, storage and disposal instructions, etc.",
"properties": {
"authors": {
@@ -18723,7 +18723,7 @@
"type": "object"
},
"DrugToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction between a drug and another entity",
"properties": {
"object": {
@@ -18748,7 +18748,7 @@
"type": "object"
},
"DrugToGeneAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction between a drug and a gene or gene product.",
"properties": {
"adjusted_p_value": {
@@ -19119,7 +19119,7 @@
"type": "object"
},
"DrugToGeneInteractionExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "drug to gene interaction exposure is a drug exposure is where the interactions of the drug with specific genes are known to constitute an 'exposure' to the organism, leading to or influencing an outcome.",
"properties": {
"category": {
@@ -19281,7 +19281,7 @@
"type": "string"
},
"DruggableGeneToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -19797,7 +19797,7 @@
"type": "object"
},
"Entity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Root Biolink Model class for all things and informational relationships, real or imagined.",
"properties": {
"category": {
@@ -19873,7 +19873,7 @@
"type": "object"
},
"EntityToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -20265,7 +20265,7 @@
"type": "object"
},
"EntityToDiseaseAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "mixin class for any association whose object (target node) is a disease",
"properties": {
"disease_context_qualifier": {
@@ -20366,7 +20366,7 @@
"type": "object"
},
"EntityToDiseaseOrPhenotypicFeatureAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"object": {
@@ -20395,7 +20395,7 @@
"type": "object"
},
"EntityToExposureEventAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between some entity and an exposure event.",
"properties": {
"object": {
@@ -20420,7 +20420,7 @@
"type": "object"
},
"EntityToFeatureOrDiseaseQualifiersMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Qualifiers for entity to disease or phenotype associations.",
"properties": {
"disease_context_qualifier": {
@@ -20518,7 +20518,7 @@
"type": "object"
},
"EntityToOutcomeAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between some entity and an outcome",
"properties": {
"object": {
@@ -20543,7 +20543,7 @@
"type": "object"
},
"EntityToPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -20935,7 +20935,7 @@
"type": "object"
},
"EntityToPhenotypicFeatureAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"anatomical_context_qualifier": {
@@ -21097,7 +21097,7 @@
"type": "object"
},
"EnvironmentalExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A environmental exposure is a factor relating to abiotic processes in the environment including sunlight (UV-B), atmospheric (heat, cold, general pollution) and water-born contaminants.",
"properties": {
"category": {
@@ -21238,7 +21238,7 @@
"type": "object"
},
"EnvironmentalFeature": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -21349,7 +21349,7 @@
"type": "object"
},
"EnvironmentalFoodContaminant": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"available_from": {
@@ -21501,7 +21501,7 @@
"type": "object"
},
"EnvironmentalProcess": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -21612,13 +21612,13 @@
"type": "object"
},
"EpidemiologicalOutcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An epidemiological outcome, such as societal disease burden, resulting from an exposure event.",
"title": "EpidemiologicalOutcome",
"type": "object"
},
"EpigenomicEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"has_biological_sequence": {
@@ -21633,7 +21633,7 @@
"type": "object"
},
"Event": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Something that happens at a given place and time.",
"properties": {
"category": {
@@ -21744,7 +21744,7 @@
"type": "object"
},
"EvidenceType": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Class of evidence that supports an association",
"properties": {
"category": {
@@ -21881,7 +21881,7 @@
"type": "object"
},
"Exon": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A region of the transcript sequence within a gene which is not removed from the primary RNA transcript by RNA splicing.",
"properties": {
"category": {
@@ -22009,7 +22009,7 @@
"type": "object"
},
"ExonToTranscriptRelationship": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A transcript is formed from multiple exons",
"properties": {
"adjusted_p_value": {
@@ -22380,7 +22380,7 @@
"type": "object"
},
"ExposureEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A (possibly time bounded) incidence of a feature of the environment of an organism that influences one or more phenotypic features of that organism, potentially mediated by genes",
"properties": {
"id": {
@@ -22403,7 +22403,7 @@
"type": "object"
},
"ExposureEventToOutcomeAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between an exposure event and an outcome.",
"properties": {
"adjusted_p_value": {
@@ -22789,7 +22789,7 @@
"type": "object"
},
"ExposureEventToPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any association between an environment and a phenotypic feature, where being in the environment influences the phenotype.",
"properties": {
"adjusted_p_value": {
@@ -23308,7 +23308,7 @@
"type": "string"
},
"FeatureOrDiseaseQualifiersToEntityMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Qualifiers for disease or phenotype to entity associations.",
"properties": {
"frequency_qualifier": {
@@ -23395,7 +23395,7 @@
"type": "object"
},
"Food": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A substance consumed by a living organism as a source of nutrition",
"properties": {
"available_from": {
@@ -23586,7 +23586,7 @@
"type": "object"
},
"FoodAdditive": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"available_from": {
@@ -23738,7 +23738,7 @@
"type": "object"
},
"FrequencyQualifierMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Qualifier for frequency type associations",
"properties": {
"frequency_qualifier": {
@@ -23770,7 +23770,7 @@
"type": "object"
},
"FrequencyQuantifier": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"has_count": {
@@ -23805,7 +23805,7 @@
"type": "object"
},
"FunctionalAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a macromolecular machine mixin (gene, gene product or complex of gene products) and either a molecular activity, a biological process or a cellular location in which a function is executed.",
"properties": {
"adjusted_p_value": {
@@ -24183,7 +24183,7 @@
"type": "object"
},
"Fungus": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A kingdom of eukaryotic, heterotrophic organisms that live as saprobes or parasites, including mushrooms, yeasts, smuts, molds, etc. They reproduce either sexually or asexually, and have life cycles that range from simple to complex. Filamentous fungi refer to those that grow as multicellular colonies (mushrooms and molds).",
"properties": {
"category": {
@@ -24311,7 +24311,7 @@
"type": "object"
},
"Gene": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A region (or regions) that includes all of the sequence elements necessary to encode a functional transcript. A gene locus may include regulatory regions, transcribed regions and/or other functional sequence regions.",
"properties": {
"category": {
@@ -24453,7 +24453,7 @@
"type": "object"
},
"GeneAffectsChemicalAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Describes an effect that a gene or gene product has on a chemical entity (e.g. an impact of on its abundance, activity, localization, processing, transport, etc.)",
"examples": [
"JsonObj(subject='TRPC4', predicate='affects', qualified_predicte='causes', object='Barium', object_aspect_qualifier='transport', object_direction_qualifier='increased')"
@@ -25032,7 +25032,7 @@
"type": "object"
},
"GeneAsAModelOfDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -25548,7 +25548,7 @@
"type": "object"
},
"GeneExpressionMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Observed gene expression intensity, context (site, stage) and associated phenotypic status within which the expression occurs.",
"properties": {
"expression_site": {
@@ -25590,7 +25590,7 @@
"type": "object"
},
"GeneFamily": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "any grouping of multiple genes or gene products related by common descent",
"properties": {
"category": {
@@ -25728,7 +25728,7 @@
"type": "object"
},
"GeneGroupingMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "any grouping of multiple genes or gene products",
"properties": {
"has_gene_or_gene_product": {
@@ -25746,7 +25746,7 @@
"type": "object"
},
"GeneHasVariantThatContributesToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -26277,7 +26277,7 @@
"type": "object"
},
"GeneOrGeneProduct": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A union of gene loci or gene products. Frequently an identifier for one will be used as proxy for another",
"properties": {
"name": {
@@ -26367,7 +26367,7 @@
"type": "string"
},
"GeneProductIsoformMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "This is an abstract class that can be mixed in with different kinds of gene products to indicate that the gene product is intended to represent a specific isoform rather than a canonical or reference or generic product. The designation of canonical or reference may be arbitrary, or it may represent the superclass of all isoforms.",
"properties": {
"name": {
@@ -26402,7 +26402,7 @@
"type": "object"
},
"GeneProductMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "The functional molecular product of a single gene locus. Gene products are either proteins or functional RNA molecules.",
"properties": {
"name": {
@@ -26437,7 +26437,7 @@
"type": "object"
},
"GeneRegulatesGeneAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Describes a regulatory relationship between two genes or gene products.",
"examples": [
"JsonObj(subject='NCBIGene:551', predicate='regulates', qualified_predicte='causes', object='NCBIGene:1636', object_aspect_qualifier='activity_or_abundance', object_direction_qualifier='downregulated')"
@@ -26846,7 +26846,7 @@
"type": "object"
},
"GeneToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -27371,7 +27371,7 @@
"type": "object"
},
"GeneToDiseaseOrPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -27898,7 +27898,7 @@
"type": "object"
},
"GeneToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"object": {
@@ -27923,7 +27923,7 @@
"type": "object"
},
"GeneToExpressionSiteAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a gene and a gene expression site, possibly qualified by stage/timing info.",
"properties": {
"adjusted_p_value": {
@@ -28317,7 +28317,7 @@
"type": "object"
},
"GeneToGeneAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "abstract parent class for different kinds of gene-gene or gene product to gene product relationships. Includes homology and interaction.",
"properties": {
"adjusted_p_value": {
@@ -28688,7 +28688,7 @@
"type": "object"
},
"GeneToGeneCoexpressionAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Indicates that two genes are co-expressed, generally under the same conditions.",
"properties": {
"adjusted_p_value": {
@@ -29096,7 +29096,7 @@
"type": "object"
},
"GeneToGeneFamilyAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Set membership of a gene in a family of genes related by common evolutionary ancestry usually inferred by sequence comparisons. The genes in a given family generally share common sequence motifs which generally map onto shared gene product structure-function relationships.",
"properties": {
"adjusted_p_value": {
@@ -29470,7 +29470,7 @@
"type": "object"
},
"GeneToGeneHomologyAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A homology association between two genes. May be orthology (in which case the species of subject and object should differ) or paralogy (in which case the species may be the same)",
"properties": {
"adjusted_p_value": {
@@ -29847,7 +29847,7 @@
"type": "object"
},
"GeneToGeneProductRelationship": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A gene is transcribed and potentially translated to a gene product",
"properties": {
"adjusted_p_value": {
@@ -30221,7 +30221,7 @@
"type": "object"
},
"GeneToGoTermAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -30598,7 +30598,7 @@
"type": "object"
},
"GeneToPathwayAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction between a gene or gene product and a biological process or pathway.",
"properties": {
"adjusted_p_value": {
@@ -30969,7 +30969,7 @@
"type": "object"
},
"GeneToPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -31496,7 +31496,7 @@
"type": "object"
},
"GeneticInheritance": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "The pattern or 'mode' in which a particular genetic trait or disorder is passed from one generation to the next, e.g. autosomal dominant, autosomal recessive, etc.",
"properties": {
"category": {
@@ -31624,7 +31624,7 @@
"type": "object"
},
"Genome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A genome is the sum of genetic material within a cell or virion.",
"properties": {
"category": {
@@ -31759,7 +31759,7 @@
"type": "object"
},
"GenomicBackgroundExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A genomic background exposure is where an individual's specific genomic background of genes, sequence variants or other pre-existing genomic conditions constitute a kind of 'exposure' to the organism, leading to or influencing an outcome.",
"properties": {
"category": {
@@ -31934,7 +31934,7 @@
"type": "object"
},
"GenomicEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"has_biological_sequence": {
@@ -31949,7 +31949,7 @@
"type": "object"
},
"GenomicSequenceLocalization": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A relationship between a sequence feature and a nucleic acid entity it is localized to. The reference entity may be a chromosome, chromosome region or information entity such as a contig.",
"properties": {
"adjusted_p_value": {
@@ -32370,7 +32370,7 @@
"type": "object"
},
"Genotype": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An information content entity that describes a genome by specifying the total variation in genomic sequence and/or gene expression, relative to some established background",
"properties": {
"category": {
@@ -32511,7 +32511,7 @@
"type": "object"
},
"GenotypeAsAModelOfDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -32961,7 +32961,7 @@
"type": "object"
},
"GenotypeToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -33411,7 +33411,7 @@
"type": "object"
},
"GenotypeToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"object": {
@@ -33436,7 +33436,7 @@
"type": "object"
},
"GenotypeToGeneAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any association between a genotype and a gene. The genotype have have multiple variants in that gene or a single one. There is no assumption of cardinality",
"properties": {
"adjusted_p_value": {
@@ -33807,7 +33807,7 @@
"type": "object"
},
"GenotypeToGenotypePartAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any association between one genotype and a genotypic entity that is a sub-component of it",
"properties": {
"adjusted_p_value": {
@@ -34181,7 +34181,7 @@
"type": "object"
},
"GenotypeToPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any association between one genotype and a phenotypic feature, where having the genotype confers the phenotype, either in isolation or through environment",
"properties": {
"adjusted_p_value": {
@@ -34692,7 +34692,7 @@
"type": "object"
},
"GenotypeToVariantAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any association between a genotype and a sequence variant.",
"properties": {
"adjusted_p_value": {
@@ -35063,7 +35063,7 @@
"type": "object"
},
"GenotypicSex": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An attribute corresponding to the genotypic sex of the individual, based upon genotypic composition of sex chromosomes.",
"properties": {
"category": {
@@ -35196,7 +35196,7 @@
"type": "object"
},
"GeographicExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A geographic exposure is a factor relating to geographic proximity to some impactful entity.",
"properties": {
"category": {
@@ -35337,7 +35337,7 @@
"type": "object"
},
"GeographicLocation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a location that can be described in lat/long coordinates",
"properties": {
"category": {
@@ -35462,7 +35462,7 @@
"type": "object"
},
"GeographicLocationAtTime": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a location that can be described in lat/long coordinates, for a particular time",
"properties": {
"category": {
@@ -35595,7 +35595,7 @@
"type": "object"
},
"GrossAnatomicalStructure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -35723,7 +35723,7 @@
"type": "object"
},
"Haplotype": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A set of zero or more Alleles on a single instance of a Sequence[VMC]",
"properties": {
"category": {
@@ -35858,7 +35858,7 @@
"type": "object"
},
"Hospitalization": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -35969,13 +35969,13 @@
"type": "object"
},
"HospitalizationOutcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An outcome resulting from an exposure event which is the increased manifestation of acute (e.g. emergency room visit) or chronic (inpatient) hospitalization.",
"title": "HospitalizationOutcome",
"type": "object"
},
"Human": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A member of the the species Homo sapiens.",
"properties": {
"category": {
@@ -36103,7 +36103,7 @@
"type": "object"
},
"IndividualOrganism": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An instance of an organism. For example, Richard Nixon, Charles Darwin, my pet cat. Example ID: ORCID:0000-0002-5355-2576",
"properties": {
"category": {
@@ -36231,7 +36231,7 @@
"type": "object"
},
"InformationContentEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a piece of information that typically describes some topic of discourse or is used as support.",
"properties": {
"category": {
@@ -36368,7 +36368,7 @@
"type": "object"
},
"InformationContentEntityToNamedThingAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "association between a named thing and a information content entity where the specific context of the relationship between that named thing and the publication is unknown. For example, model organisms databases often capture the knowledge that a gene is found in a journal article, but not specifically the context in which that gene was documented in the article. In these cases, this association with the accompanying predicate 'mentions' could be used. Conversely, for more specific associations (like 'gene to disease association', the publication should be captured as an edge property).",
"properties": {
"adjusted_p_value": {
@@ -36742,7 +36742,7 @@
"type": "object"
},
"Invertebrate": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An animal lacking a vertebral column. This group consists of 98% of all animal species.",
"properties": {
"category": {
@@ -36870,7 +36870,7 @@
"type": "object"
},
"JournalArticle": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "an article, typically presenting results of research, that is published in an issue of a scientific journal.",
"properties": {
"authors": {
@@ -37101,7 +37101,7 @@
"type": "string"
},
"LifeStage": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A stage of development or growth of an organism, including post-natal adult stages",
"properties": {
"category": {
@@ -37229,7 +37229,7 @@
"type": "object"
},
"LogOddsAnalysisResult": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A result of a log odds ratio analysis.",
"properties": {
"category": {
@@ -37376,7 +37376,7 @@
"type": "string"
},
"MacromolecularComplex": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A stable assembly of two or more macromolecules, i.e. proteins, nucleic acids, carbohydrates or lipids, in which at least one component is a protein and the constituent parts function together.",
"properties": {
"category": {
@@ -37504,7 +37504,7 @@
"type": "object"
},
"MacromolecularMachineMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A union of gene locus, gene product, and macromolecular complex. These are the basic units of function in a cell. They either carry out individual biological activities, or they encode molecules which do this.",
"properties": {
"name": {
@@ -37519,7 +37519,7 @@
"type": "object"
},
"MacromolecularMachineToBiologicalProcessAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A functional association between a macromolecular machine (gene, gene product or complex) and a biological process or pathway (as represented in the GO biological process branch), where the entity carries out some part of the process, regulates it, or acts upstream of it.",
"properties": {
"adjusted_p_value": {
@@ -37908,7 +37908,7 @@
"type": "object"
},
"MacromolecularMachineToCellularComponentAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A functional association between a macromolecular machine (gene, gene product or complex) and a cellular component (as represented in the GO cellular component branch), where the entity carries out its function in the cellular component.",
"properties": {
"adjusted_p_value": {
@@ -38297,7 +38297,7 @@
"type": "object"
},
"MacromolecularMachineToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "an association which has a macromolecular machine mixin as a subject",
"properties": {
"object": {
@@ -38333,7 +38333,7 @@
"type": "object"
},
"MacromolecularMachineToMolecularActivityAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A functional association between a macromolecular machine (gene, gene product or complex) and a molecular activity (as represented in the GO molecular function branch), where the entity carries out the activity, or contributes to its execution.",
"properties": {
"adjusted_p_value": {
@@ -38722,7 +38722,7 @@
"type": "object"
},
"Mammal": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A member of the class Mammalia, a clade of endothermic amniotes distinguished from reptiles and birds by the possession of hair, three middle ear bones, mammary glands, and a neocortex",
"properties": {
"category": {
@@ -38850,7 +38850,7 @@
"type": "object"
},
"MappingCollection": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A collection of deprecated mappings.",
"properties": {
"predicate_mappings": {
@@ -38868,7 +38868,7 @@
"type": "object"
},
"MaterialSample": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A sample is a limited quantity of something (e.g. an individual or set of individuals from a population, or a portion of a substance) to be used for testing, analysis, inspection, investigation, demonstration, or trial use. [SIO]",
"properties": {
"category": {
@@ -38979,7 +38979,7 @@
"type": "object"
},
"MaterialSampleDerivationAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a material sample and the material entity from which it is derived.",
"properties": {
"adjusted_p_value": {
@@ -39354,7 +39354,7 @@
"type": "object"
},
"MaterialSampleToDiseaseOrPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a material sample and a disease or phenotype.",
"properties": {
"adjusted_p_value": {
@@ -39729,7 +39729,7 @@
"type": "object"
},
"MaterialSampleToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a material sample and something.",
"properties": {
"object": {
@@ -39754,7 +39754,7 @@
"type": "object"
},
"MicroRNA": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -39882,7 +39882,7 @@
"type": "object"
},
"ModelToDiseaseAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "This mixin is used for any association class for which the subject (source node) plays the role of a 'model', in that it recapitulates some features of the disease in a way that is useful for studying the disease outside a patient carrying the disease",
"properties": {
"object": {
@@ -39910,7 +39910,7 @@
"type": "object"
},
"MolecularActivity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An execution of a molecular function carried out by a gene product or macromolecular complex.",
"properties": {
"category": {
@@ -40068,7 +40068,7 @@
"type": "object"
},
"MolecularActivityToChemicalEntityAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Added in response to capturing relationship between microbiome activities as measured via measurements of blood analytes as collected via blood and stool samples",
"properties": {
"adjusted_p_value": {
@@ -40439,7 +40439,7 @@
"type": "object"
},
"MolecularActivityToMolecularActivityAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Added in response to capturing relationship between microbiome activities as measured via measurements of blood analytes as collected via blood and stool samples",
"properties": {
"adjusted_p_value": {
@@ -40810,7 +40810,7 @@
"type": "object"
},
"MolecularActivityToPathwayAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Association that holds the relationship between a reaction and the pathway it participates in.",
"properties": {
"adjusted_p_value": {
@@ -41190,7 +41190,7 @@
"type": "object"
},
"MolecularEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A molecular entity is a chemical entity composed of individual or covalently bonded atoms.",
"properties": {
"available_from": {
@@ -41349,7 +41349,7 @@
"type": "object"
},
"MolecularMixture": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A molecular mixture is a chemical mixture composed of two or more molecular entities with known concentration and stoichiometry.",
"properties": {
"available_from": {
@@ -41540,13 +41540,13 @@
"type": "object"
},
"MortalityOutcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An outcome of death from resulting from an exposure event.",
"title": "MortalityOutcome",
"type": "object"
},
"NamedThing": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a databased entity or concept/class",
"properties": {
"category": {
@@ -41657,7 +41657,7 @@
"type": "object"
},
"NamedThingAssociatedWithLikelihoodOfNamedThingAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -42097,7 +42097,7 @@
"type": "object"
},
"NoncodingRNAProduct": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -42225,7 +42225,7 @@
"type": "object"
},
"NucleicAcidEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A nucleic acid entity is a molecular entity characterized by availability in gene databases of nucleotide-based sequence representations of its precise sequence; for convenience of representation, partial sequences of various kinds are included.",
"properties": {
"available_from": {
@@ -42408,7 +42408,7 @@
"type": "object"
},
"NucleicAcidSequenceMotif": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A linear nucleotide sequence pattern that is widespread and has, or is conjectured to have, a biological significance. e.g. the TATA box promoter motif, transcription factor binding consensus sequences.",
"properties": {
"category": {
@@ -42536,7 +42536,7 @@
"type": "object"
},
"NucleosomeModification": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A chemical modification of a histone protein within a nucleosome octomer or a substitution of a histone with a variant histone isoform. e.g. Histone 4 Lysine 20 methylation (H4K20me), histone variant H2AZ substituting H2A.",
"properties": {
"category": {
@@ -42671,7 +42671,7 @@
"type": "object"
},
"ObservedExpectedFrequencyAnalysisResult": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A result of a observed expected frequency analysis.",
"properties": {
"category": {
@@ -42808,13 +42808,13 @@
"type": "object"
},
"Occurrent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A processual entity.",
"title": "Occurrent",
"type": "object"
},
"Onset": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "The age group in which (disease) symptom manifestations appear.",
"properties": {
"category": {
@@ -42947,7 +42947,7 @@
"type": "object"
},
"OntologyClass": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a concept or class in an ontology, vocabulary or thesaurus. Note that nodes in a biolink compatible KG can be considered both instances of biolink classes, and OWL classes in their own right. In general you should not need to use this class directly. Instead, use the appropriate biolink class. For example, for the GO concept of endocytosis (GO:0006897), use bl:BiologicalProcess as the type.",
"examples": [
"UBERON:0000955"
@@ -42965,7 +42965,7 @@
"type": "object"
},
"OrganismAttribute": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "describes a characteristic of an organismal entity.",
"properties": {
"category": {
@@ -43098,7 +43098,7 @@
"type": "object"
},
"OrganismTaxon": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A classification of a set of organisms. Example instances: NCBITaxon:9606 (Homo sapiens), NCBITaxon:2 (Bacteria). Can also be used to represent strains or subspecies.",
"properties": {
"category": {
@@ -43215,7 +43215,7 @@
"type": "object"
},
"OrganismTaxonToEntityAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between an organism taxon and another entity",
"properties": {
"object": {
@@ -43240,7 +43240,7 @@
"type": "object"
},
"OrganismTaxonToEnvironmentAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -43611,7 +43611,7 @@
"type": "object"
},
"OrganismTaxonToOrganismTaxonAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A relationship between two organism taxon nodes",
"properties": {
"adjusted_p_value": {
@@ -43982,7 +43982,7 @@
"type": "object"
},
"OrganismTaxonToOrganismTaxonInteraction": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction relationship between two taxa. This may be a symbiotic relationship (encompassing mutualism and parasitism), or it may be non-symbiotic. Example: plague transmitted_by flea; cattle domesticated_by Homo sapiens; plague infects Homo sapiens",
"properties": {
"adjusted_p_value": {
@@ -44371,7 +44371,7 @@
"type": "object"
},
"OrganismTaxonToOrganismTaxonSpecialization": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A child-parent relationship between two taxa. For example: Homo sapiens subclass_of Homo",
"properties": {
"adjusted_p_value": {
@@ -44745,7 +44745,7 @@
"type": "object"
},
"OrganismToOrganismAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -45116,7 +45116,7 @@
"type": "object"
},
"OrganismalEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A named entity that is either a part of an organism, a whole organism, population or clade of organisms, excluding chemical entities",
"properties": {
"category": {
@@ -45244,7 +45244,7 @@
"type": "object"
},
"OrganismalEntityAsAModelOfDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -45694,13 +45694,13 @@
"type": "object"
},
"Outcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An entity that has the role of being the consequence of an exposure event. This is an abstract mixin grouping of various categories of possible biological or non-biological (e.g. clinical) outcomes.",
"title": "Outcome",
"type": "object"
},
"PairwiseGeneToGeneInteraction": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction between two genes or two gene products. May be physical (e.g. protein binding) or genetic (between genes). May be symmetric (e.g. protein interaction) or directed (e.g. phosphorylation)",
"properties": {
"adjusted_p_value": {
@@ -46082,7 +46082,7 @@
"type": "object"
},
"PairwiseMolecularInteraction": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An interaction at the molecular level between two physical entities",
"properties": {
"adjusted_p_value": {
@@ -46479,7 +46479,7 @@
"type": "object"
},
"Patent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a legal document granted by a patent issuing authority which confers upon the patenter the sole right to make, use and sell an invention for a set period of time.",
"properties": {
"authors": {
@@ -46671,13 +46671,13 @@
"type": "object"
},
"PathognomonicityQuantifier": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A relationship quantifier between a variant or symptom and a disease, which is high when the presence of the feature implies the existence of the disease",
"title": "PathognomonicityQuantifier",
"type": "object"
},
"PathologicalAnatomicalExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An abnormal anatomical structure, when viewed as an exposure, representing an precondition, leading to or influencing an outcome, e.g. thrombosis leading to an ischemic disease outcome.",
"properties": {
"category": {
@@ -46818,13 +46818,13 @@
"type": "object"
},
"PathologicalAnatomicalOutcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An outcome resulting from an exposure event which is the manifestation of an abnormal anatomical structure.",
"title": "PathologicalAnatomicalOutcome",
"type": "object"
},
"PathologicalAnatomicalStructure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An anatomical structure with the potential of have an abnormal or deleterious effect at the subcellular, cellular, multicellular, or organismal level.",
"properties": {
"category": {
@@ -46952,13 +46952,13 @@
"type": "object"
},
"PathologicalEntityMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A pathological (abnormal) structure or process.",
"title": "PathologicalEntityMixin",
"type": "object"
},
"PathologicalProcess": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A biologic function or a process having an abnormal or deleterious effect at the subcellular, cellular, multicellular, or organismal level.",
"properties": {
"category": {
@@ -47116,7 +47116,7 @@
"type": "object"
},
"PathologicalProcessExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A pathological process, when viewed as an exposure, representing a precondition, leading to or influencing an outcome, e.g. autoimmunity leading to disease.",
"properties": {
"category": {
@@ -47257,13 +47257,13 @@
"type": "object"
},
"PathologicalProcessOutcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An outcome resulting from an exposure event which is the manifestation of a pathological process.",
"title": "PathologicalProcessOutcome",
"type": "object"
},
"Pathway": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -47431,7 +47431,7 @@
"type": "string"
},
"Phenomenon": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a fact or situation that is observed to exist or happen, especially one whose cause or explanation is in question",
"properties": {
"category": {
@@ -47542,7 +47542,7 @@
"type": "object"
},
"PhenotypicFeature": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A combination of entity and quality that makes up a phenotyping statement. An observable characteristic of an individual resulting from the interaction of its genotype with its molecular and physical environment.",
"examples": [
"MP:0001262"
@@ -47673,7 +47673,7 @@
"type": "object"
},
"PhenotypicFeatureToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -48183,7 +48183,7 @@
"type": "object"
},
"PhenotypicFeatureToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"frequency_qualifier": {
@@ -48309,7 +48309,7 @@
"type": "object"
},
"PhenotypicFeatureToPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Association between two concept nodes of phenotypic character, qualified by the predicate used. This association may typically be used to specify 'similar_to' or 'member_of' relationships.",
"properties": {
"adjusted_p_value": {
@@ -48822,7 +48822,7 @@
"type": "object"
},
"PhenotypicQuality": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A property of a phenotype",
"examples": [
"weight"
@@ -48958,7 +48958,7 @@
"type": "object"
},
"PhenotypicSex": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An attribute corresponding to the phenotypic sex of the individual, based upon the reproductive organs present.",
"properties": {
"category": {
@@ -49091,7 +49091,7 @@
"type": "object"
},
"PhysicalEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An entity that has material reality (a.k.a. physical essence).",
"properties": {
"category": {
@@ -49202,19 +49202,19 @@
"type": "object"
},
"PhysicalEssence": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Semantic mixin concept. Pertains to entities that have physical properties such as mass, volume, or charge.",
"title": "PhysicalEssence",
"type": "object"
},
"PhysicalEssenceOrOccurrent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Either a physical or processual entity.",
"title": "PhysicalEssenceOrOccurrent",
"type": "object"
},
"PhysiologicalProcess": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -49372,7 +49372,7 @@
"type": "object"
},
"PlanetaryEntity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any entity or process that exists at the level of the whole planet",
"properties": {
"category": {
@@ -49483,7 +49483,7 @@
"type": "object"
},
"Plant": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -49611,7 +49611,7 @@
"type": "object"
},
"Polypeptide": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A polypeptide is a molecular entity characterized by availability in protein databases of amino-acid-based sequence representations of its precise primary structure; for convenience of representation, partial sequences of various kinds are included, even if they do not represent a physical molecule.",
"properties": {
"category": {
@@ -49739,7 +49739,7 @@
"type": "object"
},
"PopulationOfIndividualOrganisms": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A collection of individuals from the same taxonomic class distinguished by one or more characteristics. Characteristics can include, but are not limited to, shared geographic location, genetics, phenotypes.",
"properties": {
"category": {
@@ -49867,7 +49867,7 @@
"type": "object"
},
"PopulationToPopulationAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a two populations",
"properties": {
"adjusted_p_value": {
@@ -50238,7 +50238,7 @@
"type": "object"
},
"PosttranslationalModification": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A chemical modification of a polypeptide or protein that occurs after translation. e.g. polypeptide cleavage to form separate proteins, methylation or acetylation of histone tail amino acids, protein ubiquitination.",
"properties": {
"category": {
@@ -50366,7 +50366,7 @@
"type": "object"
},
"PredicateMapping": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A deprecated predicate mapping object contains the deprecated predicate and an example of the rewiring that should be done to use a qualified statement in its place.",
"properties": {
"anatomical_context_qualifier": {
@@ -50580,7 +50580,7 @@
"type": "object"
},
"PreprintPublication": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a document reresenting an early version of an author's original scholarly work, such as a research paper or a review, prior to formal peer review and publication in a peer-reviewed scholarly or scientific journal.",
"properties": {
"authors": {
@@ -50772,7 +50772,7 @@
"type": "object"
},
"Procedure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A series of actions conducted in a certain order or manner",
"properties": {
"category": {
@@ -50883,7 +50883,7 @@
"type": "object"
},
"ProcessRegulatesProcessAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Describes a regulatory relationship between two genes or gene products.",
"properties": {
"adjusted_p_value": {
@@ -51257,7 +51257,7 @@
"type": "object"
},
"ProcessedMaterial": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A chemical entity (often a mixture) processed for consumption for nutritional, medical or technical use. Is a material entity that is created or changed during material processing.",
"properties": {
"available_from": {
@@ -51448,7 +51448,7 @@
"type": "object"
},
"Protein": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A gene product that is composed of a chain of amino acid sequences and is produced by ribosome-mediated translation of mRNA",
"properties": {
"category": {
@@ -51576,7 +51576,7 @@
"type": "object"
},
"ProteinDomain": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A conserved part of protein sequence and (tertiary) structure that can evolve, function, and exist independently of the rest of the protein chain. Protein domains maintain their structure and function independently of the proteins in which they are found. e.g. an SH3 domain.",
"properties": {
"category": {
@@ -51714,7 +51714,7 @@
"type": "object"
},
"ProteinFamily": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -51852,7 +51852,7 @@
"type": "object"
},
"ProteinIsoform": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Represents a protein that is a specific isoform of the canonical or reference protein. See https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4114032/",
"properties": {
"category": {
@@ -51980,7 +51980,7 @@
"type": "object"
},
"Publication": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Any \u2018published\u2019 piece of information. Publications are considered broadly to include any document or document part made available in print or on the web - which may include scientific journal issues, individual articles, and books - as well as things like pre-prints, white papers, patents, drug labels, web pages, protocol documents, and even a part of a publication if of significant knowledge scope (e.g. a figure, figure legend, or section highlighted by NLP).",
"properties": {
"authors": {
@@ -52172,7 +52172,7 @@
"type": "object"
},
"QuantityValue": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A value of an attribute that is quantitative and measurable, expressed as a combination of a unit and a numeric value",
"properties": {
"has_numeric_value": {
@@ -52194,7 +52194,7 @@
"type": "object"
},
"RNAProduct": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -52322,7 +52322,7 @@
"type": "object"
},
"RNAProductIsoform": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Represents a protein that is a specific isoform of the canonical or reference RNA",
"properties": {
"category": {
@@ -52470,7 +52470,7 @@
"type": "string"
},
"ReactionToCatalystAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -52870,7 +52870,7 @@
"type": "object"
},
"ReactionToParticipantAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -53270,7 +53270,7 @@
"type": "object"
},
"ReagentTargetedGene": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A gene altered in its expression level in the context of some experiment as a result of being targeted by gene-knockdown reagent(s) such as a morpholino or RNAi.",
"properties": {
"category": {
@@ -53405,7 +53405,7 @@
"type": "object"
},
"RegulatoryRegion": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A region (or regions) of the genome that contains known or putative regulatory elements that act in cis- or trans- to affect the transcription of gene",
"properties": {
"category": {
@@ -53540,13 +53540,13 @@
"type": "object"
},
"RelationshipQuantifier": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"title": "RelationshipQuantifier",
"type": "object"
},
"RelationshipType": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An OWL property used as an edge label",
"properties": {
"id": {
@@ -53561,7 +53561,7 @@
"type": "object"
},
"RelativeFrequencyAnalysisResult": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A result of a relative frequency analysis.",
"properties": {
"category": {
@@ -53722,7 +53722,7 @@
"type": "string"
},
"RetrievalSource": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Provides information about how a particular InformationResource served as a source from which knowledge expressed in an Edge, or data used to generate this knowledge, was retrieved.",
"properties": {
"category": {
@@ -53876,13 +53876,13 @@
"type": "object"
},
"SensitivityQuantifier": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"title": "SensitivityQuantifier",
"type": "object"
},
"SequenceAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a sequence feature and a nucleic acid entity it is localized to.",
"properties": {
"adjusted_p_value": {
@@ -54262,7 +54262,7 @@
"type": "string"
},
"SequenceFeatureRelationship": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "For example, a particular exon is part of a particular transcript or gene",
"properties": {
"adjusted_p_value": {
@@ -54633,7 +54633,7 @@
"type": "object"
},
"SequenceVariant": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A sequence_variant is a non exact copy of a sequence_feature or genome exhibiting one or more sequence_alteration.",
"properties": {
"category": {
@@ -54782,7 +54782,7 @@
"type": "object"
},
"SequenceVariantModulatesTreatmentAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a sequence variant and a treatment or health intervention. The treatment object itself encompasses both the disease and the drug used.",
"properties": {
"adjusted_p_value": {
@@ -55153,7 +55153,7 @@
"type": "object"
},
"Serial": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "This class may rarely be instantiated except if use cases of a given knowledge graph support its utility.",
"properties": {
"authors": {
@@ -55367,7 +55367,7 @@
"type": "object"
},
"SeverityValue": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "describes the severity of a phenotypic feature or disease",
"properties": {
"category": {
@@ -55500,7 +55500,7 @@
"type": "object"
},
"SiRNA": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A small RNA molecule that is the product of a longer exogenous or endogenous dsRNA, which is either a bimolecular duplex or very long hairpin, processed (via the Dicer pathway) such that numerous siRNAs accumulate from both strands of the dsRNA. SRNAs trigger the cleavage of their target molecules.",
"properties": {
"category": {
@@ -55628,7 +55628,7 @@
"type": "object"
},
"SmallMolecule": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A small molecule entity is a molecular entity characterized by availability in small-molecule databases of SMILES, InChI, IUPAC, or other unambiguous representation of its precise chemical structure; for convenience of representation, any valid chemical representation is included, even if it is not strictly molecular (e.g., sodium ion).",
"properties": {
"available_from": {
@@ -55787,7 +55787,7 @@
"type": "object"
},
"Snv": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "SNVs are single nucleotide positions in genomic DNA at which different sequence alternatives exist",
"properties": {
"category": {
@@ -55936,7 +55936,7 @@
"type": "object"
},
"SocioeconomicAttribute": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Attributes relating to a socioeconomic manifestation",
"properties": {
"category": {
@@ -56069,7 +56069,7 @@
"type": "object"
},
"SocioeconomicExposure": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A socioeconomic exposure is a factor relating to social and financial status of an affected individual (e.g. poverty).",
"properties": {
"category": {
@@ -56208,13 +56208,13 @@
"type": "object"
},
"SocioeconomicOutcome": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An general social or economic outcome, such as healthcare costs, utilization, etc., resulting from an exposure event",
"title": "SocioeconomicOutcome",
"type": "object"
},
"SpecificityQuantifier": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"title": "SpecificityQuantifier",
"type": "object"
@@ -56231,7 +56231,7 @@
"type": "string"
},
"Study": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a detailed investigation and/or analysis",
"properties": {
"category": {
@@ -56342,7 +56342,7 @@
"type": "object"
},
"StudyPopulation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A group of people banded together or treated as a group as participants in a research study.",
"properties": {
"category": {
@@ -56470,7 +56470,7 @@
"type": "object"
},
"StudyResult": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A collection of data items from a study that are about a particular study subject or experimental unit (the 'focus' of the Result) - optionally with context/provenance metadata that may be relevant to the interpretation of this data as evidence.",
"properties": {
"category": {
@@ -56607,7 +56607,7 @@
"type": "object"
},
"StudyVariable": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a variable that is used as a measure in the investigation of a study",
"properties": {
"category": {
@@ -56744,13 +56744,13 @@
"type": "object"
},
"SubjectOfInvestigation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An entity that has the role of being studied in an investigation, study, or experiment",
"title": "SubjectOfInvestigation",
"type": "object"
},
"TaxonToTaxonAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -57121,7 +57121,7 @@
"type": "object"
},
"TaxonomicRank": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A descriptor for the rank within a taxonomic classification. Example instance: TAXRANK:0000017 (kingdom)",
"properties": {
"id": {
@@ -57136,7 +57136,7 @@
"type": "object"
},
"TextMiningResult": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A result of text mining.",
"properties": {
"category": {
@@ -57273,7 +57273,7 @@
"type": "object"
},
"ThingWithTaxon": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A mixin that can be used on any entity that can be taxonomically classified. This includes individual organisms; genes, their products and other molecular entities; body parts; biological processes",
"properties": {
"in_taxon": {
@@ -57298,7 +57298,7 @@
"type": "object"
},
"Transcript": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An RNA synthesized on a DNA or RNA template by an RNA polymerase.",
"properties": {
"category": {
@@ -57426,7 +57426,7 @@
"type": "object"
},
"TranscriptToGeneRelationship": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A gene is a collection of transcripts",
"properties": {
"adjusted_p_value": {
@@ -57797,7 +57797,7 @@
"type": "object"
},
"TranscriptionFactorBindingSite": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A region (or regions) of the genome that contains a region of DNA known or predicted to bind a protein that modulates gene transcription",
"properties": {
"category": {
@@ -57932,7 +57932,7 @@
"type": "object"
},
"Treatment": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A treatment is targeted at a disease or phenotype and may involve multiple drug 'exposures', medical devices and/or procedures",
"properties": {
"category": {
@@ -58081,7 +58081,7 @@
"type": "object"
},
"VariantAsAModelOfDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -58534,7 +58534,7 @@
"type": "object"
},
"VariantToDiseaseAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -58987,7 +58987,7 @@
"type": "object"
},
"VariantToEntityAssociationMixin": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"object": {
@@ -59016,7 +59016,7 @@
"type": "object"
},
"VariantToGeneAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a variant and a gene, where the variant has a genetic association with the gene (i.e. is in linkage disequilibrium)",
"properties": {
"adjusted_p_value": {
@@ -59396,7 +59396,7 @@
"type": "object"
},
"VariantToGeneExpressionAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a variant and expression of a gene (i.e. e-QTL)",
"properties": {
"adjusted_p_value": {
@@ -59817,7 +59817,7 @@
"type": "object"
},
"VariantToPhenotypicFeatureAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"adjusted_p_value": {
@@ -60329,7 +60329,7 @@
"type": "object"
},
"VariantToPopulationAssociation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An association between a variant and a population, where the variant has particular frequency in the population",
"properties": {
"adjusted_p_value": {
@@ -60750,7 +60750,7 @@
"type": "object"
},
"Vertebrate": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A sub-phylum of animals consisting of those having a bony or cartilaginous vertebral column.",
"properties": {
"category": {
@@ -60878,7 +60878,7 @@
"type": "object"
},
"Virus": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A virus is a microorganism that replicates itself as a microRNA and infects the host cell.",
"properties": {
"category": {
@@ -61006,7 +61006,7 @@
"type": "object"
},
"WebPage": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a document that is published according to World Wide Web standards, which may incorporate text, graphics, sound, and/or other features.",
"properties": {
"authors": {
@@ -61198,7 +61198,7 @@
"type": "object"
},
"Zygosity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"category": {
@@ -61333,7 +61333,7 @@
},
"$id": "https://w3id.org/biolink/biolink-model",
"$schema": "https://json-schema.org/draft/2019-09/schema",
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A collection of deprecated mappings.",
"metamodel_version": "1.11.0",
"properties": {
diff --git a/tests/linkml/test_generators/input/openapi/spec-head.openapi.yaml b/tests/linkml/test_generators/input/openapi/spec-head-v30.openapi.yaml
similarity index 100%
rename from tests/linkml/test_generators/input/openapi/spec-head.openapi.yaml
rename to tests/linkml/test_generators/input/openapi/spec-head-v30.openapi.yaml
diff --git a/tests/linkml/test_generators/input/openapi/spec-head-v31.openapi.yaml b/tests/linkml/test_generators/input/openapi/spec-head-v31.openapi.yaml
new file mode 100644
index 0000000000..4846d45d71
--- /dev/null
+++ b/tests/linkml/test_generators/input/openapi/spec-head-v31.openapi.yaml
@@ -0,0 +1,58 @@
+openapi: 3.1.0
+info:
+ title: LinkML tests
+ version: 1.0.0
+servers:
+ - url: https://example.org/
+security:
+ - PayloadSignature: []
+paths:
+ /api/endpoint1:
+ post:
+ security:
+ - PayloadSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/MedicalEvent"
+ responses:
+ "200":
+ description: Success
+ get:
+ security:
+ - PayloadSignature: []
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/MarriageEvent"
+ /api/endpoint2:
+ get:
+ security:
+ - PayloadSignature: []
+ responses:
+ "200":
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Person"
+
+components:
+ schemas:
+ MedicalEvent:
+ type: object
+ x-linkml-schema: https://w3id.org/linkml/tests/kitchen_sink
+ x-linkml-source: MedicalEvent
+ MarriageEvent:
+ type: object
+ x-linkml-schema: https://w3id.org/linkml/tests/kitchen_sink
+ x-linkml-source: MarriageEvent
+ Person:
+ type: object
+ x-linkml-schema: https://w3id.org/linkml/tests/kitchen_sink
+ x-linkml-source: Person
diff --git a/tests/linkml/test_generators/test_jsonschemagen.py b/tests/linkml/test_generators/test_jsonschemagen.py
index 058042ec94..b22dcc7ae3 100644
--- a/tests/linkml/test_generators/test_jsonschemagen.py
+++ b/tests/linkml/test_generators/test_jsonschemagen.py
@@ -1672,3 +1672,116 @@ def test_generate_array_error_complex_unbounded_shape(array_error_complex_unboun
_ = JsonSchemaGenerator(
array_error_complex_unbounded,
).generate()
+
+
+_EXTRA_SLOTS_SCHEMA = """
+id: https://example.org/extra-slots-default
+name: extra_slots_default
+prefixes:
+ linkml: https://w3id.org/linkml/
+default_range: string
+imports:
+ - linkml:types
+classes:
+ Closed:
+ slots:
+ - name
+ ExplicitlyOpen:
+ extra_slots:
+ allowed: true
+ slots:
+ - name
+slots:
+ name:
+ range: string
+"""
+
+
+def _generate(tmp_path, schema_text, **kwargs):
+ schema_path = tmp_path / "schema.yaml"
+ schema_path.write_text(schema_text)
+ return json.loads(JsonSchemaGenerator(str(schema_path), **kwargs).serialize())
+
+
+@pytest.mark.jsonschemagen
+@pytest.mark.parametrize(
+ "kwargs,expected",
+ [
+ pytest.param({}, False, id="default-is-closed"),
+ pytest.param({"not_closed": False}, False, id="closed"),
+ pytest.param({"not_closed": True}, True, id="not-closed"),
+ ],
+)
+def test_extra_slots_absent_defaults_to_closed(tmp_path, kwargs, expected):
+ """A class with no ``extra_slots`` is closed unless ``not_closed`` says otherwise.
+
+ ``meta.yaml`` documents an absent ``extra_slots`` as "forbid all additional data
+ (default)", so the generator must not silently open such classes.
+ """
+ schema = _generate(tmp_path, _EXTRA_SLOTS_SCHEMA, **kwargs)
+
+ assert schema["$defs"]["Closed"]["additionalProperties"] is expected
+ # An explicit `extra_slots.allowed` always wins, whatever `not_closed` says.
+ assert schema["$defs"]["ExplicitlyOpen"]["additionalProperties"] is True
+
+
+@pytest.mark.jsonschemagen
+@pytest.mark.parametrize("kwargs", [{}, {"not_closed": False}, {"not_closed": True}])
+def test_rootless_schema_keeps_an_open_top_level(tmp_path, kwargs):
+ """With no root class the top level has no properties, so it must stay open.
+
+ Closing it would produce a schema admitting nothing but ``{}``. ``not_closed``
+ governs classes, not the top level.
+ """
+ schema = _generate(tmp_path, _EXTRA_SLOTS_SCHEMA, **kwargs)
+
+ assert schema["additionalProperties"] is True
+ jsonschema.validate({"name": "alice"}, schema)
+
+
+@pytest.mark.jsonschemagen
+@pytest.mark.parametrize("root_via", ["tree_root", "top_class"])
+def test_root_class_governs_the_top_level(tmp_path, root_via):
+ """The root class sets the top-level ``additionalProperties``.
+
+ Previously only ``--top-class`` did this and ``tree_root: true`` did not, so the
+ two disagreed about the same document (linkml#3608).
+ """
+ schema_text = _EXTRA_SLOTS_SCHEMA
+ kwargs = {}
+ if root_via == "tree_root":
+ schema_text = schema_text.replace(" Closed:\n", " Closed:\n tree_root: true\n")
+ else:
+ kwargs["top_class"] = "Closed"
+
+ schema = _generate(tmp_path, schema_text, **kwargs)
+
+ assert schema["additionalProperties"] is False
+ assert schema["$defs"]["Closed"]["additionalProperties"] is False
+
+
+@pytest.mark.jsonschemagen
+def test_multiple_tree_roots_pick_one_consistently(tmp_path):
+ """With more than one ``tree_root``, the top level comes from a single class.
+
+ biolink-model declares two. The top-level ``additionalProperties`` and the
+ subschema merged beneath it must not come from different classes.
+ """
+ schema_text = _EXTRA_SLOTS_SCHEMA.replace(" Closed:\n", " Closed:\n tree_root: true\n").replace(
+ " ExplicitlyOpen:\n", " ExplicitlyOpen:\n tree_root: true\n"
+ )
+ schema = _generate(tmp_path, schema_text)
+
+ # `Closed` is first, so it is the root: closed, and its properties are merged up.
+ assert schema["additionalProperties"] is False
+ assert set(schema["properties"]) == set(schema["$defs"]["Closed"]["properties"])
+
+
+@pytest.mark.jsonschemagen
+def test_top_class_matches_regardless_of_case(tmp_path):
+ """``top_class`` is habitually passed in CamelCase for a spelled-out class name."""
+ schema_text = _EXTRA_SLOTS_SCHEMA.replace(" Closed:\n", " closed thing:\n")
+ schema = _generate(tmp_path, schema_text, top_class="ClosedThing")
+
+ assert schema["additionalProperties"] is False
+ assert "name" in schema["properties"]
diff --git a/tests/linkml/test_generators/test_linkmlgen.py b/tests/linkml/test_generators/test_linkmlgen.py
index 52529752f5..941688ce0d 100644
--- a/tests/linkml/test_generators/test_linkmlgen.py
+++ b/tests/linkml/test_generators/test_linkmlgen.py
@@ -1,3 +1,4 @@
+import pytest
import yaml
from click.testing import CliRunner
@@ -27,6 +28,27 @@ def test_linkmlgen_prefixes():
assert "equipment_schema" in parsed["prefixes"]
+def test_schemaview_generator_namespaces_access_warns(kitchen_sink_path):
+ """Accessing self.namespaces on a uses_schemaloader=False generator must
+ emit a UserWarning and still return a usable Namespaces object.
+
+ self.namespaces is a SchemaLoader-era artifact. SchemaView-based generators
+ should use self.schemaview.namespaces() directly. The warning flags any
+ hybrid use so that it can be migrated to the correct API.
+ """
+ gen = LinkmlGenerator(kitchen_sink_path, format="yaml")
+ assert not gen.uses_schemaloader
+
+ with pytest.warns(UserWarning, match="self.namespaces.*SchemaLoader-era"):
+ ns = gen.namespaces
+
+ # The returned value must still be a functional Namespaces object
+ # forwarded from self.schemaview.namespaces() so that existing callers
+ # continue to work while being migrated.
+ assert ns is not None
+ assert ns == gen.schemaview.namespaces()
+
+
def test_generate(kitchen_sink_path):
sv = SchemaView(kitchen_sink_path)
assert "activity" in sv.all_classes(imports=True)
diff --git a/tests/linkml/test_generators/test_openapigen.py b/tests/linkml/test_generators/test_openapigen.py
index 9642a7447d..ad76c091e5 100644
--- a/tests/linkml/test_generators/test_openapigen.py
+++ b/tests/linkml/test_generators/test_openapigen.py
@@ -3,7 +3,7 @@
import pytest
import yaml
-from openapi_spec_validator import OpenAPIV30SpecValidator, validate
+from openapi_spec_validator import OpenAPIV30SpecValidator, OpenAPIV31SpecValidator, validate
from referencing.exceptions import PointerToNowhere
from linkml.generators.openapigen import OpenApiGenerator
@@ -14,6 +14,19 @@
# Reusable YAML fragments
# ---------------------------------------------------------------------------
+# OpenAPI versions the test-suite is driven with, mapped to the validator class from
+# openapi-spec-validator used to check the generated specs. The generator selects the
+# generation path from the template's top-level ``openapi`` attribute, so a version is
+# exercised simply by advertising it in the template. Extend this dict together with
+# the generator when a new OpenAPI version becomes supported.
+OAS_VALIDATORS: dict[str, type] = {
+ "3.0.3": OpenAPIV30SpecValidator,
+ "3.1.0": OpenAPIV31SpecValidator,
+}
+
+# Default OpenAPI version used by templates/tests that are not version-parametrized.
+DEFAULT_OAS_VERSION = "3.0.3"
+
# LinkML schema document preamble shared by the inline enum/chain test schemas.
LINKML_HEADER = """\
id: https://w3id.org/linkml/tests/{name}
@@ -25,7 +38,7 @@
# OpenAPI template header (title + quoted version) shared by most small templates.
OPENAPI_HEADER = """\
-openapi: 3.0.3
+openapi: {oas_version}
info:
title: {title}
version: '1.0.0'
@@ -202,6 +215,7 @@ def openapi_template(
header: str = "",
comment: str = "",
components: str = "",
+ oas_version: str = DEFAULT_OAS_VERSION,
) -> str:
"""Compose an OpenAPI template from the shared header, endpoints and schemas.
@@ -211,8 +225,9 @@ def openapi_template(
:param header: optional extra header block (e.g. servers/security)
:param comment: optional leading comment line(s)
:param components: extra ``components`` sections before ``schemas`` (e.g. responses)
+ :param oas_version: the OpenAPI version the template advertises
"""
- doc = dedent(OPENAPI_HEADER).format(title=title)
+ doc = dedent(OPENAPI_HEADER).format(title=title, oas_version=oas_version)
if header:
doc += dedent(header) + "\n"
doc += "paths:\n"
@@ -238,6 +253,7 @@ def single_endpoint_template(
header: str = "",
comment: str = "",
secure: bool = False,
+ oas_version: str = DEFAULT_OAS_VERSION,
) -> str:
"""Compose a template with one GET endpoint referencing one generated schema.
@@ -250,10 +266,13 @@ def single_endpoint_template(
:param header: optional extra header block (e.g. servers/security)
:param comment: optional leading comment line(s)
:param secure: add per-endpoint ``security``
+ :param oas_version: the OpenAPI version the template advertises
"""
endpoint = get_endpoint(path_name, schema_name, description=description, secure=secure)
schemas = schema_stub(schema_name, schema_id, source)
- return openapi_template(title, endpoints=endpoint, schemas=schemas, header=header, comment=comment)
+ return openapi_template(
+ title, endpoints=endpoint, schemas=schemas, header=header, comment=comment, oas_version=oas_version
+ )
# ---------------------------------------------------------------------------
@@ -320,177 +339,265 @@ def single_endpoint_template(
# Inline OpenAPI templates
# ---------------------------------------------------------------------------
-TEMPLATE_ENDPOINT_ENUM = single_endpoint_template(
- "Endpoint Enum Test",
- "/fixed-enum",
- "FixedEnum",
- schema_id=ENDPOINT_ENUM_ID,
- source="FixedEnum",
- description="ok",
-)
-TEMPLATE_ENUM_SLOT_DESCRIPTION = single_endpoint_template(
- "Enum Slot Description Test",
- "/foo",
- "Foo",
- schema_id=ENUM_SLOT_DESCRIPTION_ID,
- source="Foo",
- description="ok",
-)
+def template_endpoint_enum(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose endpoint references an enum defined in the LinkML schema.
-TEMPLATE_EXAMPLES = single_endpoint_template(
- "LinkML examples test",
- "/api/with-examples",
- "WithExamples",
- schema_id=TYPES_AND_ENUMS_ID,
- source="WithExamples",
- header=TEMPLATE_SERVERS_SECURITY,
- comment="# OpenAPI template referring a class whose slot declares multiple examples",
-)
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return single_endpoint_template(
+ "Endpoint Enum Test",
+ "/fixed-enum",
+ "FixedEnum",
+ schema_id=ENDPOINT_ENUM_ID,
+ source="FixedEnum",
+ description="ok",
+ oas_version=oas_version,
+ )
-TEMPLATE_FIXED = openapi_template(
- "LinkML tests",
- endpoints=POST_FIXED,
- schemas="",
- header=TEMPLATE_SERVERS_SECURITY,
- comment="# OpenAPI template provided as template that is fully fixed\n# because there are no fields to be replaced",
-)
-TEMPLATE_KEEP_SCOPED = single_endpoint_template(
- "Keep Unreferenced Scoped Test",
- "/api/foo",
- "Foo",
- schema_id=UNREFERENCED_WITH_UNRELATED_ID,
- source="Foo",
-)
+def template_enum_slot_description(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose referenced class has a slot-level enum description.
-TEMPLATE_LOWERCASE_CLASS = single_endpoint_template(
- "LinkML tests",
- "/api/dataset",
- "Dataset",
- schema_id=KITCHEN_SINK_ID,
- source="Dataset",
-)
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return single_endpoint_template(
+ "Enum Slot Description Test",
+ "/foo",
+ "Foo",
+ schema_id=ENUM_SLOT_DESCRIPTION_ID,
+ source="Foo",
+ description="ok",
+ oas_version=oas_version,
+ )
-TEMPLATE_MISSING_XLINKML_SOURCE = """\
-openapi: 3.0.3
-info: {title: Foo API, version: "1.0"}
-paths:
- /foo:
- get:
- responses:
- '200':
- description: ok
- content:
- application/json:
- schema: {$ref: '#/components/schemas/Foo'}
-components:
- schemas:
-""" + "".join(
- [
- schema_stub("Foo", FOO_ID, "Foo"),
- """\
+
+def template_examples(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose referenced class declares multiple slot examples.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return single_endpoint_template(
+ "LinkML examples test",
+ "/api/with-examples",
+ "WithExamples",
+ schema_id=TYPES_AND_ENUMS_ID,
+ source="WithExamples",
+ header=TEMPLATE_SERVERS_SECURITY,
+ comment="# OpenAPI template referring a class whose slot declares multiple examples",
+ oas_version=oas_version,
+ )
+
+
+def template_fixed(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a fully fixed template with no replaceable fields.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "LinkML tests",
+ endpoints=POST_FIXED,
+ schemas="",
+ header=TEMPLATE_SERVERS_SECURITY,
+ comment="# OpenAPI template provided as template that is fully fixed\n"
+ + "# because there are no fields to be replaced",
+ oas_version=oas_version,
+ )
+
+
+def template_keep_scoped(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose single schema keeps ``keep_unreferenced`` scoped.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return single_endpoint_template(
+ "Keep Unreferenced Scoped Test",
+ "/api/foo",
+ "Foo",
+ schema_id=UNREFERENCED_WITH_UNRELATED_ID,
+ source="Foo",
+ oas_version=oas_version,
+ )
+
+
+def template_lowercase_class(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose endpoint references a lowercase-named LinkML class.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return single_endpoint_template(
+ "LinkML tests",
+ "/api/dataset",
+ "Dataset",
+ schema_id=KITCHEN_SINK_ID,
+ source="Dataset",
+ oas_version=oas_version,
+ )
+
+
+def template_renamed_type(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template exposing a LinkML type under a different OpenAPI resource name.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return single_endpoint_template(
+ "Renamed Type Test",
+ "/fixed",
+ "Fixed",
+ schema_id=TYPES_AND_ENUMS_ID,
+ source="FixedType",
+ description="ok",
+ oas_version=oas_version,
+ )
+
+
+def template_renaming(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template exposing a LinkML class under a different OpenAPI resource name."""
+ return single_endpoint_template(
+ "LinkML tests - renaming",
+ "/api/persons",
+ "PersonResource",
+ schema_id=KITCHEN_SINK_ID,
+ source="Person",
+ header=TEMPLATE_SERVERS_SECURITY,
+ oas_version=oas_version,
+ )
+
+
+def template_missing_xlinkml_source(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose second schema stub omits the ``x-linkml-source`` key.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "Foo API",
+ endpoints=get_endpoint("/foo", "Foo", description="ok"),
+ schemas=""
+ + "".join(
+ [
+ schema_stub("Foo", FOO_ID, "Foo"),
+ """\
Bar:
type: object
x-linkml-schema: https://example.org/foo
""",
- ]
-)
+ ]
+ ),
+ oas_version=oas_version,
+ )
-TEMPLATE_RENAMED_TYPE = single_endpoint_template(
- "Renamed Type Test",
- "/fixed",
- "Fixed",
- schema_id=TYPES_AND_ENUMS_ID,
- source="FixedType",
- description="ok",
-)
-TEMPLATE_RENAMING = single_endpoint_template(
- "LinkML tests - renaming",
- "/api/persons",
- "PersonResource",
- schema_id=KITCHEN_SINK_ID,
- source="Person",
- header=TEMPLATE_SERVERS_SECURITY,
-)
+def template_shared_responses(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template referencing a reusable ``components/responses`` entry.
-TEMPLATE_SHARED_RESPONSES = openapi_template(
- "Shared Responses Test",
- endpoints=get_endpoint(
- "/foo",
- "Person",
- description="ok",
- responses=""" '404':
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "Shared Responses Test",
+ endpoints=get_endpoint(
+ "/foo",
+ "Person",
+ description="ok",
+ responses=""" '404':
$ref: '#/components/responses/NotFound'
""",
- ),
- schemas=schema_stub("Person", KITCHEN_SINK_ID, "Person"),
- components=""" responses:
+ ),
+ schemas=schema_stub("Person", KITCHEN_SINK_ID, "Person"),
+ components=""" responses:
NotFound:
description: not found
""",
-)
+ oas_version=oas_version,
+ )
-TEMPLATE_REFERENCED_PARAMETER = openapi_template(
- "t",
- endpoints=get_endpoint(
- "/foo",
- "Foo",
- description="ok",
- extra=""" parameters:
+
+def template_referenced_parameter(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose endpoint parameter is given as a ``$ref``.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "t",
+ endpoints=get_endpoint(
+ "/foo",
+ "Foo",
+ description="ok",
+ extra=""" parameters:
- $ref: '#/components/parameters/Limit'
""",
- ),
- schemas=schema_stub("Foo", FOO_ID, "Foo"),
- components=""" parameters:
+ ),
+ schemas=schema_stub("Foo", FOO_ID, "Foo"),
+ components=""" parameters:
Limit:
name: limit
in: query
schema:
type: integer
""",
-)
+ oas_version=oas_version,
+ )
-TEMPLATE_TYPES = single_endpoint_template(
- "LinkML type constraints test",
- "/api/code",
- "CodeStringRef",
- schema_id=TYPES_AND_ENUMS_ID,
- source="CodeString",
- comment="# OpenAPI template referring a Type defined in the LinkML schema",
-)
-TEMPLATE_TYPES_ENUMS = single_endpoint_template(
- "Types and Enums Test",
- "/api/fixed",
- "FixedType",
- schema_id=TYPES_AND_ENUMS_ID,
- source="FixedType",
- header=TEMPLATE_SERVERS_SECURITY,
-)
+def template_types(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose endpoint references a LinkML Type (constraints inlined)."""
+ return single_endpoint_template(
+ "LinkML type constraints test",
+ "/api/code",
+ "CodeStringRef",
+ schema_id=TYPES_AND_ENUMS_ID,
+ source="CodeString",
+ comment="# OpenAPI template referring a Type defined in the LinkML schema",
+ oas_version=oas_version,
+ )
-TEMPLATE_WRONG_SCHEMA_ID = single_endpoint_template(
- "LinkML tests - wrong schema id",
- "/api/endpoint1",
- "Person",
- schema_id=WRONG_SCHEMA_ID,
- source="Person",
- header=TEMPLATE_SERVERS_SECURITY,
-)
-TEMPLATE_COMMENTS = openapi_template(
- "Comment Preservation Test",
- endpoints=get_endpoint(
- "/api/person",
+def template_types_enums(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose endpoint references a LinkML type.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return single_endpoint_template(
+ "Types and Enums Test",
+ "/api/fixed",
+ "FixedType",
+ schema_id=TYPES_AND_ENUMS_ID,
+ source="FixedType",
+ header=TEMPLATE_SERVERS_SECURITY,
+ oas_version=oas_version,
+ )
+
+
+def template_wrong_schema_id(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose schema declares a mismatched ``x-linkml-schema`` id."""
+ return single_endpoint_template(
+ "LinkML tests - wrong schema id",
+ "/api/endpoint1",
"Person",
- secure=True,
- comment="# this endpoint comment must survive",
- ),
- schemas=schema_stub("Person", KITCHEN_SINK_ID, "Person"),
- header=TEMPLATE_SERVERS_SECURITY,
- comment="# top-level comment must survive round-trip",
-)
+ schema_id=WRONG_SCHEMA_ID,
+ source="Person",
+ header=TEMPLATE_SERVERS_SECURITY,
+ oas_version=oas_version,
+ )
+
+
+def template_comments(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template carrying comments on the header and an endpoint.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "Comment Preservation Test",
+ endpoints=get_endpoint(
+ "/api/person",
+ "Person",
+ secure=True,
+ comment="# this endpoint comment must survive",
+ ),
+ schemas=schema_stub("Person", KITCHEN_SINK_ID, "Person"),
+ header=TEMPLATE_SERVERS_SECURITY,
+ comment="# top-level comment must survive round-trip",
+ oas_version=oas_version,
+ )
# ---------------------------------------------------------------------------
@@ -510,72 +617,107 @@ def single_endpoint_template(
header=TEMPLATE_SERVERS_SECURITY,
)
-TEMPLATE_KEEP_UNREFERENCED = openapi_template(
- "Keep Unreferenced Test",
- endpoints=get_endpoint("/api/person", "Person", secure=True),
- schemas=schema_stubs(
- [
- ("Person", KITCHEN_SINK_ID, "Person"),
- ("OpaqueEvent", KITCHEN_SINK_ID, "MarriageEvent"),
- ]
- ),
- header=TEMPLATE_SERVERS_SECURITY,
-)
-TEMPLATE_SHARED_ENUM = openapi_template(
- "Shared Enum Test",
- endpoints=get_endpoint("/foo", "Foo", description="ok") + get_endpoint("/bar", "Bar", description="ok"),
- schemas=schema_stubs(
- [
- ("Foo", SHARED_ENUM_ID, "Foo"),
- ("Bar", SHARED_ENUM_ID, "Bar"),
- ]
- ),
-)
+def template_keep_unreferenced(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose second schema is not referenced by any endpoint.
-TEMPLATE_DANGLING_REF = openapi_template(
- "Dangling Reference Test",
- endpoints=get_endpoint("/api/person", "Person", secure=True) + get_endpoint("/api/foo", "Foo", secure=True),
- schemas=schema_stubs(
- [
- ("Person", KITCHEN_SINK_ID, "Person"),
- ("Foo", KITCHEN_SINK_ID, "NonExistentClass"),
- ]
- ),
- header=TEMPLATE_SERVERS_SECURITY,
-)
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "Keep Unreferenced Test",
+ endpoints=get_endpoint("/api/person", "Person", secure=True),
+ schemas=schema_stubs(
+ [
+ ("Person", KITCHEN_SINK_ID, "Person"),
+ ("OpaqueEvent", KITCHEN_SINK_ID, "MarriageEvent"),
+ ]
+ ),
+ header=TEMPLATE_SERVERS_SECURITY,
+ oas_version=oas_version,
+ )
-TEMPLATE_DANGLING_REFS_MULTIPLE = openapi_template(
- "Multiple Dangling References Test",
- endpoints=(
- get_endpoint("/api/person", "Person", secure=True)
- + get_endpoint("/api/foo", "Foo", secure=True)
- + get_endpoint("/api/bar", "Bar", secure=True)
- ),
- schemas=schema_stubs(
- [
- ("Person", KITCHEN_SINK_ID, "Person"),
- ("Foo", KITCHEN_SINK_ID, "NonExistentClassFoo"),
- ("Bar", KITCHEN_SINK_ID, "NonExistentClassBar"),
- ]
- ),
- header=TEMPLATE_SERVERS_SECURITY,
-)
-TEMPLATE_HEAD = openapi_template(
- "LinkML tests",
- endpoints=POST_MEDICAL_EVENT
- + get_endpoint("/api/person", "Person", secure=True)
- + get_endpoint("/api/endpoint2", "MarriageEvent", secure=True),
- schemas=schema_stubs(
- [
- ("MedicalEvent", KITCHEN_SINK_ID, "MedicalEvent"),
- ("Person", KITCHEN_SINK_ID, "Person"),
- ("MarriageEvent", KITCHEN_SINK_ID, "MarriageEvent"),
- ]
- ),
- header=TEMPLATE_SERVERS_SECURITY,
-)
+def template_shared_enum(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template whose two endpoints reference classes sharing an enum.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "Shared Enum Test",
+ endpoints=get_endpoint("/foo", "Foo", description="ok") + get_endpoint("/bar", "Bar", description="ok"),
+ schemas=schema_stubs(
+ [
+ ("Foo", SHARED_ENUM_ID, "Foo"),
+ ("Bar", SHARED_ENUM_ID, "Bar"),
+ ]
+ ),
+ oas_version=oas_version,
+ )
+
+
+def template_dangling_ref(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template with one schema sourced from a non-existent LinkML class.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "Dangling Reference Test",
+ endpoints=get_endpoint("/api/person", "Person", secure=True) + get_endpoint("/api/foo", "Foo", secure=True),
+ schemas=schema_stubs(
+ [
+ ("Person", KITCHEN_SINK_ID, "Person"),
+ ("Foo", KITCHEN_SINK_ID, "NonExistentClass"),
+ ]
+ ),
+ header=TEMPLATE_SERVERS_SECURITY,
+ oas_version=oas_version,
+ )
+
+
+def template_dangling_refs_multiple(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose a template with several schemas sourced from non-existent LinkML classes.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "Multiple Dangling References Test",
+ endpoints=(
+ get_endpoint("/api/person", "Person", secure=True)
+ + get_endpoint("/api/foo", "Foo", secure=True)
+ + get_endpoint("/api/bar", "Bar", secure=True)
+ ),
+ schemas=schema_stubs(
+ [
+ ("Person", KITCHEN_SINK_ID, "Person"),
+ ("Foo", KITCHEN_SINK_ID, "NonExistentClassFoo"),
+ ("Bar", KITCHEN_SINK_ID, "NonExistentClassBar"),
+ ]
+ ),
+ header=TEMPLATE_SERVERS_SECURITY,
+ oas_version=oas_version,
+ )
+
+
+def template_head(oas_version: str = DEFAULT_OAS_VERSION) -> str:
+ """Compose the kitchen_sink template with one post and two get endpoints.
+
+ :param oas_version: the OpenAPI version the template advertises
+ """
+ return openapi_template(
+ "LinkML tests",
+ endpoints=POST_MEDICAL_EVENT
+ + get_endpoint("/api/person", "Person", secure=True)
+ + get_endpoint("/api/endpoint2", "MarriageEvent", secure=True),
+ schemas=schema_stubs(
+ [
+ ("MedicalEvent", KITCHEN_SINK_ID, "MedicalEvent"),
+ ("Person", KITCHEN_SINK_ID, "Person"),
+ ("MarriageEvent", KITCHEN_SINK_ID, "MarriageEvent"),
+ ]
+ ),
+ header=TEMPLATE_SERVERS_SECURITY,
+ oas_version=oas_version,
+ )
# ---------------------------------------------------------------------------
@@ -600,21 +742,40 @@ def gen_openapi_spec(head_path, kitchen_sink_path):
return openapigen.serialize(head_path)
+def assert_fixed_value(schema: dict, expected, oas_version: str) -> None:
+ """Assert a fixed-value schema exposes ``expected`` via the const/enum keyword of its version.
+
+ The v3.0.3 JsonSchema path emits a fixed LinkML type as a single-item ``enum`` while
+ the v3.1.0 Pydantic path keeps JSON Schema's native ``const``.
+ """
+ if oas_version == "3.0.3":
+ assert schema["enum"] == [expected]
+ assert "const" not in schema
+ else:
+ assert schema["const"] == expected
+ assert "enum" not in schema
+
+
+@pytest.fixture(params=list(OAS_VALIDATORS))
+def oas_version(request):
+ """The OpenAPI version under test; drives the version-parametrized fixtures/tests."""
+ return request.param
+
+
@pytest.fixture
-def openapi_spec(tmp_path, kitchen_sink_path):
- head_path = write_template(tmp_path, TEMPLATE_HEAD)
- openapigen = OpenApiGenerator(kitchen_sink_path)
- return yaml.safe_load(openapigen.serialize(head_path))
+def openapi_spec(tmp_path, kitchen_sink_path, oas_version):
+ head_path = write_template(tmp_path, template_head(oas_version=oas_version))
+ return yaml.safe_load(gen_openapi_spec(head_path, kitchen_sink_path))
-def test_openapi(tmp_path, kitchen_sink_path):
+def test_openapi(tmp_path, kitchen_sink_path, oas_version):
"""Test if generation succeeds without failure and returns valid YAML."""
- head_path = write_template(tmp_path, TEMPLATE_HEAD)
+ head_path = write_template(tmp_path, template_head(oas_version=oas_version))
openapi_spec = gen_openapi_spec(head_path, kitchen_sink_path)
# ensure that valid YAML has been generated
assert yaml.safe_load(openapi_spec)
# ensure that valid OpenAPI spec has been generated
- assert validate(yaml.safe_load(openapi_spec), cls=OpenAPIV30SpecValidator) is None
+ assert validate(yaml.safe_load(openapi_spec), cls=OAS_VALIDATORS[oas_version]) is None
def test_openapi_missing_template(kitchen_sink_path):
@@ -623,9 +784,9 @@ def test_openapi_missing_template(kitchen_sink_path):
OpenApiGenerator(kitchen_sink_path).serialize()
-def test_openapi_fixed_template(tmp_path, kitchen_sink_path):
+def test_openapi_fixed_template(tmp_path, kitchen_sink_path, oas_version):
"""Test that a template with no replaceable fields is emitted byte-for-byte."""
- head_path = write_template(tmp_path, TEMPLATE_FIXED)
+ head_path = write_template(tmp_path, template_fixed(oas_version=oas_version))
oa_spec = OpenApiGenerator(kitchen_sink_path).serialize(head_path)
assert Path(head_path).read_text() == oa_spec
@@ -636,13 +797,29 @@ def test_openapi_spec_no_defs_references(openapi_spec):
assert "#/$defs/" not in str(schema)
-def test_openapi_spec_const_to_enum_conversion(openapi_spec):
- """Test that const values are converted to single-item enum arrays."""
+def test_openapi_spec_const_conversion(openapi_spec, oas_version):
+ """Test const handling per version: enum arrays on 3.0.3, preserved const on 3.1.0."""
person = openapi_spec["components"]["schemas"]["Person"]
- assert person["properties"]["species_name"]["enum"] == ["human"]
- assert person["properties"]["stomach_count"]["enum"] == [1]
- assert "const" not in person["properties"]["species_name"]
- assert "const" not in person["properties"]["stomach_count"]
+ species_name = person["properties"]["species_name"]
+ stomach_count = person["properties"]["stomach_count"]
+ if oas_version == "3.0.3":
+ # OpenAPI 3.0 has no ``const``; the generator rewrites it to a single-item ``enum``
+ assert species_name["enum"] == ["human"]
+ assert stomach_count["enum"] == [1]
+ assert "const" not in species_name
+ assert "const" not in stomach_count
+ else:
+ # OpenAPI 3.1 is aligned with JSON Schema 2020-12, so ``const`` is kept as-is
+ assert "const" in str(species_name)
+ assert "const" in str(stomach_count)
+
+
+def test_openapi_v31_no_linkml_meta(tmp_path, kitchen_sink_path):
+ """Test that the v3.1.0 Pydantic path strips ``linkml_meta`` annotations from schemas."""
+ head_path = write_template(tmp_path, template_head(oas_version="3.1.0"))
+ spec = yaml.safe_load(gen_openapi_spec(head_path, kitchen_sink_path))
+ for schema in spec["components"]["schemas"].values():
+ assert "linkml_meta" not in str(schema)
def test_openapi_spec_class_level_title_stripped(openapi_spec):
@@ -652,13 +829,17 @@ def test_openapi_spec_class_level_title_stripped(openapi_spec):
assert person["properties"]["age_in_years"]["description"] == "number of years since birth"
-def test_openapi_spec_nullable_type_conversion(openapi_spec):
- """Test that nullable type arrays are converted to anyOf."""
+def test_openapi_spec_nullable_type_conversion(openapi_spec, oas_version):
+ """Test nullable handling per version: anyOf on 3.0.3, native type arrays on 3.1.0."""
emp_event = openapi_spec["components"]["schemas"]["EmploymentEvent"]
- assert "anyOf" in emp_event["properties"]["type"]
- assert "type" not in emp_event["properties"]["type"] or not isinstance(
- emp_event["properties"]["type"]["type"], list
- )
+ type_prop = emp_event["properties"]["type"]
+ if oas_version == "3.0.3":
+ # OpenAPI 3.0 forbids type arrays; nullable ``["x", "null"]`` becomes ``anyOf``
+ assert "anyOf" in type_prop
+ assert "type" not in type_prop or not isinstance(type_prop["type"], list)
+ else:
+ # OpenAPI 3.1 permits nullable type arrays and ``anyOf`` alike; either is valid
+ assert "anyOf" in type_prop or isinstance(type_prop.get("type"), list)
def test_openapi_spec_schemas_are_extensible(openapi_spec):
@@ -666,12 +847,23 @@ def test_openapi_spec_schemas_are_extensible(openapi_spec):
APIs are typically extended backwards-compatibly by adding new objects or new
attributes to existing objects. Closed schemas (additionalProperties: false) block
- that, so the generated OpenAPI schemas must stay open.
+ that, so the generated OpenAPI schemas must stay open -- at every nesting level,
+ including inlined sub-schemas (relevant for the v3.1.0 Pydantic path, which must be
+ driven with ``extra_fields="allow"``).
"""
- for name, schema in openapi_spec["components"]["schemas"].items():
- assert schema.get("additionalProperties") is not False, (
- f"schema '{name}' is closed (additionalProperties: false), blocking API extension"
- )
+
+ def _closed_paths(obj, path=""):
+ if isinstance(obj, dict):
+ if obj.get("additionalProperties") is False:
+ yield path or ""
+ for key, value in obj.items():
+ yield from _closed_paths(value, f"{path}/{key}")
+ elif isinstance(obj, list):
+ for i, item in enumerate(obj):
+ yield from _closed_paths(item, f"{path}[{i}]")
+
+ closed = list(_closed_paths(openapi_spec["components"]["schemas"]))
+ assert not closed, f"closed schemas (additionalProperties: false) block API extension: {closed}"
def test_resources_presence_and_absence(openapi_spec):
@@ -688,33 +880,33 @@ def test_printout_template(kitchen_sink_path):
"""Test that printout_template returns a valid YAML generic template."""
output = OpenApiGenerator(kitchen_sink_path).printout_template()
parsed = yaml.safe_load(output)
- assert parsed["openapi"] == "3.0.3"
+ assert parsed["openapi"] == "x.y.z"
assert "paths" in parsed
assert "schemas" in parsed["components"]
# the schema id from kitchen_sink must appear in the template
assert "https://w3id.org/linkml/tests/kitchen_sink" in output
-def test_schema_id_mismatch_raises(tmp_path, kitchen_sink_path):
+def test_schema_id_mismatch_raises(tmp_path, kitchen_sink_path, oas_version):
"""Test that a mismatched x-linkml-schema raises ValueError with a descriptive message."""
- head_path = write_template(tmp_path, TEMPLATE_WRONG_SCHEMA_ID)
+ head_path = write_template(tmp_path, template_wrong_schema_id(oas_version=oas_version))
with pytest.raises(ValueError, match="x-linkml-schema"):
OpenApiGenerator(kitchen_sink_path).serialize(head_path)
-def test_missing_x_linkml_source_raises(input_path, tmp_path):
+def test_missing_x_linkml_source_raises(input_path, tmp_path, oas_version):
"""Test that a template schema missing x-linkml-source raises a descriptive KeyError.
x-linkml-schema presence/value are validated nicely, but x-linkml-source was
skipped, surfacing as a bare ``KeyError: 'x-linkml-source'`` during instantiation.
"""
schema_path = input_path("openapi/schema_foo.yaml")
- head_path = write_template(tmp_path, TEMPLATE_MISSING_XLINKML_SOURCE)
+ head_path = write_template(tmp_path, template_missing_xlinkml_source(oas_version=oas_version))
with pytest.raises(KeyError, match="Bar.*missing required 'x-linkml-source'"):
OpenApiGenerator(schema_path, keep_unreferenced=True).serialize(head_path)
-def test_referenced_parameter_does_not_crash(input_path, tmp_path):
+def test_referenced_parameter_does_not_crash(input_path, tmp_path, oas_version):
"""Test that a template parameter given as a $ref does not raise KeyError.
A parameter entry of the form ``{$ref: '#/components/parameters/Limit'}`` has no
@@ -722,13 +914,13 @@ def test_referenced_parameter_does_not_crash(input_path, tmp_path):
reading ``param_spec["schema"]`` unconditionally crashed before generation.
"""
schema_path = input_path("openapi/schema_foo.yaml")
- head_path = write_template(tmp_path, TEMPLATE_REFERENCED_PARAMETER)
+ head_path = write_template(tmp_path, template_referenced_parameter(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(schema_path).serialize(head_path))
# the reusable parameter survives untouched
assert spec["paths"]["/foo"]["get"]["parameters"] == [{"$ref": "#/components/parameters/Limit"}]
# and the endpoint schema is generated as usual
assert "Foo" in spec["components"]["schemas"]
- assert validate(spec, cls=OpenAPIV30SpecValidator) is None
+ assert validate(spec, cls=OAS_VALIDATORS[oas_version]) is None
def test_missing_schema_declaration_raises(tmp_path, kitchen_sink_path):
@@ -752,25 +944,30 @@ def test_missing_schema_declaration_raises(tmp_path, kitchen_sink_path):
OpenApiGenerator(kitchen_sink_path).serialize(str(template))
-def test_openapi_type_constraints(input_path, tmp_path):
- """Test that LinkML types with constraints (e.g., pattern) are properly generated in the spec."""
+def test_openapi_type_constraints(input_path, tmp_path, oas_version):
+ """Test that a LinkML type (constraints inlined) still yields a standalone component schema.
+
+ On both the v3.0.3 (JsonSchema) and v3.1.0 (Pydantic) paths, LinkML types are not
+ emitted as classes; an endpoint referencing a type directly (via x-linkml-source)
+ must still produce a component schema, otherwise the spec has a dangling ``$ref``.
+ """
schema_path = input_path("openapi/schema_types_and_enums.yaml")
- head_path = write_template(tmp_path, TEMPLATE_TYPES)
+ head_path = write_template(tmp_path, template_types(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(schema_path).serialize(head_path))
schemas = spec["components"]["schemas"]
- # the type schema is exposed under the template's resource name
+ # the type schema is exposed under the template's resource name, not dangling
code_str = schemas["CodeStringRef"]
assert code_str["type"] == "string"
assert code_str["pattern"] == "^[A-Z]{2,10}$"
assert code_str["description"] == "A 2-10 character uppercase code"
- assert validate(spec, cls=OpenAPIV30SpecValidator) is None
+ assert validate(spec, cls=OAS_VALIDATORS[oas_version]) is None
for schema in schemas.values():
assert "#/$defs/" not in str(schema)
-def test_renaming(tmp_path, kitchen_sink_path):
+def test_renaming(tmp_path, kitchen_sink_path, oas_version):
"""Test that resource names differing from LinkML class names are renamed throughout the spec."""
- head_path = write_template(tmp_path, TEMPLATE_RENAMING)
+ head_path = write_template(tmp_path, template_renaming(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(kitchen_sink_path).serialize(head_path))
schemas = spec["components"]["schemas"]
# resource is exposed under the template name, not the LinkML class name
@@ -788,7 +985,7 @@ def test_openapi_examples_converted_to_singular_example(input_path, tmp_path):
slot declaring ``examples`` -- this is a blocker, not a cosmetic gap.
"""
schema_path = input_path("openapi/schema_types_and_enums.yaml")
- head_path = write_template(tmp_path, TEMPLATE_EXAMPLES)
+ head_path = write_template(tmp_path, template_examples())
spec = yaml.safe_load(OpenApiGenerator(schema_path).serialize(head_path))
name_schema = spec["components"]["schemas"]["WithExamples"]["properties"]["name"]
# first example is kept; the plural form is gone entirely
@@ -797,14 +994,14 @@ def test_openapi_examples_converted_to_singular_example(input_path, tmp_path):
assert validate(spec, cls=OpenAPIV30SpecValidator) is None
-def test_template_text_preserved(tmp_path, kitchen_sink_path):
+def test_template_text_preserved(tmp_path, kitchen_sink_path, oas_version):
"""Test that everything above ``components/schemas`` is emitted verbatim.
The generator no longer YAML round-trips the whole template (which would drop
comments and normalise quoting/styling). Only the ``components/schemas`` section
is regenerated; the header, paths and any comments above it must survive intact.
"""
- head_path = write_template(tmp_path, TEMPLATE_COMMENTS)
+ head_path = write_template(tmp_path, template_comments(oas_version=oas_version))
result = OpenApiGenerator(kitchen_sink_path).serialize(head_path)
# comments are dropped by a YAML round-trip but preserved by text handling
assert "# top-level comment must survive round-trip" in result
@@ -818,9 +1015,9 @@ def test_template_text_preserved(tmp_path, kitchen_sink_path):
assert result.startswith(prefix)
-def test_unreferenced_schema_removed_by_default(tmp_path, kitchen_sink_path):
+def test_unreferenced_schema_removed_by_default(tmp_path, kitchen_sink_path, oas_version):
"""Test that template schemas not referenced by any endpoint are removed by default."""
- head_path = write_template(tmp_path, TEMPLATE_KEEP_UNREFERENCED)
+ head_path = write_template(tmp_path, template_keep_unreferenced(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(kitchen_sink_path).serialize(head_path))
schemas = spec["components"]["schemas"]
assert "Person" in schemas
@@ -828,19 +1025,21 @@ def test_unreferenced_schema_removed_by_default(tmp_path, kitchen_sink_path):
assert "MarriageEvent" not in schemas
-def test_keep_unreferenced_preserves_template_schema(tmp_path, kitchen_sink_path):
+def test_keep_unreferenced_preserves_template_schema(tmp_path, kitchen_sink_path, oas_version):
"""Test that keep_unreferenced retains template schemas not referenced by any endpoint.
Unreferenced sub-schemas can convey objects that are opaque to the API but relevant
to clients (e.g. present in provided artifacts). The keep_unreferenced flag makes
their removal switchable.
"""
- head_path = write_template(tmp_path, TEMPLATE_KEEP_UNREFERENCED)
+ head_path = write_template(tmp_path, template_keep_unreferenced(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(kitchen_sink_path, keep_unreferenced=True).serialize(head_path))
schemas = spec["components"]["schemas"]
assert "Person" in schemas
- # OpaqueEvent(OpenAPI)/MarriageEvent(LinkML) is kept even though no endpoint references it
- assert "MarriageEvent" in schemas
+ # OpaqueEvent(OpenAPI)/MarriageEvent(LinkML) is kept even though no endpoint references it,
+ # and is exposed under its OpenAPI resource name, not the LinkML class name
+ assert "OpaqueEvent" in schemas
+ assert "MarriageEvent" not in schemas
def test_unreferenced_chain_pruned_by_default(tmp_path):
@@ -880,7 +1079,7 @@ def test_keep_unreferenced_pulls_transitive_chain(tmp_path):
assert "Baz Qux" in schemas
-def test_keep_unreferenced_does_not_add_unrelated_schemas(tmp_path):
+def test_keep_unreferenced_does_not_add_unrelated_schemas(tmp_path, oas_version):
"""Test that keep_unreferenced stays scoped to the template, not "dump everything".
The chain schema also contains classes not reachable from ``Foo`` or the template
@@ -891,7 +1090,7 @@ def test_keep_unreferenced_does_not_add_unrelated_schemas(tmp_path):
every class.
"""
schema_path = load_schema(SCHEMA_UNREFERENCED_WITH_UNRELATED)
- head_path = write_template(tmp_path, TEMPLATE_KEEP_SCOPED)
+ head_path = write_template(tmp_path, template_keep_scoped(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(schema_path, keep_unreferenced=True).serialize(head_path))
schemas = spec["components"]["schemas"]
assert "Foo" in schemas
@@ -911,13 +1110,13 @@ def test_enums_as_separate_schemas_by_default(openapi_spec):
assert {"$ref": "#/components/schemas/EmploymentEventType"} in type_schema["anyOf"]
-def test_inline_enums_inlines_enum_schemas(tmp_path, kitchen_sink_path):
+def test_inline_enums_inlines_enum_schemas(tmp_path, kitchen_sink_path, oas_version):
"""Test that inline_enums inlines enum sub-schemas into their parents.
With the flag set, an enum no longer gets its own ``components/schemas`` entry;
instead its definition is inlined where it was referenced.
"""
- head_path = write_template(tmp_path, TEMPLATE_HEAD)
+ head_path = write_template(tmp_path, template_head(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(kitchen_sink_path, inline_enums=True).serialize(head_path))
schemas = spec["components"]["schemas"]
# the enum no longer has a standalone schema entry
@@ -930,7 +1129,7 @@ def test_inline_enums_inlines_enum_schemas(tmp_path, kitchen_sink_path):
assert "EmploymentEventType" not in str(spec)
-def test_inline_enums_does_not_inline_types(input_path, tmp_path):
+def test_inline_enums_does_not_inline_types(input_path, tmp_path, oas_version):
"""Test that inline_enums does not mistake fixed-value LinkML types for enums.
A type with ``equals_string`` becomes a single-element ``enum`` after the
@@ -939,27 +1138,27 @@ def test_inline_enums_does_not_inline_types(input_path, tmp_path):
types must keep their named schema entry even when inlining is enabled.
"""
schema_path = input_path("openapi/schema_types_and_enums.yaml")
- head_path = write_template(tmp_path, TEMPLATE_TYPES_ENUMS)
+ head_path = write_template(tmp_path, template_types_enums(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(schema_path, inline_enums=True).serialize(head_path))
schemas = spec["components"]["schemas"]
# the fixed-value type keeps its own named schema entry (not inlined)
assert "FixedType" in schemas
- assert schemas["FixedType"]["enum"] == ["fixed-value"]
+ assert_fixed_value(schemas["FixedType"], "fixed-value", oas_version)
assert schemas["FixedType"]["type"] == "string"
-def test_inline_enums_disabled_keeps_types_and_enums_separate(input_path, tmp_path):
+def test_inline_enums_disabled_keeps_types_and_enums_separate(input_path, tmp_path, oas_version):
"""Test that with inline_enums disabled both types and enums keep separate schema entries."""
schema_path = input_path("openapi/schema_types_and_enums.yaml")
- head_path = write_template(tmp_path, TEMPLATE_TYPES_ENUMS)
+ head_path = write_template(tmp_path, template_types_enums(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(schema_path, inline_enums=False).serialize(head_path))
schemas = spec["components"]["schemas"]
assert "FixedType" in schemas
- assert schemas["FixedType"]["enum"] == ["fixed-value"]
+ assert_fixed_value(schemas["FixedType"], "fixed-value", oas_version)
assert schemas["FixedType"]["type"] == "string"
-def test_inline_enums_keeps_endpoint_referenced_enum(tmp_path):
+def test_inline_enums_keeps_endpoint_referenced_enum(tmp_path, oas_version):
"""Test that inline_enums does not inline an enum referenced directly by an endpoint.
Inlining removes the enum's standalone ``components/schemas`` entry, which would
@@ -967,7 +1166,7 @@ def test_inline_enums_keeps_endpoint_referenced_enum(tmp_path):
endpoint must therefore keep its entry even when inlining is enabled.
"""
schema_path = load_schema(SCHEMA_ENDPOINT_ENUM)
- head_path = write_template(tmp_path, TEMPLATE_ENDPOINT_ENUM)
+ head_path = write_template(tmp_path, template_endpoint_enum(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(schema_path, inline_enums=True).serialize(head_path))
schemas = spec["components"]["schemas"]
assert "FixedEnum" in schemas
@@ -977,7 +1176,7 @@ def test_inline_enums_keeps_endpoint_referenced_enum(tmp_path):
}
-def test_inline_enums_does_not_inline_renamed_enums(input_path, tmp_path):
+def test_inline_enums_does_not_inline_renamed_enums(input_path, tmp_path, oas_version):
"""Test that inlining a renamed enum does not bypass the type guard.
When the endpoint refers to a LinkML ``enum`` under a different OpenAPI name, the
@@ -985,17 +1184,17 @@ def test_inline_enums_does_not_inline_renamed_enums(input_path, tmp_path):
would be inlined away, leaving the endpoint's ``$ref`` dangling.
"""
schema_path = input_path("openapi/schema_types_and_enums.yaml")
- head_path = write_template(tmp_path, TEMPLATE_RENAMED_TYPE)
+ head_path = write_template(tmp_path, template_renamed_type(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(schema_path, inline_enums=True).serialize(head_path))
schemas = spec["components"]["schemas"]
assert "Fixed" in schemas
- assert schemas["Fixed"]["enum"] == ["fixed-value"]
+ assert_fixed_value(schemas["Fixed"], "fixed-value", oas_version)
assert spec["paths"]["/fixed"]["get"]["responses"]["200"]["content"]["application/json"]["schema"] == {
"$ref": "#/components/schemas/Fixed"
}
-def test_inline_enums_shared_enum_no_yaml_anchors(tmp_path):
+def test_inline_enums_shared_enum_no_yaml_anchors(tmp_path, oas_version):
"""Test that inlining a shared enum does not emit YAML anchors.
When the same enum is referenced from two classes, the inlined copy must not be the
@@ -1004,7 +1203,7 @@ def test_inline_enums_shared_enum_no_yaml_anchors(tmp_path):
contain no anchors or aliases.
"""
schema_path = load_schema(SCHEMA_SHARED_ENUM)
- head_path = write_template(tmp_path, TEMPLATE_SHARED_ENUM)
+ head_path = write_template(tmp_path, template_shared_enum(oas_version=oas_version))
result = OpenApiGenerator(schema_path, inline_enums=True).serialize(head_path)
spec = yaml.safe_load(result)
color_foo = spec["components"]["schemas"]["Foo"]["properties"]["color"]
@@ -1019,7 +1218,7 @@ def test_inline_enums_shared_enum_no_yaml_anchors(tmp_path):
assert color_foo is not color_bar
-def test_inline_enums_preserves_slot_description(tmp_path):
+def test_inline_enums_preserves_slot_description(tmp_path, oas_version):
"""Test that inlining an enum keeps the slot-level description of the referencing property.
A property that references an enum carries its own ``description`` next to the
@@ -1028,7 +1227,7 @@ def test_inline_enums_preserves_slot_description(tmp_path):
must not eclipse it).
"""
schema_path = load_schema(SCHEMA_ENUM_SLOT_DESCRIPTION)
- head_path = write_template(tmp_path, TEMPLATE_ENUM_SLOT_DESCRIPTION)
+ head_path = write_template(tmp_path, template_enum_slot_description(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(schema_path, inline_enums=True).serialize(head_path))
color = spec["components"]["schemas"]["Foo"]["properties"]["color"]
assert color["enum"] == ["FOO", "BAR"]
@@ -1054,7 +1253,7 @@ def _refs(obj):
assert ref.removeprefix("#/components/schemas/") in schema_names
-def test_lowercase_class_name_preserved(tmp_path, kitchen_sink_path):
+def test_lowercase_class_name_preserved(tmp_path, kitchen_sink_path, oas_version):
"""Test that a lowercase LinkML class name is preserved, not camelCased, in the spec.
``JsonSchemaGenerator`` camelCases ``$defs`` keys unless ``preserve_names=True``.
@@ -1063,7 +1262,7 @@ def test_lowercase_class_name_preserved(tmp_path, kitchen_sink_path):
is keyed ``Activity`` while the ``$ref`` from ``Dataset`` points to ``activity``,
yielding a missing schema and a dangling reference.
"""
- head_path = write_template(tmp_path, TEMPLATE_LOWERCASE_CLASS)
+ head_path = write_template(tmp_path, template_lowercase_class(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(kitchen_sink_path).serialize(head_path))
schemas = spec["components"]["schemas"]
# the LinkML name is preserved verbatim, not camelCased
@@ -1072,28 +1271,28 @@ def test_lowercase_class_name_preserved(tmp_path, kitchen_sink_path):
# Dataset references the activity schema under its original name
assert schemas["Dataset"]["properties"]["activities"]["items"] == {"$ref": "#/components/schemas/activity"}
# the produced spec is valid (no dangling reference)
- assert validate(spec, cls=OpenAPIV30SpecValidator) is None
+ assert validate(spec, cls=OAS_VALIDATORS[oas_version]) is None
-def test_dangling_reference_raises(tmp_path, kitchen_sink_path):
+def test_dangling_reference_raises(tmp_path, kitchen_sink_path, oas_version):
"""Test that a generated spec containing an unresolvable $ref is rejected.
The template declares a ``Foo`` schema sourced from a non-existent LinkML class,
so no schema is generated for it while an endpoint still references it. The
generator must detect the dangling ``$ref`` and fail loudly.
"""
- head_path = write_template(tmp_path, TEMPLATE_DANGLING_REF)
+ head_path = write_template(tmp_path, template_dangling_ref(oas_version=oas_version))
with pytest.raises(ValueError, match="Dangling .ref"):
OpenApiGenerator(kitchen_sink_path).serialize(head_path)
-def test_dangling_reference_reports_all(tmp_path, kitchen_sink_path):
+def test_dangling_reference_reports_all(tmp_path, kitchen_sink_path, oas_version):
"""All dangling ``$ref`` targets must be gathered and reported together, not just the first one.
The template declares two schemas (``Foo`` and ``Bar``) sourced from non-existent LinkML classes,
each referenced by its own endpoint. The single raised error must mention both.
"""
- head_path = write_template(tmp_path, TEMPLATE_DANGLING_REFS_MULTIPLE)
+ head_path = write_template(tmp_path, template_dangling_refs_multiple(oas_version=oas_version))
with pytest.raises(ValueError, match="Dangling .ref") as exc_info:
OpenApiGenerator(kitchen_sink_path).serialize(head_path)
message = str(exc_info.value)
@@ -1101,17 +1300,17 @@ def test_dangling_reference_reports_all(tmp_path, kitchen_sink_path):
assert "#/components/schemas/Bar" in message
-def test_refs_to_non_schema_components_allowed(tmp_path, kitchen_sink_path):
+def test_refs_to_non_schema_components_allowed(tmp_path, kitchen_sink_path, oas_version):
"""Test that $refs to reusable components other than schemas (e.g. responses) are allowed.
The dangling-reference check must resolve every internal ``$ref`` against its own
``components`` section rather than assuming all targets live under ``schemas``.
"""
- head_path = write_template(tmp_path, TEMPLATE_SHARED_RESPONSES)
+ head_path = write_template(tmp_path, template_shared_responses(oas_version=oas_version))
spec = yaml.safe_load(OpenApiGenerator(kitchen_sink_path).serialize(head_path))
# the reusable response survives and is still referenced by the endpoint
assert "NotFound" in spec["components"]["responses"]
assert spec["paths"]["/foo"]["get"]["responses"]["404"] == {"$ref": "#/components/responses/NotFound"}
# the schema is generated as usual
assert "Person" in spec["components"]["schemas"]
- assert validate(spec, cls=OpenAPIV30SpecValidator) is None
+ assert validate(spec, cls=OAS_VALIDATORS[oas_version]) is None
diff --git a/tests/linkml/test_generators/test_shaclgen.py b/tests/linkml/test_generators/test_shaclgen.py
index 8604f712de..f367de97e7 100644
--- a/tests/linkml/test_generators/test_shaclgen.py
+++ b/tests/linkml/test_generators/test_shaclgen.py
@@ -2784,3 +2784,52 @@ def test_exclusive_value_coexists_with_boolean_guard():
has_boolean = any("BOUND" in q for q in queries)
assert has_exclusive, "Expected one exclusive-value SPARQL constraint"
assert has_boolean, "Expected one boolean-guard SPARQL constraint"
+
+
+def test_shacl_modular_schema_with_reused_attribute_name(tmp_path) -> None:
+ """A modular schema imported by relative path generates SHACL (#3878).
+
+ Two classes reuse an attribute name with distinct slot_uris, and the schema declaring
+ them is imported as ``../nucleo/core``, so its closure key differs from its name.
+ """
+ nucleo = tmp_path / "nucleo"
+ dominios = tmp_path / "dominios"
+ nucleo.mkdir()
+ dominios.mkdir()
+ (nucleo / "core.yaml").write_text(
+ "id: https://example.org/core\n"
+ "name: core\n"
+ "prefixes: {linkml: 'https://w3id.org/linkml/', core: 'https://example.org/core/'}\n"
+ "default_prefix: core\n"
+ "default_range: string\n"
+ "imports: [linkml:types]\n"
+ "classes:\n"
+ " Transaccion:\n"
+ " attributes:\n"
+ " id_transaccion: {identifier: true}\n"
+ " estado: {slot_uri: core:transaccion_estado}\n"
+ " Compromiso:\n"
+ " attributes:\n"
+ " id_compromiso: {identifier: true}\n"
+ " estado: {slot_uri: core:compromiso_estado}\n"
+ )
+ domain = dominios / "domain.yaml"
+ domain.write_text(
+ "id: https://example.org/domain\n"
+ "name: domain\n"
+ "prefixes: {linkml: 'https://w3id.org/linkml/', core: 'https://example.org/core/', "
+ "dom: 'https://example.org/domain/'}\n"
+ "default_prefix: dom\n"
+ "default_range: string\n"
+ "imports: [linkml:types, ../nucleo/core]\n"
+ "classes:\n"
+ " Pedido:\n"
+ " is_a: Transaccion\n"
+ " attributes:\n"
+ " importe: {range: float}\n"
+ )
+
+ graph = rdflib.Graph()
+ graph.parse(data=ShaclGenerator(str(domain)).serialize(), format="turtle")
+ shapes = set(graph.subjects(RDF.type, SH.NodeShape))
+ assert URIRef("https://example.org/domain/Pedido") in shapes
diff --git a/tests/linkml/test_issues/__snapshots__/issue_120.json b/tests/linkml/test_issues/__snapshots__/issue_120.json
index 5f5c4d5f52..a0780fec2f 100644
--- a/tests/linkml/test_issues/__snapshots__/issue_120.json
+++ b/tests/linkml/test_issues/__snapshots__/issue_120.json
@@ -1,7 +1,7 @@
{
"$defs": {
"Course": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"name": {
@@ -12,7 +12,7 @@
"type": "object"
},
"Student": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"courses": {
diff --git a/tests/linkml/test_issues/__snapshots__/issue_177.json b/tests/linkml/test_issues/__snapshots__/issue_177.json
index d520ab4c1d..70cc2303ee 100644
--- a/tests/linkml/test_issues/__snapshots__/issue_177.json
+++ b/tests/linkml/test_issues/__snapshots__/issue_177.json
@@ -1,7 +1,7 @@
{
"$defs": {
"C1": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"sa": {
@@ -21,7 +21,7 @@
"type": "object"
},
"C2": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"sb": {
@@ -37,7 +37,7 @@
},
"$id": "http://example.org/tests/issue177",
"$schema": "https://json-schema.org/draft/2019-09/schema",
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"metamodel_version": "1.11.0",
"properties": {
diff --git a/tests/linkml/test_issues/__snapshots__/issue_202.json.schema b/tests/linkml/test_issues/__snapshots__/issue_202.json.schema
index 50644c1f8b..3ad99309c8 100644
--- a/tests/linkml/test_issues/__snapshots__/issue_202.json.schema
+++ b/tests/linkml/test_issues/__snapshots__/issue_202.json.schema
@@ -1,7 +1,7 @@
{
"$defs": {
"GeospatialDDCoordLocation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"latitude": {
diff --git a/tests/linkml/test_issues/__snapshots__/issue_239.json b/tests/linkml/test_issues/__snapshots__/issue_239.json
index 81ee092116..006ef21a37 100644
--- a/tests/linkml/test_issues/__snapshots__/issue_239.json
+++ b/tests/linkml/test_issues/__snapshots__/issue_239.json
@@ -1,7 +1,7 @@
{
"$defs": {
"C": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {},
diff --git a/tests/linkml/test_issues/__snapshots__/issue_2499.json b/tests/linkml/test_issues/__snapshots__/issue_2499.json
index 14afe9de30..196c4d7b54 100644
--- a/tests/linkml/test_issues/__snapshots__/issue_2499.json
+++ b/tests/linkml/test_issues/__snapshots__/issue_2499.json
@@ -1,7 +1,7 @@
{
"$defs": {
"Thing": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"identifier": {
@@ -18,7 +18,7 @@
},
"$id": "http://example.com/dataset",
"$schema": "https://json-schema.org/draft/2019-09/schema",
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"metamodel_version": "1.11.0",
"properties": {
diff --git a/tests/linkml/test_metamodel_compat/input/metamodel/meta.yaml b/tests/linkml/test_metamodel_compat/input/metamodel/meta.yaml
index 63dadbfd74..cbe98b801a 100644
--- a/tests/linkml/test_metamodel_compat/input/metamodel/meta.yaml
+++ b/tests/linkml/test_metamodel_compat/input/metamodel/meta.yaml
@@ -436,11 +436,13 @@ slots:
aliases:
- workflow status
domain: element
- range: uriorcurie
+ range: string
description: status of the element
- slot_uri: bibo:status
+ close_mappings:
+ - bibo:status
examples:
- - value: "bibo:draft"
+ - value: "testing"
+ - value: "unstable"
see_also:
- https://www.hl7.org/fhir/valueset-publication-status.html ## Draft, Active, Retired, Unknown
- https://www.hl7.org/fhir/versions.html#std-process ## Draft, Trial Use, Normative, Informative, Deprecated
diff --git a/tests/linkml/test_scripts/__snapshots__/genjsonschema/meta.json b/tests/linkml/test_scripts/__snapshots__/genjsonschema/meta.json
index 7879b587a7..9bf8ffdfee 100644
--- a/tests/linkml/test_scripts/__snapshots__/genjsonschema/meta.json
+++ b/tests/linkml/test_scripts/__snapshots__/genjsonschema/meta.json
@@ -1,7 +1,7 @@
{
"$defs": {
"Activity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a provence-generating activity",
"properties": {
"description": {
@@ -53,7 +53,7 @@
"type": "object"
},
"Address": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"altitude": {
@@ -79,7 +79,7 @@
"type": "object"
},
"Agent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a provence-generating agent",
"properties": {
"acted_on_behalf_of": {
@@ -117,7 +117,7 @@
]
},
"AnyOfClasses": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute2": {
@@ -135,7 +135,7 @@
"type": "object"
},
"AnyOfEnums": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute3": {
@@ -156,7 +156,7 @@
"type": "object"
},
"AnyOfMix": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute4": {
@@ -180,7 +180,7 @@
"type": "object"
},
"AnyOfSimpleType": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute1": {
@@ -201,7 +201,7 @@
"type": "object"
},
"BirthEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"ended_at_time": {
@@ -246,7 +246,7 @@
"type": "object"
},
"ClassWithSpaces": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"slot_with_space_1": {
@@ -260,7 +260,7 @@
"type": "object"
},
"CodeSystem": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {
@@ -290,7 +290,7 @@
"type": "object"
},
"Company": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"aliases": {
@@ -325,7 +325,7 @@
"type": "object"
},
"Concept": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {
@@ -361,7 +361,7 @@
"type": "string"
},
"Dataset": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"activities": {
@@ -416,7 +416,7 @@
"type": "object"
},
"DiagnosisConcept": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {
@@ -450,7 +450,7 @@
"type": "string"
},
"EmploymentEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"employed_at": {
@@ -519,7 +519,7 @@
"type": "string"
},
"EqualsString": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute5": {
@@ -534,7 +534,7 @@
"type": "object"
},
"EqualsStringIn": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute6": {
@@ -552,7 +552,7 @@
"type": "object"
},
"Event": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"ended_at_time": {
@@ -591,7 +591,7 @@
"type": "object"
},
"FakeClass": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"test_attribute": {
@@ -605,7 +605,7 @@
"type": "object"
},
"FamilialRelationship": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"cordialness": {
@@ -657,7 +657,7 @@
"type": "string"
},
"Friend": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"name": {
@@ -671,7 +671,7 @@
"type": "object"
},
"HasAliases": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"aliases": {
@@ -731,7 +731,7 @@
"type": "string"
},
"MarriageEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"ended_at_time": {
@@ -782,7 +782,7 @@
"type": "object"
},
"MedicalEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"diagnosis": {
@@ -847,7 +847,7 @@
"type": "object"
},
"Organization": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An organization.\n\nThis description\nincludes newlines\n\n## Markdown headers\n\n * and\n * a\n * list",
"properties": {
"aliases": {
@@ -884,7 +884,7 @@
"type": "string"
},
"Person": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A person, living or dead",
"properties": {
"addresses": {
@@ -994,7 +994,7 @@
"type": "object"
},
"Place": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"aliases": {
@@ -1023,7 +1023,7 @@
"type": "object"
},
"ProcedureConcept": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {
@@ -1049,7 +1049,7 @@
"type": "object"
},
"Relationship": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"cordialness": {
@@ -1093,7 +1093,7 @@
"type": "object"
},
"SubSubClass2": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"slot_with_space_1": {
@@ -1117,7 +1117,7 @@
"type": "object"
},
"SubclassTest": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"slot_with_space_1": {
@@ -1141,7 +1141,7 @@
"type": "object"
},
"TubSubClass1": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Same depth as Sub sub class 1",
"properties": {
"slot_with_space_1": {
@@ -1165,7 +1165,7 @@
"type": "object"
},
"WithLocation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"in_location": {
@@ -1181,7 +1181,7 @@
},
"$id": "https://w3id.org/linkml/tests/kitchen_sink",
"$schema": "https://json-schema.org/draft/2019-09/schema",
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"metamodel_version": "1.11.0",
"properties": {
diff --git a/tests/linkml/test_scripts/__snapshots__/genjsonschema/meta_inline.json b/tests/linkml/test_scripts/__snapshots__/genjsonschema/meta_inline.json
index 7879b587a7..9bf8ffdfee 100644
--- a/tests/linkml/test_scripts/__snapshots__/genjsonschema/meta_inline.json
+++ b/tests/linkml/test_scripts/__snapshots__/genjsonschema/meta_inline.json
@@ -1,7 +1,7 @@
{
"$defs": {
"Activity": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a provence-generating activity",
"properties": {
"description": {
@@ -53,7 +53,7 @@
"type": "object"
},
"Address": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"altitude": {
@@ -79,7 +79,7 @@
"type": "object"
},
"Agent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "a provence-generating agent",
"properties": {
"acted_on_behalf_of": {
@@ -117,7 +117,7 @@
]
},
"AnyOfClasses": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute2": {
@@ -135,7 +135,7 @@
"type": "object"
},
"AnyOfEnums": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute3": {
@@ -156,7 +156,7 @@
"type": "object"
},
"AnyOfMix": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute4": {
@@ -180,7 +180,7 @@
"type": "object"
},
"AnyOfSimpleType": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute1": {
@@ -201,7 +201,7 @@
"type": "object"
},
"BirthEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"ended_at_time": {
@@ -246,7 +246,7 @@
"type": "object"
},
"ClassWithSpaces": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"slot_with_space_1": {
@@ -260,7 +260,7 @@
"type": "object"
},
"CodeSystem": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {
@@ -290,7 +290,7 @@
"type": "object"
},
"Company": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"aliases": {
@@ -325,7 +325,7 @@
"type": "object"
},
"Concept": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {
@@ -361,7 +361,7 @@
"type": "string"
},
"Dataset": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"activities": {
@@ -416,7 +416,7 @@
"type": "object"
},
"DiagnosisConcept": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {
@@ -450,7 +450,7 @@
"type": "string"
},
"EmploymentEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"employed_at": {
@@ -519,7 +519,7 @@
"type": "string"
},
"EqualsString": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute5": {
@@ -534,7 +534,7 @@
"type": "object"
},
"EqualsStringIn": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"attribute6": {
@@ -552,7 +552,7 @@
"type": "object"
},
"Event": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"ended_at_time": {
@@ -591,7 +591,7 @@
"type": "object"
},
"FakeClass": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"test_attribute": {
@@ -605,7 +605,7 @@
"type": "object"
},
"FamilialRelationship": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"cordialness": {
@@ -657,7 +657,7 @@
"type": "string"
},
"Friend": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"name": {
@@ -671,7 +671,7 @@
"type": "object"
},
"HasAliases": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"aliases": {
@@ -731,7 +731,7 @@
"type": "string"
},
"MarriageEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"ended_at_time": {
@@ -782,7 +782,7 @@
"type": "object"
},
"MedicalEvent": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"diagnosis": {
@@ -847,7 +847,7 @@
"type": "object"
},
"Organization": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "An organization.\n\nThis description\nincludes newlines\n\n## Markdown headers\n\n * and\n * a\n * list",
"properties": {
"aliases": {
@@ -884,7 +884,7 @@
"type": "string"
},
"Person": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "A person, living or dead",
"properties": {
"addresses": {
@@ -994,7 +994,7 @@
"type": "object"
},
"Place": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"aliases": {
@@ -1023,7 +1023,7 @@
"type": "object"
},
"ProcedureConcept": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"id": {
@@ -1049,7 +1049,7 @@
"type": "object"
},
"Relationship": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"cordialness": {
@@ -1093,7 +1093,7 @@
"type": "object"
},
"SubSubClass2": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"slot_with_space_1": {
@@ -1117,7 +1117,7 @@
"type": "object"
},
"SubclassTest": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"slot_with_space_1": {
@@ -1141,7 +1141,7 @@
"type": "object"
},
"TubSubClass1": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "Same depth as Sub sub class 1",
"properties": {
"slot_with_space_1": {
@@ -1165,7 +1165,7 @@
"type": "object"
},
"WithLocation": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"properties": {
"in_location": {
@@ -1181,7 +1181,7 @@
},
"$id": "https://w3id.org/linkml/tests/kitchen_sink",
"$schema": "https://json-schema.org/draft/2019-09/schema",
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"metamodel_version": "1.11.0",
"properties": {
diff --git a/tests/linkml/test_scripts/__snapshots__/genjsonschema/roottest.json b/tests/linkml/test_scripts/__snapshots__/genjsonschema/roottest.json
index 72d4e72e39..76b928cea0 100644
--- a/tests/linkml/test_scripts/__snapshots__/genjsonschema/roottest.json
+++ b/tests/linkml/test_scripts/__snapshots__/genjsonschema/roottest.json
@@ -1,13 +1,13 @@
{
"$defs": {
"C1": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"title": "C1",
"type": "object"
},
"C2": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"title": "C2",
"type": "object"
@@ -15,7 +15,7 @@
},
"$id": "http://example.org/tests/issue177",
"$schema": "https://json-schema.org/draft/2019-09/schema",
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"metamodel_version": "1.11.0",
"title": "issue177",
diff --git a/tests/linkml/test_scripts/__snapshots__/genjsonschema/roottest2.json b/tests/linkml/test_scripts/__snapshots__/genjsonschema/roottest2.json
index 72d4e72e39..76b928cea0 100644
--- a/tests/linkml/test_scripts/__snapshots__/genjsonschema/roottest2.json
+++ b/tests/linkml/test_scripts/__snapshots__/genjsonschema/roottest2.json
@@ -1,13 +1,13 @@
{
"$defs": {
"C1": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"title": "C1",
"type": "object"
},
"C2": {
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"title": "C2",
"type": "object"
@@ -15,7 +15,7 @@
},
"$id": "http://example.org/tests/issue177",
"$schema": "https://json-schema.org/draft/2019-09/schema",
- "additionalProperties": true,
+ "additionalProperties": false,
"description": "",
"metamodel_version": "1.11.0",
"title": "issue177",
diff --git a/tests/linkml/test_scripts/test_diff_stable.py b/tests/linkml/test_scripts/test_diff_stable.py
new file mode 100644
index 0000000000..d1c292276b
--- /dev/null
+++ b/tests/linkml/test_scripts/test_diff_stable.py
@@ -0,0 +1,45 @@
+"""CLI checks for the optional diff-stable RDF labels."""
+
+import re
+from pathlib import Path
+
+import pyoxigraph as ox
+import pytest
+from click.testing import CliRunner
+
+from linkml.generators.shaclgen import cli as shacl_cli
+
+_SCHEMA = """id: https://example.org/cli
+name: cli
+prefixes:
+ linkml: https://w3id.org/linkml/
+ ex: https://example.org/cli/
+default_prefix: ex
+imports: [linkml:types]
+classes:
+ Thing:
+ attributes:
+ label:
+ range: string
+"""
+
+
+@pytest.mark.parametrize(
+ ("flag", "label_pattern"),
+ [
+ pytest.param("--no-diff-stable", r"c14n[0-9]+", id="disabled"),
+ pytest.param("--diff-stable", r"b[0-9a-f]{12}(?:_[1-9][0-9]*)?", id="enabled", marks=pytest.mark.diffable_rdf),
+ ],
+)
+def test_the_flag_reaches_the_serializer(tmp_path: Path, flag: str, label_pattern: str) -> None:
+ """The CLI flag selects the blank-node labelling it names."""
+ schema = tmp_path / "schema.yaml"
+ schema.write_text(_SCHEMA, encoding="utf-8")
+
+ result = CliRunner().invoke(shacl_cli, [str(schema), flag])
+
+ assert result.exit_code == 0, result.output
+ quads = list(ox.parse(result.stdout, format=ox.RdfFormat.TURTLE))
+ blank_nodes = {term for q in quads for term in (q.subject, q.object) if isinstance(term, ox.BlankNode)}
+ assert blank_nodes
+ assert all(re.fullmatch(label_pattern, node.value) for node in blank_nodes)
diff --git a/tests/linkml/test_scripts/test_gen_openapi.py b/tests/linkml/test_scripts/test_gen_openapi.py
index dce6f8b8bf..de001b3d72 100644
--- a/tests/linkml/test_scripts/test_gen_openapi.py
+++ b/tests/linkml/test_scripts/test_gen_openapi.py
@@ -1,26 +1,25 @@
+import pytest
from click.testing import CliRunner
from linkml.generators.openapigen import cli
from tests.conftest import KITCHEN_SINK_PATH
-OPENAPI_TEMPLATE_PATH = str(
- __import__("pathlib").Path(__file__).parent.parent
- / "test_generators"
- / "input"
- / "openapi"
- / "spec-head.openapi.yaml"
-)
+OPENAPI_TEMPLATE_PATH_PREFIX = str(__import__("pathlib").Path(__file__).parent.parent / "test_generators" / "input")
def test_help():
runner = CliRunner()
result = runner.invoke(cli, ["--help"])
- assert "Generate an OpenAPI v3.0.3 spec" in result.output
+ assert "Generate an OpenAPI YAML" in result.output
-def test_valid_call():
+@pytest.mark.parametrize(
+ "template_path",
+ ["openapi/spec-head-v30.openapi.yaml", "openapi/spec-head-v31.openapi.yaml"],
+)
+def test_valid_call(template_path):
runner = CliRunner()
- result = runner.invoke(cli, [KITCHEN_SINK_PATH, "--template", OPENAPI_TEMPLATE_PATH])
+ result = runner.invoke(cli, [KITCHEN_SINK_PATH, "--template", f"{OPENAPI_TEMPLATE_PATH_PREFIX}/{template_path}"])
assert result.exit_code == 0
assert "MarriageEvent" in result.output
assert "MedicalEvent" in result.output
@@ -31,5 +30,5 @@ def test_missing_template():
runner = CliRunner()
result = runner.invoke(cli, [KITCHEN_SINK_PATH], standalone_mode=False)
assert result.exit_code == 0
- assert "openapi: 3.0.3" in result.output
+ assert "openapi: x.y.z" in result.output
assert "x-linkml-schema:" in result.output
diff --git a/tests/linkml/test_utils/test_generator.py b/tests/linkml/test_utils/test_generator.py
index 48cd036029..94d96a1545 100644
--- a/tests/linkml/test_utils/test_generator.py
+++ b/tests/linkml/test_utils/test_generator.py
@@ -84,6 +84,11 @@ def visit_subset(self, subset: SubsetDefinition) -> None:
self.visited.append(f"subset: {subset.name}")
+@dataclass
+class SchemaViewGeneratorTest(GeneratorTest):
+ uses_schemaloader = False
+
+
# visit_all_class_slots = True, visits_are_sorted = False, sort_class_slots = False
expected1 = [
"init",
@@ -396,6 +401,85 @@ def test_default_prefix():
GeneratorTest(model + "\n\ndefault_prefix: CCCC")
+def test_schema_view_prefix_namespaces(tmp_path):
+ """SchemaView prefix objects are unwrapped when namespaces are initialized."""
+ schema_path = tmp_path / "schema.yaml"
+ schema_path.write_text(
+ """id: https://example.org/test
+name: test
+prefixes:
+ ex: https://example.org/test/
+default_prefix: ex
+classes:
+ Foo:
+"""
+ )
+
+ generator = SchemaViewGeneratorTest(schema_path)
+
+ assert str(generator.namespaces["ex"]) == "https://example.org/test/"
+
+
+def test_namespaces_constructor_kwarg():
+ """The ``namespaces=`` constructor kwarg is accepted for backward compat.
+
+ ``namespaces`` is exposed as a property backed by ``_namespaces``. Renaming
+ the backing field must not drop the public ``namespaces=`` kwarg that worked
+ before the SchemaLoader/SchemaView split.
+
+ - On the SchemaLoader path the kwarg is accepted (no ``TypeError``) and the
+ map is (re)populated from the resolved schema, as on ``main``.
+ - On the SchemaView path an injected map is honored verbatim.
+ """
+ from linkml_runtime.utils.namespaces import Namespaces
+
+ model = """
+id: http://example.org/test/t1
+name: t1
+default_range: string
+prefixes:
+ xsd: http://www.w3.org/2001/XMLSchema#
+default_prefix: xsd
+"""
+
+ # SchemaLoader path: kwarg must be accepted (previously raised TypeError).
+ injected = Namespaces()
+ injected["ex"] = "http://example.org/injected/"
+ gen = GeneratorTest(model, namespaces=injected)
+ assert gen.namespaces is not None
+ assert "xsd" in gen.namespaces
+
+ # Omitting the kwarg still yields a populated map on the SchemaLoader path.
+ gen_default = GeneratorTest(model)
+ assert gen_default.namespaces is not None
+ assert "xsd" in gen_default.namespaces
+
+
+def test_namespaces_constructor_kwarg_injection_schemaview(tmp_path):
+ """On the SchemaView path an injected ``namespaces=`` map is honored."""
+ from linkml_runtime.utils.namespaces import Namespaces
+
+ schema_path = tmp_path / "schema.yaml"
+ schema_path.write_text(
+ """id: https://example.org/test
+name: test
+prefixes:
+ ex: https://example.org/test/
+default_prefix: ex
+classes:
+ Foo:
+"""
+ )
+
+ injected = Namespaces()
+ injected["custom"] = "http://example.org/custom/"
+ generator = SchemaViewGeneratorTest(schema_path, namespaces=injected)
+
+ with pytest.warns(UserWarning, match="self.namespaces.*SchemaLoader-era"):
+ namespaces = generator.namespaces
+ assert namespaces["custom"] == "http://example.org/custom/"
+
+
def test_duplicate_names():
"""Test duplicate name for slot and type detection"""
model = """
diff --git a/tests/linkml_runtime/test_utils/test_rdf_canonicalize.py b/tests/linkml_runtime/test_utils/test_rdf_canonicalize.py
index 63dbf095a1..8da48fa8b3 100644
--- a/tests/linkml_runtime/test_utils/test_rdf_canonicalize.py
+++ b/tests/linkml_runtime/test_utils/test_rdf_canonicalize.py
@@ -1,17 +1,24 @@
"""Tests for deterministic RDF serialization via pyoxigraph RDFC-1.0."""
+import difflib
import os
import re
import subprocess
import sys
import textwrap
+from pathlib import Path
import pyoxigraph as ox
import pytest
import rdflib
-from rdflib import BNode, Graph, Literal, URIRef
-from rdflib.namespace import RDF
-
+from rdflib import BNode, Graph, Literal, Namespace, URIRef
+from rdflib.namespace import OWL, RDF, RDFS, SH, XSD
+
+from linkml.generators.owlgen import OwlSchemaGenerator
+from linkml.generators.rdfgen import RDFGenerator
+from linkml.generators.shaclgen import ShaclGenerator
+from linkml.generators.shexgen import ShExGenerator
+from linkml.utils.generator import Generator
from linkml_runtime.utils import rdf_canonicalize as rdf_canon_mod
from linkml_runtime.utils.rdf_canonicalize import (
RDFCanonicalizationWarning,
@@ -507,3 +514,244 @@ def run(seed: str) -> str:
assert out_a == out_b, (
"Fallback output differs across PYTHONHASHSEED values; blank-node canonicalization may be missing"
)
+
+
+def _shapes_graph(count: int, extra: bool = False) -> Graph:
+ """A graph shaped like generator output: one blank node per named subject."""
+ g = Graph()
+ g.bind("ex", "http://example.com/")
+ names = [f"{i:02d}" for i in range(count)] + (["AAAinserted"] if extra else [])
+ for name in names:
+ subject = URIRef(f"http://example.com/Shape{name}")
+ prop = BNode()
+ g.add((subject, URIRef("http://example.com/property"), prop))
+ g.add((prop, URIRef("http://example.com/path"), URIRef(f"http://example.com/p{name}")))
+ return g
+
+
+def _changed_line_count(before: str, after: str) -> int:
+ """Count added and removed content lines in a unified diff."""
+
+ diff = difflib.unified_diff(before.splitlines(), after.splitlines(), n=0, lineterm="")
+ return sum(1 for line in diff if line[:1] in "+-" and not line.startswith(("+++", "---")))
+
+
+def test_diff_stable_without_the_extra_names_the_extra(mock_missing_import) -> None:
+ """Requesting the opt-in path without diffable-rdf says how to install it."""
+ mock_missing_import("diffable_rdf")
+
+ with pytest.raises(ImportError, match=r"linkml-runtime\[diff-stable\]"):
+ canonicalize_rdf_graph(_make_graph_with_bnodes(), diff_stable=True)
+
+
+@pytest.mark.parametrize("diff_stable", [False, pytest.param(True, marks=pytest.mark.diffable_rdf)])
+@pytest.mark.parametrize("output_format", ["nt", "turtle"])
+def test_diff_stable_preserves_semantics(diff_stable: bool, output_format: str) -> None:
+ """Each serialization preserves the source graph under blank-node renaming."""
+ graph = _make_graph_with_bnodes()
+ output = canonicalize_rdf_graph(graph, output_format=output_format, diff_stable=diff_stable)
+ parsed = Graph().parse(data=output, format=output_format)
+ assert len(parsed) == len(graph)
+ assert rdflib.compare.isomorphic(graph, parsed)
+
+
+@pytest.mark.diffable_rdf
+def test_diff_stable_is_deterministic() -> None:
+ """A cycle's tied signatures remain independent of input blank-node IDs."""
+ graphs = []
+ predicate = URIRef("http://example.com/next")
+ for labels, reverse in [("abcd", False), ("dbac", True)]:
+ nodes = [BNode(label) for label in labels]
+ triples = [(node, predicate, nodes[(i + 1) % len(nodes)]) for i, node in enumerate(nodes)]
+ graph = Graph()
+ for triple in reversed(triples) if reverse else triples:
+ graph.add(triple)
+ graphs.append(graph)
+
+ assert rdflib.compare.isomorphic(*graphs)
+ outputs = [canonicalize_rdf_graph(graph, output_format="nt", diff_stable=True) for graph in graphs]
+ assert outputs[0] == outputs[1]
+ for graph, output in zip(graphs, outputs):
+ assert rdflib.compare.isomorphic(graph, Graph().parse(data=output, format="nt"))
+
+
+@pytest.mark.parametrize("diff_stable", [False, pytest.param(True, marks=pytest.mark.diffable_rdf)])
+def test_diff_stable_insertion_labels(diff_stable: bool) -> None:
+ """An independent insertion renumbers RDFC labels but preserves the old WL label."""
+ before = Graph().parse(
+ data="""@prefix ex: .
+ ex:Shape00 ex:property _:old .
+ _:old ex:path ex:p00 .
+ """,
+ format="turtle",
+ )
+ after = Graph()
+ for triple in before:
+ after.add(triple)
+ inserted = BNode()
+ ex = Namespace("http://example.com/")
+ after.add((ex.ShapeAAAinserted, ex.property, inserted))
+ after.add((inserted, ex.path, ex.pAAAinserted))
+
+ outputs = [canonicalize_rdf_graph(graph, output_format="nt", diff_stable=diff_stable) for graph in (before, after)]
+ parsed = [set(ox.parse(output, format=ox.RdfFormat.N_TRIPLES)) for output in outputs]
+ old_subject = ox.NamedNode(str(ex.Shape00))
+ new_subject = ox.NamedNode(str(ex.ShapeAAAinserted))
+ property_iri = ox.NamedNode(str(ex.property))
+ path_iri = ox.NamedNode(str(ex.path))
+ (old_before,) = {q.object for q in parsed[0] if q.subject == old_subject and q.predicate == property_iri}
+ (old_after,) = {q.object for q in parsed[1] if q.subject == old_subject and q.predicate == property_iri}
+ (new_after,) = {q.object for q in parsed[1] if q.subject == new_subject and q.predicate == property_iri}
+ assert all(isinstance(node, ox.BlankNode) for node in (old_before, old_after, new_after))
+ assert old_after != new_after
+ inserted_quads = {
+ ox.Quad(new_subject, property_iri, new_after),
+ ox.Quad(new_after, path_iri, ox.NamedNode(str(ex.pAAAinserted))),
+ }
+ assert parsed[0] == {
+ ox.Quad(old_subject, property_iri, old_before),
+ ox.Quad(old_before, path_iri, ox.NamedNode(str(ex.p00))),
+ }
+ assert (
+ parsed[1]
+ == {
+ ox.Quad(old_subject, property_iri, old_after),
+ ox.Quad(old_after, path_iri, ox.NamedNode(str(ex.p00))),
+ }
+ | inserted_quads
+ )
+
+ if diff_stable:
+ assert all(re.fullmatch(_STABLE_LABEL, node.value) for node in (old_before, old_after, new_after))
+ assert old_before == old_after
+ assert parsed[1] - parsed[0] == inserted_quads
+ assert parsed[0] - parsed[1] == set()
+ else:
+ assert (old_before.value, old_after.value, new_after.value) == ("c14n0", "c14n1", "c14n0")
+ for graph, output in zip((before, after), outputs):
+ assert rdflib.compare.isomorphic(graph, Graph().parse(data=output, format="nt"))
+
+
+@pytest.mark.diffable_rdf
+def test_diff_stable_confines_an_insertion_to_the_lines_it_touches() -> None:
+ """Diff-stable labels reduce Turtle line churn for independent property nodes."""
+ before, after = _shapes_graph(20), _shapes_graph(20, extra=True)
+ baseline = _changed_line_count(canonicalize_rdf_graph(before), canonicalize_rdf_graph(after))
+ stable = _changed_line_count(
+ canonicalize_rdf_graph(before, diff_stable=True),
+ canonicalize_rdf_graph(after, diff_stable=True),
+ )
+ assert stable < baseline / 4, f"expected diff-stable output to churn far less; got {stable} vs baseline {baseline}"
+
+
+_STABLE_LABEL = r"b[0-9a-f]{12}(?:_[1-9][0-9]*)?"
+
+
+_DIFF_STABLE_SCHEMA = """\
+id: https://example.org/diffstable
+name: diffstable
+prefixes:
+ linkml: https://w3id.org/linkml/
+ ex: https://example.org/diffstable/
+default_prefix: ex
+default_range: string
+imports:
+ - linkml:types
+classes:
+ Person:
+ slots: [name, knows]
+ Organization:
+ slots: [name]
+slots:
+ name:
+ range: string
+ knows:
+ range: Person
+ multivalued: true
+"""
+
+
+@pytest.fixture
+def diff_stable_schema(tmp_path: Path) -> Path:
+ """Write the small schema for real generator calls."""
+ schema = tmp_path / "schema.yaml"
+ schema.write_text(_DIFF_STABLE_SCHEMA, encoding="utf-8")
+ return schema
+
+
+@pytest.mark.diffable_rdf
+@pytest.mark.parametrize(
+ ("generator", "kwargs"),
+ [
+ pytest.param(OwlSchemaGenerator, {}, id="owlgen"),
+ pytest.param(RDFGenerator, {}, id="rdfgen", marks=pytest.mark.network),
+ pytest.param(ShaclGenerator, {}, id="shaclgen"),
+ pytest.param(ShExGenerator, {"format": "rdf"}, id="shexgen", marks=pytest.mark.network),
+ ],
+)
+def test_diff_stable_reaches_every_rdf_generator(
+ diff_stable_schema: Path, generator: type[Generator], kwargs: dict[str, str]
+) -> None:
+ """Every RDF generator emits the designed schema with diff-stable labels."""
+ output = generator(str(diff_stable_schema), diff_stable=True, **kwargs).serialize()
+ quads = list(ox.parse(output, format=ox.RdfFormat.TURTLE))
+ blank_nodes = {term for quad in quads for term in (quad.subject, quad.object) if isinstance(term, ox.BlankNode)}
+ assert blank_nodes
+ assert all(re.fullmatch(_STABLE_LABEL, node.value) for node in blank_nodes)
+ graph = Graph().parse(data=output, format="turtle")
+ ex = Namespace("https://example.org/diffstable/")
+ if generator is OwlSchemaGenerator:
+ assert (ex.Person, RDF.type, OWL.Class) in graph
+ assert (ex.Organization, RDF.type, OWL.Class) in graph
+ (restriction,) = {
+ node
+ for node in graph.objects(ex.Person, RDFS.subClassOf)
+ if (node, OWL.onProperty, ex.knows) in graph and (node, OWL.allValuesFrom, ex.Person) in graph
+ }
+ assert isinstance(restriction, BNode)
+ assert (restriction, RDF.type, OWL.Restriction) in graph
+ assert (restriction, OWL.allValuesFrom, ex.Person) in graph
+ elif generator is ShaclGenerator:
+ for subject, paths in [(ex.Person, {ex.name, ex.knows}), (ex.Organization, {ex.name})]:
+ assert (subject, RDF.type, SH.NodeShape) in graph
+ assert (subject, SH.targetClass, subject) in graph
+ properties = set(graph.objects(subject, SH.property))
+ assert {graph.value(prop, SH.path) for prop in properties} == paths
+ (ignored,) = graph.objects(subject, SH.ignoredProperties)
+ assert set(graph.predicate_objects(ignored)) == {(RDF.first, RDF.type), (RDF.rest, RDF.nil)}
+ (knows,) = {node for node in graph.objects(ex.Person, SH.property) if (node, SH.path, ex.knows) in graph}
+ assert (knows, SH["class"], ex.Person) in graph
+ elif generator is RDFGenerator:
+ linkml = Namespace("https://w3id.org/linkml/")
+ schema = ex.diffstable
+ assert (schema, RDF.type, linkml.SchemaDefinition) in graph
+ assert set(graph.objects(schema, linkml.classes)) == {ex.Person, ex.Organization}
+ (prefix,) = {node for node in graph.objects(schema, SH.declare) if (node, SH.prefix, Literal("ex")) in graph}
+ assert (prefix, SH.namespace, Literal(str(ex), datatype=XSD.anyURI)) in graph
+ else:
+ assert generator is ShExGenerator
+ shex = Namespace("http://www.w3.org/ns/shex#")
+ assert (ex.Person, RDF.type, shex.Shape) in graph
+ (expression,) = graph.objects(ex.Person, shex.expression)
+ assert (expression, RDF.type, shex.EachOf) in graph
+ (expressions,) = graph.objects(expression, shex.expressions)
+ assert ex.Person_tes in set(graph.items(expressions))
+ assert (ex.Person_tes, RDF.type, shex.EachOf) in graph
+ (slots,) = graph.objects(ex.Person_tes, shex.expressions)
+ (knows,) = {node for node in graph.items(slots) if (node, shex.predicate, ex.knows) in graph}
+ assert (knows, RDF.type, shex.TripleConstraint) in graph
+ assert (knows, shex.valueExpr, ex.Person) in graph
+ assert (knows, shex.min, Literal(0)) in graph
+ assert (knows, shex.max, Literal(-1)) in graph
+
+
+@pytest.mark.diffable_rdf
+def test_diff_stable_warns_instead_of_silently_no_opping_on_the_fallback() -> None:
+ """A relative IRI requires the fallback and a warning that labels cannot apply."""
+ graph = _make_graph_with_bnodes()
+ graph.add((URIRef("testing"), URIRef("http://example.com/p"), Literal("v")))
+ with pytest.warns(RDFCanonicalizationWarning, match="NOT diff-stable"):
+ stable = canonicalize_rdf_graph(graph, diff_stable=True)
+ with pytest.warns(RDFCanonicalizationWarning):
+ plain = canonicalize_rdf_graph(graph, diff_stable=False)
+ assert stable == plain
diff --git a/tests/linkml_runtime/test_utils/test_rdf_canonicalize_defects.py b/tests/linkml_runtime/test_utils/test_rdf_canonicalize_defects.py
new file mode 100644
index 0000000000..806be2e3eb
--- /dev/null
+++ b/tests/linkml_runtime/test_utils/test_rdf_canonicalize_defects.py
@@ -0,0 +1,353 @@
+"""Correctness differences between linkml's RDF canonicalizer and the extracted library.
+
+``linkml_runtime.utils.rdf_canonicalize`` and
+``diffable_rdf.canonicalize_rdf_graph`` are the same code: the module was
+extracted into a standalone library at the maintainers' request
+(linkml/linkml#3295). The two copies have since diverged, so each test here
+asserts one correctness property against *both* implementations and marks the
+one that does not hold it as a strict xfail.
+
+Every remaining gap runs one way -- the extracted copy received fixes the local
+one did not -- and two of them are silent data corruption, where the output
+parses cleanly and says something the input never said. That is worse than a
+crash, because a generated artifact gets committed and reviewed on the
+assumption that it means what the schema meant.
+
+One gap used to run the other way: the library dropped ``@base`` on the
+degraded path. That was the last property blocking delegation, and
+diffable-rdf 0.4.0 fixed it, so the case now passes on both sides and is kept
+unmarked. Asserting both directions is what made the fix land where it
+belongs.
+
+The xfails are strict, so this file is a ratchet in both directions: when a fix
+lands on either side, its case starts passing and the suite fails until the
+mark is removed.
+"""
+
+import os
+import subprocess
+import sys
+import textwrap
+import warnings
+
+import diffable_rdf
+import pytest
+import rdflib
+from rdflib import BNode, Graph, Literal, Namespace, URIRef
+from rdflib.namespace import RDF
+
+from linkml_runtime.utils.rdf_canonicalize import canonicalize_rdf_graph as linkml_canonicalize
+
+EX = Namespace("http://example.org/")
+
+_IMPLEMENTATIONS = (
+ (linkml_canonicalize, "linkml"),
+ (diffable_rdf.canonicalize_rdf_graph, "diffable-rdf"),
+)
+
+
+def _impls(**known_failures: str):
+ """Parametrize over both implementations, xfailing the ones named.
+
+ :param known_failures: implementation id (``linkml`` / ``diffable_rdf``)
+ mapped to why that implementation does not hold the property.
+ """
+ params = []
+ for fn, ident in _IMPLEMENTATIONS:
+ reason = known_failures.get(ident.replace("-", "_"))
+ marks = [pytest.mark.xfail(strict=True, reason=reason)] if reason else []
+ params.append(pytest.param(fn, id=ident, marks=marks))
+ return params
+
+
+def _apply(fn, graph, output_format="turtle"):
+ """Call an implementation, ignoring the warnings both emit on the fallback path."""
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ return fn(graph, output_format)
+
+
+# --------------------------------------------------------------------------
+# Silent data corruption
+# --------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+ "canonicalize",
+ _impls(linkml="passes graph.base to the serializer without checking it is safe to relativize against"),
+)
+def test_base_with_a_fragment_does_not_rewrite_every_iri(canonicalize):
+ """A base IRI ending in ``#`` must not change the graph's terms.
+
+ Relativizing ``http://ex.org/d#a`` against base ``http://ex.org/d#`` gives
+ ``<#a>``, which is correct per RFC 3986 -- but rdflib's parser resolves a
+ fragment reference by concatenation and reads it back as
+ ``http://ex.org/d##a``. Every term of the graph silently changes, and the
+ output parses cleanly, so nothing reports it.
+ """
+ graph = Graph(base="http://ex.org/d#")
+ graph.add((URIRef("http://ex.org/d#a"), EX.p, Literal("v")))
+
+ round_tripped = Graph()
+ round_tripped.parse(data=_apply(canonicalize, graph), format="turtle")
+
+ assert {str(s) for s in round_tripped.subjects()} == {"http://ex.org/d#a"}
+
+
+@pytest.mark.parametrize(
+ "canonicalize",
+ _impls(linkml="rdflib's collection syntax materializes a shared list tail once per referencing list"),
+)
+def test_a_shared_rdf_list_tail_is_not_duplicated(canonicalize):
+ """Two lists sharing a tail must not gain triples on the way out.
+
+ rdflib's Turtle writer renders ``( ... )`` collection syntax per list, so a
+ tail referenced from two lists is written twice as two separate blank
+ nodes. The result asserts more than the input did.
+ """
+ graph = Graph()
+ graph.bind("ex", EX)
+ tail = BNode()
+ graph.add((tail, RDF.first, Literal("shared")))
+ graph.add((tail, RDF.rest, RDF.nil))
+ for name in ("l1", "l2"):
+ head = BNode()
+ graph.add((EX[name], EX.list, head))
+ graph.add((head, RDF.first, Literal(name)))
+ graph.add((head, RDF.rest, tail))
+ # A relative IRI is non-standard RDF, so pyoxigraph refuses the graph and
+ # both implementations degrade to rdflib -- the path this defect lives on.
+ graph.add((URIRef("testing"), EX.p, Literal("forces-fallback")))
+
+ round_tripped = Graph()
+ round_tripped.parse(data=_apply(canonicalize, graph), format="turtle")
+
+ assert len(round_tripped) == len(graph)
+
+
+# --------------------------------------------------------------------------
+# Output that no parser will read
+# --------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+ "canonicalize",
+ _impls(linkml="the rdflib fallback writes N-Triples for a graph pyoxigraph already refused"),
+)
+def test_nt_output_either_parses_or_refuses(canonicalize):
+ """N-Triples admits only absolute IRIs, so a relative one must not be written.
+
+ The fallback is taken *because* the graph holds a term pyoxigraph rejected.
+ Writing it out anyway produces a file that fails on line 1. Refusing with a
+ message naming the term and a format that can carry the graph is the fix;
+ returning unreadable text is not.
+ """
+ graph = Graph()
+ graph.add((URIRef("testing"), EX.p, Literal("v")))
+
+ try:
+ serialized = _apply(canonicalize, graph, "nt")
+ except ValueError:
+ return # refused, with an explanation -- the correct outcome
+
+ rdflib.Graph().parse(data=serialized, format="nt")
+
+
+@pytest.mark.parametrize(
+ "canonicalize",
+ _impls(linkml="sorts fallback output with str.splitlines(), which breaks on U+2028 and five other characters"),
+)
+def test_a_unicode_line_separator_in_a_literal_survives(canonicalize):
+ """Sorting N-Triples lines must split on newlines only.
+
+ N-Triples permits U+2028 raw inside a quoted literal, but
+ ``str.splitlines()`` breaks on it as well as on U+2029, U+0085, U+000B,
+ U+000C and U+001C-1E. One statement becomes two lines, the halves sort
+ independently, the separator is rewritten as a newline, and the document
+ no longer parses.
+ """
+ graph = Graph()
+ graph.add((EX.s, EX.p, Literal("a\u2028b")))
+ graph.add((URIRef("testing"), EX.p, Literal("forces-fallback")))
+
+ try:
+ serialized = _apply(canonicalize, graph, "nt")
+ except ValueError:
+ return # refused because of the relative IRI, before the sort
+
+ assert "\u2028" in serialized
+ rdflib.Graph().parse(data=serialized, format="nt")
+
+
+# --------------------------------------------------------------------------
+# Non-determinism, which is the property the module exists to provide
+# --------------------------------------------------------------------------
+
+_ACROSS_PROCESSES = textwrap.dedent(
+ """
+ import sys, warnings
+ warnings.simplefilter("ignore")
+ from rdflib import BNode, Graph, Literal, Namespace, URIRef
+ from rdflib.namespace import RDF
+
+ module, output_format, force_fallback = sys.argv[1], sys.argv[2], sys.argv[3] == "1"
+ if module == "linkml":
+ from linkml_runtime.utils.rdf_canonicalize import canonicalize_rdf_graph
+ else:
+ from diffable_rdf import canonicalize_rdf_graph
+
+ EX = Namespace("http://example.org/")
+ g = Graph()
+ if force_fallback:
+ g.add((URIRef("testing"), EX.p, Literal("forces-fallback")))
+ for i in range(12):
+ b = BNode()
+ g.add((EX["s%02d" % i], EX.child, b))
+ g.add((b, EX.k, Literal("v%d" % i)))
+ g.add((b, RDF.type, EX.T))
+ for host in "abcdefgh":
+ g.add((URIRef("http://%s.example/s" % host),
+ URIRef("http://%s.example/p" % host),
+ URIRef("http://%s.example/o" % host)))
+ sys.stdout.write(canonicalize_rdf_graph(g, output_format))
+ """
+)
+
+
+def _outputs_across_processes(module, output_format, force_fallback):
+ """Serialize the same graph in four processes with different hash seeds."""
+ outputs = set()
+ for seed in ("0", "1", "12345", "999"):
+ completed = subprocess.run(
+ [sys.executable, "-c", _ACROSS_PROCESSES, module, output_format, "1" if force_fallback else "0"],
+ capture_output=True,
+ text=True,
+ encoding="utf-8",
+ env={**os.environ, "PYTHONHASHSEED": seed},
+ check=False,
+ )
+ if completed.returncode != 0:
+ pytest.fail(f"{module}/{output_format} failed:\n{completed.stderr[-2000:]}")
+ outputs.add(completed.stdout)
+ return outputs
+
+
+_XML_TRAVERSAL = "degraded RDF/XML is ordered by rdflib's graph traversal, which follows set iteration order"
+_NS_NAMES = "auto-generated ns1/ns2 prefix names are allocated in traversal order"
+_JSONLD_UNMAPPED = "json-ld is absent from the format map, so it falls through to rdflib with no determinism guarantee"
+
+
+@pytest.mark.parametrize(
+ ("module", "output_format", "force_fallback"),
+ [
+ pytest.param(
+ "linkml", "xml", True, id="linkml-xml-fallback", marks=pytest.mark.xfail(strict=True, reason=_XML_TRAVERSAL)
+ ),
+ pytest.param("diffable_rdf", "xml", True, id="diffable-rdf-xml-fallback"),
+ pytest.param(
+ "linkml",
+ "turtle",
+ True,
+ id="linkml-turtle-fallback",
+ marks=pytest.mark.xfail(strict=True, reason=_NS_NAMES),
+ ),
+ pytest.param("diffable_rdf", "turtle", True, id="diffable-rdf-turtle-fallback"),
+ pytest.param(
+ "linkml",
+ "json-ld",
+ False,
+ id="linkml-jsonld",
+ marks=pytest.mark.xfail(strict=True, reason=_JSONLD_UNMAPPED),
+ ),
+ pytest.param("diffable_rdf", "json-ld", False, id="diffable-rdf-jsonld"),
+ ],
+)
+def test_output_is_byte_identical_across_processes(module, output_format, force_fallback):
+ """The same graph must serialize to the same bytes in any process.
+
+ Three separate causes break this in the local copy. Degraded RDF/XML is
+ ordered by rdflib's own graph traversal, which follows set iteration order.
+ Auto-generated ``ns1``/``ns2`` prefix names are allocated in traversal order
+ too, so the *names* move even when the triples do not. And ``json-ld`` is
+ not in the format map at all, so it falls through to rdflib's serializer,
+ which carries no determinism guarantee.
+ """
+ assert len(_outputs_across_processes(module, output_format, force_fallback)) == 1
+
+
+# --------------------------------------------------------------------------
+# Interface
+# --------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+ "canonicalize",
+ _impls(linkml="returns the serializer's own trailing whitespace, which differs by format"),
+)
+def test_every_format_ends_with_exactly_one_newline(canonicalize):
+ """A committed artifact should not depend on which format wrote it.
+
+ Passing through whatever the serializer produced gives no trailing newline
+ for RDF/XML and one for Turtle, so POSIX text tools disagree about whether
+ the file has a last line.
+ """
+ counts = {}
+ for output_format in ("turtle", "nt", "xml", "trig", "n3"):
+ graph = Graph()
+ graph.bind("ex", EX)
+ graph.add((EX.s, EX.p, Literal("v")))
+ serialized = _apply(canonicalize, graph, output_format)
+ counts[output_format] = len(serialized) - len(serialized.rstrip("\n"))
+
+ assert counts == dict.fromkeys(counts, 1)
+
+
+@pytest.mark.parametrize(
+ "canonicalize",
+ _impls(linkml="Dataset is a Graph subclass, so the type check accepts it and the named graphs are flattened"),
+)
+def test_a_dataset_is_refused_rather_than_partly_serialized(canonicalize):
+ """A Dataset is a Graph subclass, so it is accepted and quietly flattened.
+
+ Every named graph collapses into one document and the graph names are
+ dropped, which is a different dataset. The caller asked for something this
+ function cannot express and should be told so.
+ """
+ dataset = rdflib.Dataset()
+ dataset.graph(URIRef("http://ex/g1")).add((EX.a, EX.p, Literal("in-g1")))
+ dataset.graph(URIRef("http://ex/g2")).add((EX.b, EX.p, Literal("in-g2")))
+
+ with pytest.raises(TypeError):
+ _apply(canonicalize, dataset)
+
+
+# --------------------------------------------------------------------------
+# A property both implementations now hold
+# --------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize("canonicalize", _impls())
+def test_base_survives_the_degraded_path(canonicalize):
+ """``@base`` must not disappear just because pyoxigraph refused the graph.
+
+ ``rdflib_dumper.dumps(..., prefix_map={"@base": ...})`` is a supported way
+ to ask for a document whose IRIs are written relative to a base, and the
+ metamodel routinely produces graphs that fall back (a bare ``status:
+ testing`` on a ``uriorcurie`` slot serializes to the relative ````,
+ which pyoxigraph rejects). Losing the directive on exactly that path means
+ the feature works only for graphs that never needed the fallback.
+
+ This ran the other way until diffable-rdf 0.4.0, which was the last
+ property blocking delegation. The case is kept because it is the one the
+ two implementations reach differently: linkml passes ``graph.base`` to the
+ serializer unconditionally, which is why it also fails
+ :func:`test_base_with_a_fragment_does_not_rewrite_every_iri` above, while
+ the library keeps the base only after confirming that re-reading the
+ result still yields every absolute IRI of the source.
+ """
+ graph = Graph(base="http://example.org/default/")
+ graph.bind("ex", EX)
+ graph.add((EX.s, EX.p, Literal("v")))
+ graph.add((URIRef("testing"), EX.p, Literal("forces-fallback")))
+
+ assert "@base ." in _apply(canonicalize, graph)
diff --git a/tests/linkml_runtime/test_utils/test_schemaview.py b/tests/linkml_runtime/test_utils/test_schemaview.py
index fa467d82c3..4b93365caa 100644
--- a/tests/linkml_runtime/test_utils/test_schemaview.py
+++ b/tests/linkml_runtime/test_utils/test_schemaview.py
@@ -664,6 +664,59 @@ def test_import_map_in_memory_dict_transitive(tmp_path: Path) -> None:
assert view.induced_slot("leaf_attr", "RootClass").range == "string"
+def _write_redirect_tree(tmp_path: Path, sub_imports: list[str]) -> Path:
+ """``root_dir/main.yaml`` imports ``sub``, which an importmap redirects to
+ ``elsewhere/sub.yaml``; ``sub``'s own imports must resolve next to it, not next to ``main``.
+ Returns the path to ``main.yaml``."""
+ root_dir = tmp_path / "root_dir"
+ elsewhere = tmp_path / "elsewhere"
+ root_dir.mkdir()
+ elsewhere.mkdir()
+ (root_dir / "main.yaml").write_text(
+ "id: https://example.org/main\nname: main\ndefault_range: string\nimports: [sub]\n"
+ "classes:\n Thing:\n slots: [sub_slot]\n"
+ )
+ (elsewhere / "sub.yaml").write_text(
+ f"id: https://example.org/sub\nname: sub\ndefault_range: string\nimports: {sub_imports}\nslots:\n sub_slot:\n"
+ )
+ (elsewhere / "leaf.yaml").write_text(
+ "id: https://example.org/leaf\nname: leaf\ndefault_range: string\nslots:\n leaf_slot:\n"
+ )
+ return root_dir / "main.yaml"
+
+
+@pytest.mark.parametrize("importmap_value", ["../elsewhere/sub", "ABSOLUTE"])
+def test_importmap_redirected_schema_resolves_own_relative_imports(tmp_path: Path, importmap_value: str) -> None:
+ """A schema redirected outside the root tree by an importmap resolves its own relative
+ imports next to itself, not in the root schema's directory (#3499)."""
+ main = _write_redirect_tree(tmp_path, sub_imports=["./leaf"])
+ if importmap_value == "ABSOLUTE":
+ importmap_value = str(tmp_path / "elsewhere" / "sub")
+
+ view = SchemaView(str(main), importmap={"sub": importmap_value})
+ slots = view.all_slots(imports=True)
+ assert "sub_slot" in slots
+ assert "leaf_slot" in slots
+
+
+def test_importmap_redirected_schema_resolves_nested_relative_imports(tmp_path: Path) -> None:
+ """Relative imports resolve against the importing schema at every depth below a redirect."""
+ main = _write_redirect_tree(tmp_path, sub_imports=["./nested/deep"])
+ nested = tmp_path / "elsewhere" / "nested"
+ nested.mkdir()
+ (nested / "deep.yaml").write_text(
+ "id: https://example.org/deep\nname: deep\ndefault_range: string\nimports: [./deepest]\nslots:\n deep_slot:\n"
+ )
+ (nested / "deepest.yaml").write_text(
+ "id: https://example.org/deepest\nname: deepest\ndefault_range: string\nslots:\n deepest_slot:\n"
+ )
+
+ view = SchemaView(str(main), importmap={"sub": "../elsewhere/sub"})
+ slots = view.all_slots(imports=True)
+ assert "deep_slot" in slots
+ assert "deepest_slot" in slots
+
+
def test_merge_imports_kwargs(schema_view_with_imports: SchemaView, sv_merged_imports_keyword: SchemaView) -> None:
"""Ensure that imports are or are not merged, depending on the kwargs."""
@@ -1678,6 +1731,55 @@ def test_all_enums(schema_view_with_imports: SchemaView) -> None:
assert e.from_schema == "https://w3id.org/linkml/tests/core"
+def _write_shared_attribute_tree(tmp_path: Path) -> Path:
+ """Two modules, each declaring the same attribute name, imported as ./child_a and ./child_b."""
+ for suffix in ("a", "b"):
+ (tmp_path / f"child_{suffix}.yaml").write_text(
+ f"id: https://example.org/child_{suffix}\n"
+ f"name: child_{suffix}\n"
+ "prefixes: {linkml: 'https://w3id.org/linkml/', ex: 'https://example.org/'}\n"
+ "default_prefix: ex\n"
+ "default_range: string\n"
+ "imports: [linkml:types]\n"
+ "classes:\n"
+ f" Child{suffix}:\n"
+ " is_a: Parent\n"
+ " attributes:\n"
+ " shared_attribute:\n"
+ " range: string\n"
+ )
+ main = tmp_path / "main.yaml"
+ main.write_text(
+ "id: https://example.org/main\n"
+ "name: main\n"
+ "prefixes: {linkml: 'https://w3id.org/linkml/', ex: 'https://example.org/'}\n"
+ "default_prefix: ex\n"
+ "default_range: string\n"
+ "imports: [linkml:types, ./child_a, ./child_b]\n"
+ "classes:\n"
+ " Parent:\n"
+ " description: parent class, defined in the importing schema\n"
+ )
+ return main
+
+
+def test_get_uri_element_defined_in_relatively_imported_schema(tmp_path: Path) -> None:
+ """get_uri resolves an element whose schema was imported under a relative path.
+
+ ``schema_map`` is keyed by the import as written (``./child_b``) while ``in_schema()``
+ reports the schema's name (``child_b``). An attribute declared in more than one class
+ has no ``from_schema``, so the lookup falls back to the key and must tolerate the
+ difference. This is what makes ``gen-shacl`` fail on modular schemas (#3878).
+ """
+ main = _write_shared_attribute_tree(tmp_path)
+ view = SchemaView(str(main))
+
+ # precondition: the attribute is declared in more than one class, so it has no
+ # from_schema and get_uri has to locate its schema rather than being handed it
+ assert view.get_element("shared_attribute").from_schema is None
+ assert view.get_uri("shared_attribute", expand=True) == "https://example.org/shared_attribute"
+
+
def test_get_uri(schema_view_with_imports: SchemaView) -> None:
"""Test the get_uri function."""
view = schema_view_with_imports
@@ -3878,3 +3980,36 @@ def test_annotation_dict_for_induced_slot() -> None:
assert original_annots["bar"] == "some value"
induced_annots = sv.annotation_dict("foo", class_name="TestClass")
assert induced_annots["bar"] == "some other value"
+
+
+def test_relative_import_in_url_imported_schema(tmp_path: Path) -> None:
+ """A relative import inside a schema imported by URL resolves against that URL.
+
+ The importing schema's key is an absolute URL, which must not be normalised as a
+ filesystem path: doing so collapses the ``//`` of the scheme and the result is then
+ mistaken for a CURIE (#3499).
+ """
+ modules = tmp_path / "modules"
+ modules.mkdir()
+ (modules / "leaf.yaml").write_text(
+ "id: https://example.org/leaf\nname: leaf\ndefault_range: string\nslots:\n leaf_slot:\n"
+ )
+ (modules / "middle.yaml").write_text(
+ "id: https://example.org/middle\nname: middle\ndefault_range: string\n"
+ "imports: [./leaf]\nslots:\n middle_slot:\n"
+ )
+ consumer = tmp_path / "consumer"
+ consumer.mkdir()
+ root = consumer / "root.yaml"
+ # as_uri() rather than an f-string: on Windows a bare path yields backslashes and only
+ # two slashes after the scheme, which is not a valid URIorCURIE and is rejected when the
+ # importing schema is loaded
+ middle_url = (modules / "middle").as_uri()
+ root.write_text(
+ "id: https://example.org/root\nname: root\ndefault_range: string\n"
+ f"imports: ['{middle_url}']\nclasses:\n Thing:\n slots: [middle_slot]\n"
+ )
+
+ slots = SchemaView(str(root)).all_slots(imports=True)
+ assert "middle_slot" in slots
+ assert "leaf_slot" in slots
diff --git a/uv.lock b/uv.lock
index 35dada7d4f..5b9d556b7c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -10,7 +10,7 @@ resolution-markers = [
]
[options]
-exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
+exclude-newer = "2026-09-24T21:22:43.997041597Z"
exclude-newer-span = "P7D"
[manifest]
@@ -661,7 +661,7 @@ resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
wheels = [
@@ -734,7 +734,7 @@ resolution-markers = [
"python_full_version == '3.11.*'",
]
dependencies = [
- { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" } },
+ { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [
@@ -1084,6 +1084,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
+[[package]]
+name = "diffable-rdf"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyoxigraph" },
+ { name = "rdflib" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/df/1d71c0c984eac1cfd25531aabf84528941b01083c875da49d44882d1b5af/diffable_rdf-0.4.0.tar.gz", hash = "sha256:a84afaa10332e6a039b6ddcabf76bced3049e4da467c88ee786d0fa35218111e", size = 838040, upload-time = "2026-09-11T11:43:12.844Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/49/b62ee864fe6769b759f04e3709fd79711b1cdf49223b1bb0e985e507ead3/diffable_rdf-0.4.0-py3-none-any.whl", hash = "sha256:c59c57429465f786fdbc8d38ddbb687da16748c5752f0c2ac4a77b5527aa9aed", size = 46126, upload-time = "2026-09-11T11:43:11.536Z" },
+]
+
[[package]]
name = "distlib"
version = "0.4.0"
@@ -1172,7 +1185,7 @@ name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
@@ -1714,17 +1727,17 @@ resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "decorator" },
- { name = "exceptiongroup" },
- { name = "jedi" },
- { name = "matplotlib-inline" },
- { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit" },
- { name = "pygments" },
- { name = "stack-data" },
- { name = "traitlets" },
- { name = "typing-extensions" },
+ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version < '3.11'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "jedi", marker = "python_full_version < '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
+ { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
+ { name = "pygments", marker = "python_full_version < '3.11'" },
+ { name = "stack-data", marker = "python_full_version < '3.11'" },
+ { name = "traitlets", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" }
wheels = [
@@ -1742,17 +1755,17 @@ resolution-markers = [
"python_full_version == '3.11.*'",
]
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "decorator" },
- { name = "ipython-pygments-lexers" },
- { name = "jedi" },
- { name = "matplotlib-inline" },
- { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit" },
- { name = "pygments" },
- { name = "stack-data" },
- { name = "traitlets" },
- { name = "typing-extensions", marker = "python_full_version < '3.12'" },
+ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version >= '3.11'" },
+ { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
+ { name = "jedi", marker = "python_full_version >= '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
+ { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+ { name = "stack-data", marker = "python_full_version >= '3.11'" },
+ { name = "traitlets", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2a/34/29b18c62e39ee2f7a6a3bba7efd952729d8aadd45ca17efc34453b717665/ipython-9.6.0.tar.gz", hash = "sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731", size = 4396932, upload-time = "2025-09-29T10:55:53.948Z" }
wheels = [
@@ -1773,7 +1786,7 @@ name = "ipython-pygments-lexers"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pygments" },
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
wheels = [
@@ -2147,7 +2160,7 @@ wheels = [
[[package]]
name = "jupyterlab"
-version = "4.6.2"
+version = "4.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "async-lru" },
@@ -2166,9 +2179,9 @@ dependencies = [
{ name = "traitlets" },
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7a/7f/51c0c856ab286bdaf5709cf61ed13584ed9d4bee906479707da45b11b353/jupyterlab-4.6.2.tar.gz", hash = "sha256:e18ce8b34f3de350e93cd5b2c4f3ae884cbe266eb76bf5d6825a4ed34c13bcff", size = 28183650, upload-time = "2026-07-21T12:05:24.051Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/8d/995cc142f6083346b35e7d3eadc5a6717ee89ce64ea53577010ac493bc3c/jupyterlab-4.6.4.tar.gz", hash = "sha256:404f49b081819378524886c9db66dba57a5565981eff885830df1baba3a17df5", size = 28335647, upload-time = "2026-09-21T15:25:18.779Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/63/1f/e39b248c76bb3736bc05a9491a6aa1315414c32dea12f548ecc1b24c758e/jupyterlab-4.6.2-py3-none-any.whl", hash = "sha256:5964447036629adfcd3fc0969effc1da6f47d2cbd0a60b2c2eea7c31be0ec6a8", size = 17166703, upload-time = "2026-07-21T12:05:19.818Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/e4/072bc0d3c6d45d414062a06af7ee81c02a2c12d769c358ec9919f9995ffd/jupyterlab-4.6.4-py3-none-any.whl", hash = "sha256:15b13f991d3985129c797eb84d9949eeb8b6615e14b444868e642411f2c418b2", size = 17171596, upload-time = "2026-09-21T15:25:14.123Z" },
]
[[package]]
@@ -2403,6 +2416,11 @@ dependencies = [
{ name = "watchdog" },
]
+[package.optional-dependencies]
+diff-stable = [
+ { name = "linkml-runtime", extra = ["diff-stable"] },
+]
+
[package.dev-dependencies]
bigquery = [
{ name = "sqlalchemy-bigquery" },
@@ -2417,6 +2435,7 @@ dev = [
{ name = "ipython-genutils" },
{ name = "jsonpatch" },
{ name = "jupyter" },
+ { name = "linkml-runtime", extra = ["diff-stable"] },
{ name = "mock" },
{ name = "myst-nb" },
{ name = "nbconvert" },
@@ -2469,6 +2488,7 @@ shacl = [
tests = [
{ name = "black" },
{ name = "duckdb" },
+ { name = "linkml-runtime", extra = ["diff-stable"] },
{ name = "numpydantic" },
{ name = "pyshacl" },
{ name = "sqlalchemy-bigquery" },
@@ -2508,10 +2528,11 @@ requires-dist = [
{ name = "jsonasobj2", specifier = ">=1.0.3,<2.0.0" },
{ name = "jsonschema", extras = ["format"], specifier = ">=4.26.0" },
{ name = "linkml-runtime", editable = "packages/linkml_runtime" },
+ { name = "linkml-runtime", extras = ["diff-stable"], marker = "extra == 'diff-stable'", editable = "packages/linkml_runtime" },
{ name = "openapi-spec-validator", specifier = ">=0.8.4" },
{ name = "openpyxl" },
{ name = "parse" },
- { name = "platformdirs", specifier = ">=4.11.8" },
+ { name = "platformdirs", specifier = ">=4.11.12" },
{ name = "prefixcommons", specifier = ">=0.1.7" },
{ name = "prefixmaps", specifier = ">=0.2.2" },
{ name = "pydantic", specifier = ">=2.13.5,<3.0.0" },
@@ -2524,10 +2545,11 @@ requires-dist = [
{ name = "rdflib", specifier = ">=7.6.0" },
{ name = "requests", specifier = ">=2.22" },
{ name = "sphinx-click", specifier = ">=6.0.0" },
- { name = "sqlalchemy", specifier = ">=2.0.53" },
+ { name = "sqlalchemy", specifier = ">=2.0.54" },
{ name = "typing-extensions", marker = "python_full_version < '3.12'", specifier = ">=4.6.0" },
{ name = "watchdog", specifier = ">=0.9.0" },
]
+provides-extras = ["diff-stable"]
[package.metadata.requires-dev]
bigquery = [{ name = "sqlalchemy-bigquery", specifier = ">=1.9.0" }]
@@ -2541,6 +2563,7 @@ dev = [
{ name = "ipython-genutils" },
{ name = "jsonpatch", specifier = ">=1.33" },
{ name = "jupyter" },
+ { name = "linkml-runtime", extras = ["diff-stable"], editable = "packages/linkml_runtime" },
{ name = "mock", specifier = ">=5.1.0" },
{ name = "myst-nb", marker = "python_full_version >= '3.10'", specifier = ">=1.4.0" },
{ name = "nbconvert" },
@@ -2563,7 +2586,7 @@ dev = [
{ name = "sqlalchemy-bigquery", specifier = ">=1.17.2" },
{ name = "testcontainers", specifier = "==4.15.0" },
{ name = "tomli-w", specifier = ">=1.2.0" },
- { name = "tox", specifier = ">=4.61.2" },
+ { name = "tox", specifier = ">=4.64.1" },
{ name = "tox-uv" },
]
docs = [
@@ -2588,6 +2611,7 @@ shacl = [{ name = "pyshacl", specifier = ">=0.40.1" }]
tests = [
{ name = "black", specifier = ">=24.0.0" },
{ name = "duckdb", specifier = ">=1.5.5" },
+ { name = "linkml-runtime", extras = ["diff-stable"], editable = "packages/linkml_runtime" },
{ name = "numpydantic", specifier = ">=1.10.0" },
{ name = "pyshacl", specifier = ">=0.40.1" },
{ name = "sqlalchemy-bigquery", specifier = ">=1.17.2" },
@@ -2607,7 +2631,7 @@ tests-extra = [
{ name = "testcontainers", specifier = "==4.15.0" },
]
tests-rustgen = [{ name = "maturin", specifier = ">=1.15.0" }]
-typedb = [{ name = "typedb-driver", specifier = ">=3.12.3,<4.0" }]
+typedb = [{ name = "typedb-driver", specifier = ">=3.13.6,<4.0" }]
typing = [{ name = "numpydantic", specifier = ">=1.10.0" }]
[[package]]
@@ -2636,6 +2660,9 @@ dev = [
{ name = "coverage" },
{ name = "requests-cache" },
]
+diff-stable = [
+ { name = "diffable-rdf" },
+]
[package.dev-dependencies]
dev = [
@@ -2649,6 +2676,7 @@ requires-dist = [
{ name = "coverage", marker = "extra == 'dev'" },
{ name = "curies", specifier = ">=0.14.6" },
{ name = "deprecated" },
+ { name = "diffable-rdf", marker = "extra == 'diff-stable'", specifier = ">=0.4.0" },
{ name = "hbreader" },
{ name = "isodate", marker = "python_full_version < '3.11'", specifier = ">=0.7.2,<1.0.0" },
{ name = "json-flattener", specifier = ">=0.1.9" },
@@ -2663,7 +2691,7 @@ requires-dist = [
{ name = "requests" },
{ name = "requests-cache", marker = "extra == 'dev'", specifier = ">=1.3.3" },
]
-provides-extras = ["dev"]
+provides-extras = ["dev", "diff-stable"]
[package.metadata.requires-dev]
dev = [
@@ -3175,7 +3203,7 @@ wheels = [
[[package]]
name = "notebook"
-version = "7.6.0"
+version = "7.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jupyter-builder" },
@@ -3185,9 +3213,9 @@ dependencies = [
{ name = "notebook-shim" },
{ name = "tornado" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0d/44/d5c65783f490298473bb1c05722e05ee2256231389559c2c5ae0a3e5d975/notebook-7.6.0.tar.gz", hash = "sha256:ea13e79e601bf273074895fdfb17dd3f2da916d3c045e0b9c47d18b16ab62481", size = 5497344, upload-time = "2026-06-18T16:18:55.202Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/42/ac/aedb759dc683dcd129f6c75271bab5eb23020b0f7969163c726343718e1e/notebook-7.6.3.tar.gz", hash = "sha256:e2c08e469c0ae20bb0b3214f0ab77e79653317a2f8e5b34c10361c66874a5b50", size = 5501618, upload-time = "2026-09-21T18:07:24.261Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/93/d1/e617c40db57ff40e75f43a7d4d1c305e3a54c053ab5cb0534a6c314664f9/notebook-7.6.0-py3-none-any.whl", hash = "sha256:98aa2811b54ac191321d5dfce12ca700f8a511a33a26e4de2fa106a357c43d6a", size = 5544575, upload-time = "2026-06-18T16:18:52.551Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/f7/907b98438cf00bdc4e296570058f29a347214bb42338a48e3b7eae58ae2a/notebook-7.6.3-py3-none-any.whl", hash = "sha256:ad7e0eb765fba836cd4a2ab0c7a3a26cde1d91665fbf6f533b6ae7b2de6d88d2", size = 5548452, upload-time = "2026-09-21T18:07:21.563Z" },
]
[[package]]
@@ -3673,11 +3701,11 @@ wheels = [
[[package]]
name = "platformdirs"
-version = "4.11.8"
+version = "4.11.12"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/53/18/f3bb8ef0d3b930692343da8aa4d3cbcd6749477c053959395ac81965a6e9/platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc", size = 37182, upload-time = "2026-09-08T22:20:42.866Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/dd/65804b0c2925a1c821a05502ea57517b69a073ff400d25ab9faa3a2cf012/platformdirs-4.11.12.tar.gz", hash = "sha256:e8dc1cb58f1153fd7f61db1374317770baababec2480b37b8f01c6cc25b45267", size = 40016, upload-time = "2026-09-22T01:02:36.973Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/e1/5b7b8bbb55084d1425bcb9bc823ff519e1b2be05f6ebb0089e2eacc38413/platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081", size = 24027, upload-time = "2026-09-08T22:20:41.537Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/2b/69de6317fbf48865bb12aa56c9cfcfff46b3add0cd45250725d96146dce0/platformdirs-4.11.12-py3-none-any.whl", hash = "sha256:b1ba966ac76153d9985a0787ffab951e7c18416189873d4e4ddb2c8a04a0a254", size = 24976, upload-time = "2026-09-22T01:02:35.742Z" },
]
[[package]]
@@ -4357,15 +4385,14 @@ wheels = [
[[package]]
name = "python-discovery"
-version = "1.5.0"
+version = "1.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
- { name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f1/51/276f964496a5714ab9f320896195639086881c2b39c03b5ad13de84acbb8/python_discovery-1.5.0.tar.gz", hash = "sha256:3e014c6327154d3dda27939a9a0dc9c5c000439f1906d3f303b48f984bd2ecef", size = 72483, upload-time = "2026-07-21T13:14:14.641Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/57/250bd238b966cece44328235eb85290045d059265fdaf7527a3a958123db/python_discovery-1.6.1.tar.gz", hash = "sha256:cf87d3627dfb4412437fdd5b13eae402607722998d21567993aedbc59b23c15e", size = 84338, upload-time = "2026-09-18T01:31:53.971Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/7b/14882602ddee241d7984a742fcb423cb4a30fb0d6efc546ac3129fba475a/python_discovery-1.5.0-py3-none-any.whl", hash = "sha256:70c4fc61b4e7404e44f01d6fc44a715c4d685ca6cea83d295922f05891877c98", size = 34205, upload-time = "2026-07-21T13:14:13.398Z" },
+ { url = "https://files.pythonhosted.org/packages/16/7d/e9ffbadfbf89c93848412d04594135c4ae8c1d37d9e053b9c3ed718fabc4/python_discovery-1.6.1-py3-none-any.whl", hash = "sha256:d43fcdef879fe795352bd13ccf8d185ba5a9f86f36cfcd00529f596e737442b3", size = 38664, upload-time = "2026-09-18T01:31:52.448Z" },
]
[[package]]
@@ -4965,23 +4992,23 @@ resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
- { name = "alabaster" },
- { name = "babel" },
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "docutils" },
- { name = "imagesize" },
- { name = "jinja2" },
- { name = "packaging" },
- { name = "pygments" },
- { name = "requests" },
- { name = "snowballstemmer" },
- { name = "sphinxcontrib-applehelp" },
- { name = "sphinxcontrib-devhelp" },
- { name = "sphinxcontrib-htmlhelp" },
- { name = "sphinxcontrib-jsmath" },
- { name = "sphinxcontrib-qthelp" },
- { name = "sphinxcontrib-serializinghtml" },
- { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" } },
+ { name = "alabaster", marker = "python_full_version < '3.11'" },
+ { name = "babel", marker = "python_full_version < '3.11'" },
+ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
+ { name = "docutils", marker = "python_full_version < '3.11'" },
+ { name = "imagesize", marker = "python_full_version < '3.11'" },
+ { name = "jinja2", marker = "python_full_version < '3.11'" },
+ { name = "packaging", marker = "python_full_version < '3.11'" },
+ { name = "pygments", marker = "python_full_version < '3.11'" },
+ { name = "requests", marker = "python_full_version < '3.11'" },
+ { name = "snowballstemmer", marker = "python_full_version < '3.11'" },
+ { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" },
+ { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" },
+ { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" },
+ { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" },
+ { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" },
+ { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" },
+ { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" }
wheels = [
@@ -4999,23 +5026,23 @@ resolution-markers = [
"python_full_version == '3.11.*'",
]
dependencies = [
- { name = "alabaster" },
- { name = "babel" },
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "docutils" },
- { name = "imagesize" },
- { name = "jinja2" },
- { name = "packaging" },
- { name = "pygments" },
- { name = "requests" },
- { name = "roman-numerals-py" },
- { name = "snowballstemmer" },
- { name = "sphinxcontrib-applehelp" },
- { name = "sphinxcontrib-devhelp" },
- { name = "sphinxcontrib-htmlhelp" },
- { name = "sphinxcontrib-jsmath" },
- { name = "sphinxcontrib-qthelp" },
- { name = "sphinxcontrib-serializinghtml" },
+ { name = "alabaster", marker = "python_full_version >= '3.11'" },
+ { name = "babel", marker = "python_full_version >= '3.11'" },
+ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
+ { name = "docutils", marker = "python_full_version >= '3.11'" },
+ { name = "imagesize", marker = "python_full_version >= '3.11'" },
+ { name = "jinja2", marker = "python_full_version >= '3.11'" },
+ { name = "packaging", marker = "python_full_version >= '3.11'" },
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+ { name = "requests", marker = "python_full_version >= '3.11'" },
+ { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" },
+ { name = "snowballstemmer", marker = "python_full_version >= '3.11'" },
+ { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" },
+ { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" },
+ { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" },
+ { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" },
+ { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" },
+ { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" }
wheels = [
@@ -5191,51 +5218,57 @@ wheels = [
[[package]]
name = "sqlalchemy"
-version = "2.0.53"
+version = "2.0.54"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4c/de/59fc3d2e63dfd367e803417c40e70e35b6c0a0997420cb082d351cdd052b/sqlalchemy-2.0.53.tar.gz", hash = "sha256:b568a8ac0778988c2710476017ff74fb5dcb9b9322d757189c3d7fc4cc8026fa", size = 9966200, upload-time = "2026-09-14T20:37:31.731Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6d/4b/303c139417e53e7827e4f46a44a5bcc3f50ce713029abf26d79d93f2f314/sqlalchemy-2.0.53-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cfd51b94590814cfa5020534225f61dda1af485299cd2dbb1ee8112d0e92b52f", size = 2189252, upload-time = "2026-09-14T22:39:47.549Z" },
- { url = "https://files.pythonhosted.org/packages/b4/bf/43ed9a166b20f4d0edd7f7b6af3d40801aba9498c3223f4dcbd4afe0a735/sqlalchemy-2.0.53-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f90d64f69cd80ef0acba1e92357bb412e4ba71476b745c83d1499c3b0383303", size = 3358346, upload-time = "2026-09-14T22:46:41.389Z" },
- { url = "https://files.pythonhosted.org/packages/55/74/c61a6c8ccc5c7db3d2d85e2d28233bc82f9fd775986a858f52da46e45146/sqlalchemy-2.0.53-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7d6d72fcffbba8258cfe663786b87d711812e09dad757a1f58996ab7f0d8f2", size = 3357676, upload-time = "2026-09-14T22:50:10.66Z" },
- { url = "https://files.pythonhosted.org/packages/8a/36/8cdbfa4b5652413028bff4714d2b72b5c7b4505d5e8a37f627e7bf4da8ea/sqlalchemy-2.0.53-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12a3bd96dafcf830bcebeecbbda5921cdd57e41bd074646c285a92d4f76862c6", size = 3308245, upload-time = "2026-09-14T22:46:42.773Z" },
- { url = "https://files.pythonhosted.org/packages/db/15/ad2c2e030350b58a59b433c7740a49f724b879de623ed589a9bf57505998/sqlalchemy-2.0.53-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49e366b972504f0c600de8ad58054017d7b7a5176e5765cea7df76b5e05548d2", size = 3336719, upload-time = "2026-09-14T22:50:12.443Z" },
- { url = "https://files.pythonhosted.org/packages/73/16/a9e95ebbec394f88d4a072506f389f91e87e0699059455da57b3f80b6ac9/sqlalchemy-2.0.53-cp310-cp310-win32.whl", hash = "sha256:d31e2714d6f5df4a308d8e5aafecb6aea1bc386bbee671044492f9a124461ce1", size = 2144280, upload-time = "2026-09-14T22:55:55.714Z" },
- { url = "https://files.pythonhosted.org/packages/df/98/184d7d53c27bf5473a3764954bdf8a85a753aba4323474b05a2d820359b1/sqlalchemy-2.0.53-cp310-cp310-win_amd64.whl", hash = "sha256:3fe213af5d7e11c91692505ecadb7b0980c424b050d8b6458e39f38090094d7c", size = 2170302, upload-time = "2026-09-14T22:55:57.57Z" },
- { url = "https://files.pythonhosted.org/packages/e7/58/68f32c4428e01aa784d045d74fdd9ceacca1f27b7030d468a7c90dcb0839/sqlalchemy-2.0.53-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b0a3481fdea08ce9f93c318e40c446c91bc63260a5eae1cf06d85b4d4d5651d4", size = 2187761, upload-time = "2026-09-14T22:39:48.925Z" },
- { url = "https://files.pythonhosted.org/packages/9d/ad/57b82748084536340703d42b23694a259bef66db17f21cf37a0475480cfa/sqlalchemy-2.0.53-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29e7eb8bb305411f26ead6854e7591cfc6a5b3b6b924a31bd8b843ba243a3ebd", size = 3428938, upload-time = "2026-09-14T22:46:44.295Z" },
- { url = "https://files.pythonhosted.org/packages/a1/f9/9c719de2e6628b8d0884082590da324674a10ec3bb177face202a8bb439b/sqlalchemy-2.0.53-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:255c1ca3810da6994b755c80b3abc27a5fb1d45b6703985092a2f6663f9daa9e", size = 3428551, upload-time = "2026-09-14T22:50:14.85Z" },
- { url = "https://files.pythonhosted.org/packages/0a/0c/93586869f7c852b4138736d9ea6121f4b2f967878bec616a36caa9cb2502/sqlalchemy-2.0.53-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:764eebce5bf2046172f6f32d51ee47b0acc0dbdb05262a6382875267bc5b97d4", size = 3377684, upload-time = "2026-09-14T22:46:45.882Z" },
- { url = "https://files.pythonhosted.org/packages/bb/4d/078b5badf416692545382a8cc803230f27dab29ab2dc970840566a221b7a/sqlalchemy-2.0.53-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab1489a2a417f5d8f03d2824771e168ac2da8db9c34e05de8a404182390d67e7", size = 3402953, upload-time = "2026-09-14T22:50:16.54Z" },
- { url = "https://files.pythonhosted.org/packages/61/24/31d22eee205f611286cfc172dcc71f502454895a469e66ef5ecb32998241/sqlalchemy-2.0.53-cp311-cp311-win32.whl", hash = "sha256:90242b1ad2bdad5fcebf32fb2971465edd4034e0381c1d6126bcb5a5b1851f18", size = 2142283, upload-time = "2026-09-14T22:55:58.909Z" },
- { url = "https://files.pythonhosted.org/packages/80/bd/6ac820a60ca104a20977b267381ebe78d84fdf3aaaa44fd98a952cdcc962/sqlalchemy-2.0.53-cp311-cp311-win_amd64.whl", hash = "sha256:05faf168e324b5c609df10530d25620fcce7a815635c653903d3f459617b85bd", size = 2169208, upload-time = "2026-09-14T22:56:00.347Z" },
- { url = "https://files.pythonhosted.org/packages/57/a9/fa8d769bdbcdf9db40ce1491da397bb7ea45e381791800319b4e7c5d6635/sqlalchemy-2.0.53-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ca22c441645ead511eb73f1efdb0f3a723f138e28f8362224813f64a8097e15", size = 2187584, upload-time = "2026-09-14T22:43:16.399Z" },
- { url = "https://files.pythonhosted.org/packages/52/a7/4534510e1deda24a505504194d805f22ef592bb51243a4f4b2f97f116593/sqlalchemy-2.0.53-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d26a8411cedab7102f31d766711f3e5fd93bf7d3a1470200bacaaaac7faaa826", size = 3448731, upload-time = "2026-09-14T22:49:36.249Z" },
- { url = "https://files.pythonhosted.org/packages/b7/a0/30c213493d215e685d6ba8a0d8ead0fb908e731f07f924ae2ab77297aec8/sqlalchemy-2.0.53-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9049aaeae45d30dfa3bfba105175a358bf32e9fa563e281a016cf44d8aaa6b5f", size = 3466629, upload-time = "2026-09-14T23:06:58.975Z" },
- { url = "https://files.pythonhosted.org/packages/07/90/f1b1ce1ec34f6c8b81c557ffda2a7fe191c0bed64e649a720e5feee7c5ab/sqlalchemy-2.0.53-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0417dac5a5bbc7bff3fd95f53ff193c0fdcbdde4b3119ca2f46651190e65144c", size = 3401504, upload-time = "2026-09-14T22:49:37.94Z" },
- { url = "https://files.pythonhosted.org/packages/0e/6c/e3721f102b8549831036465f368e7bf3e96dcfa9920ee36d9a69fba89e88/sqlalchemy-2.0.53-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6499513a0ca3984f7e4c4af22dbccc156eeda8846a4c4a2287a5b5e33b3544ff", size = 3432576, upload-time = "2026-09-14T23:07:00.982Z" },
- { url = "https://files.pythonhosted.org/packages/8c/13/9e5ab604abf0f8a229daa68b413ef717a76042db6b43ea0b3fbeddc3b045/sqlalchemy-2.0.53-cp312-cp312-win32.whl", hash = "sha256:dd915d37ae2442be08d6e1317814ac9be173341a1ad83ce133a77b4abba2d09d", size = 2140815, upload-time = "2026-09-14T23:03:36.688Z" },
- { url = "https://files.pythonhosted.org/packages/a2/de/0cd77e71f217cb6d849bd830b093710cf3cb2dc3a2d0af2361cf8d12b54a/sqlalchemy-2.0.53-cp312-cp312-win_amd64.whl", hash = "sha256:bb684d78694f77991cfc6fd8ba3bba093f06d7efbc354ea84b077a0d2116464d", size = 2169991, upload-time = "2026-09-14T23:03:38.168Z" },
- { url = "https://files.pythonhosted.org/packages/89/0e/394bbfadbbad7e427a86bc2f553eeea0bdd1f3f268aafed8cdf78af8cb41/sqlalchemy-2.0.53-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95b04f3113d8e02b711bbb3f6be5493ed94585b086b5b68571ea324e175022e5", size = 2184241, upload-time = "2026-09-14T22:43:17.736Z" },
- { url = "https://files.pythonhosted.org/packages/78/70/0ab026a838a0213b0b62521a49abbcd66d56d8e54ae8726d2aa268cbabb6/sqlalchemy-2.0.53-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acc9ee56130c32b609f562ac4041f77311aa6b3c51d507bd3ac93e280631eeed", size = 3398629, upload-time = "2026-09-14T22:49:39.568Z" },
- { url = "https://files.pythonhosted.org/packages/c8/4c/294eb6a87d5133874a04d2e7257fcf843176d6fbd1ba4ef5b3152fff861e/sqlalchemy-2.0.53-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06f3a9a4e82bf984d840c26dafd19688a4e2bfc7401d423cbec960ec737c610f", size = 3417528, upload-time = "2026-09-14T23:07:02.923Z" },
- { url = "https://files.pythonhosted.org/packages/0a/a3/ce985c9aee8e123174a022dd72aaa435b9c077a7d9f973d56ad2ad566638/sqlalchemy-2.0.53-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ff97dfff33e9c81e57be565ac58b9299affb566e10570cca668e4d67c69567a", size = 3349198, upload-time = "2026-09-14T22:49:41.2Z" },
- { url = "https://files.pythonhosted.org/packages/bf/bc/9dbae8af3dc6f14b14d08c3aeecbe1f3c5884ed1240f7509e8a2270297e2/sqlalchemy-2.0.53-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0cdefc36945decf40c199ee39f9df3f0765754b9625cf136118b04206854a05a", size = 3382852, upload-time = "2026-09-14T23:07:04.554Z" },
- { url = "https://files.pythonhosted.org/packages/29/04/228ab9a0115b8219c4af3071fead000b6c5c11696d2eae24163b6ff249d5/sqlalchemy-2.0.53-cp313-cp313-win32.whl", hash = "sha256:b06667e375da212f2ff4e7e54bf547ffeb63700c0743fc21c107f7d64f632922", size = 2138920, upload-time = "2026-09-14T23:03:39.638Z" },
- { url = "https://files.pythonhosted.org/packages/9e/7f/8b93de13745cc35540cbbd45e5d2247787fe9fec0ac7c9e64c7842f8f4dc/sqlalchemy-2.0.53-cp313-cp313-win_amd64.whl", hash = "sha256:88c296bd7b6e653d428aa0c97e806ac1e0ad25c385f4dab627b832ba46b7ad91", size = 2166486, upload-time = "2026-09-14T23:03:41.26Z" },
- { url = "https://files.pythonhosted.org/packages/bf/c4/164f0b33493a694a525e174d1ff978264167315d85f5128657d67522932a/sqlalchemy-2.0.53-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bc02d73633f5cee4ebecfc6c20b09a28bdfd940cc2ccf5bf119a6a7de6b5a3a3", size = 2185890, upload-time = "2026-09-14T22:43:19.088Z" },
- { url = "https://files.pythonhosted.org/packages/bf/85/15503b7c81567602a88a45ae14ecaef2f9372bf82e4cd2e3c7588e8cdea5/sqlalchemy-2.0.53-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b06e32d92ed898240793982820c46ae22bdec4f24dcb4ae4a6859001674db186", size = 3394772, upload-time = "2026-09-14T22:49:42.787Z" },
- { url = "https://files.pythonhosted.org/packages/fe/bb/a35e0ccb4f1d6f37b7b9d8810de787cbd429c427082e77e0ecdedf99b313/sqlalchemy-2.0.53-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03dfba40d1de2da62370d5e85ba13ffb8e75f16e8af9e93170a9cf4f394636a5", size = 3402720, upload-time = "2026-09-14T23:07:06.829Z" },
- { url = "https://files.pythonhosted.org/packages/40/06/adb43aa08a16e8abf55148a2efd17a66291a217c461455d7b4de053fd092/sqlalchemy-2.0.53-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:659daf25d61973ad6dff7bbfe9fccd116da2ab27618f57c3b9bfb97cb02000f5", size = 3341444, upload-time = "2026-09-14T22:49:44.316Z" },
- { url = "https://files.pythonhosted.org/packages/ee/07/15eaf0ae3573a9fb3ad0e5437f8cb36e03e415839ba6e77c645f555fb0b2/sqlalchemy-2.0.53-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:577195de5eccba4fea6c289e7537e4d309d9767d16b31afe82deda1f241feb80", size = 3373151, upload-time = "2026-09-14T23:07:09.063Z" },
- { url = "https://files.pythonhosted.org/packages/3a/c7/b5c078b0d36adb22cee1aca21e745e20e69705357c1f0dae2d8d728360fa/sqlalchemy-2.0.53-cp314-cp314-win32.whl", hash = "sha256:eeb39947f8d3a0531afdcb422f659e6ac09653b50813dbedc9baf84ce2358320", size = 2142012, upload-time = "2026-09-14T23:03:42.677Z" },
- { url = "https://files.pythonhosted.org/packages/59/4d/81b6bf1eaa969fc74b54104f6059d08f52383416654e8149b55ad168f2fb/sqlalchemy-2.0.53-cp314-cp314-win_amd64.whl", hash = "sha256:414a71d25b18cd3d1d5ab18b512a550a34398dcfbd68336adff5cd1e93fc7f0d", size = 2169212, upload-time = "2026-09-14T23:03:44.125Z" },
- { url = "https://files.pythonhosted.org/packages/eb/27/06028ccab7e03887c68c5031a8e4e7bfc85f30c87449c79dcba49e7ccd55/sqlalchemy-2.0.53-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41548248e8a5897d3a5f67ad8c1edb78efe72ec00dcc985b5c19b7850949d039", size = 2208469, upload-time = "2026-09-14T21:24:39.051Z" },
- { url = "https://files.pythonhosted.org/packages/fc/3c/6ee5fe746b0820335d30c5172527e9a2c7409ac632376d15fded38ff715c/sqlalchemy-2.0.53-py3-none-any.whl", hash = "sha256:87eb05fc61a0835dfe1cbf135d0f99cb2764ad09350d92fdd3fe1b827a557d65", size = 1958002, upload-time = "2026-09-14T22:40:04.431Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/29/9c/271aa905cf2964f841371a97f3e63ab692bf51b4423d0491e67bc7f64037/sqlalchemy-2.0.54.tar.gz", hash = "sha256:baa8521e8ee9f24e75dfc7aaabc08020e551ef0d48d7c3e3536f5cddf277586b", size = 9969559, upload-time = "2026-09-15T21:06:57.337Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/2f/7a427d7e10bc5f285d41fa1cadf46031ffc6c84f35a9d6e19737587a4eb0/sqlalchemy-2.0.54-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24ae093dec196ba37fc2beb0316de53e7871d3d246a50faecbbb53034e41ded2", size = 2189267, upload-time = "2026-09-15T22:11:14.024Z" },
+ { url = "https://files.pythonhosted.org/packages/84/1f/80122de187c0f80cb47addce97609160004a8605010afdd815e608fcf003/sqlalchemy-2.0.54-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8cc6532f930c27974e9239e5ce5abebe7600ba9807cea4fcf42f1b6cab18fe7", size = 3358362, upload-time = "2026-09-15T22:33:36.534Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/93/9d26b109f5472017bf6e7a1b9dac12ac5f6165ba37a297834d0690c42cf3/sqlalchemy-2.0.54-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e7a76d5dce712ce50435d0f97181eb955ec27d138c004176f01282e063bac52", size = 3357692, upload-time = "2026-09-15T22:33:14.781Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/d4/63e800b45bd4fa59cb4b14c013130e0aa92c7a019a0b8bf68115643bf32b/sqlalchemy-2.0.54-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f5c09090b1a7c4d389d1431f820931e8df318f82caafc53f9a72c872fef467c5", size = 3308258, upload-time = "2026-09-15T22:33:38.469Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/fe/6c96fe018b9db15476b46619228dc0749cdd9b4dc8baab89aca59205b605/sqlalchemy-2.0.54-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:762cfe4d340c56368256d936a98b620a9a5650e49c1c84eba51d6edd17ffefb2", size = 3336734, upload-time = "2026-09-15T22:33:16.533Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f1/8d225d6b77863ae83ba51e913fe5954bcf58b49dd54229f0b5ba0bc983f0/sqlalchemy-2.0.54-cp310-cp310-win32.whl", hash = "sha256:6b6d4e601c4f6d85e99bb3416107cc9418c5603ca73d4ee0f5f8d79c2a1ed9e8", size = 2144294, upload-time = "2026-09-15T22:41:20.424Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/f6/4247fdd087bf1b39be90930bb2b276a1c55041540628c8736512b4242669/sqlalchemy-2.0.54-cp310-cp310-win_amd64.whl", hash = "sha256:03cbf8d9a67da618bd65500a5eb3ddac89caf4c61e99b2f03fa4a1952a0725a9", size = 2170318, upload-time = "2026-09-15T22:41:21.973Z" },
+ { url = "https://files.pythonhosted.org/packages/76/bf/7339b18ef05c03335a32f077e450c8040eab0c952c2964e32cd889eafc22/sqlalchemy-2.0.54-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d03084f3352dd92048cb19c71d90f116d076c9c7937e0ebc7752c4685de6d38", size = 2187776, upload-time = "2026-09-15T22:32:35.535Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/bb/3b6f5f3f51582d63af4c88b1e70da7888e936dee6b86a237b7633fb3f216/sqlalchemy-2.0.54-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92622fbbda1b1fe1632f3402a6e516a93c0e41d9158839c6b3dfb12117f26b72", size = 3428952, upload-time = "2026-09-15T22:40:14.441Z" },
+ { url = "https://files.pythonhosted.org/packages/77/d5/ffd6b59795ebec81d582ad866f71ec7c73d356768e0442c6a07bba61c40c/sqlalchemy-2.0.54-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5800ddea045c2c860ef1d359a07a3066c7c0c426f45e3abc3874e116cb3c6937", size = 3428566, upload-time = "2026-09-15T22:35:33.739Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ff/1f0bffb0653b30711e2445a48968eff9412b4e5bf0870f1e72e8a69595ef/sqlalchemy-2.0.54-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019abef05a4b5eafc8eae6fb483167fa28a4dbe5f518d577b744f31a5276a37", size = 3377697, upload-time = "2026-09-15T22:40:16.494Z" },
+ { url = "https://files.pythonhosted.org/packages/97/76/774906be0d41cc4a14ecfbc015112fb7d0a3f1c3393f2c179d10388f8f0b/sqlalchemy-2.0.54-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b67749f7da3985a529cefbb1474783cb91ef44371cb9713630bade3de908760d", size = 3402966, upload-time = "2026-09-15T22:35:35.726Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/bb/1d7fe50ace612bf342d3440d8b09544746949120326c3dc487cfc2592fba/sqlalchemy-2.0.54-cp311-cp311-win32.whl", hash = "sha256:2f61a70b3b82e2ec7ad6a4f2301422b9ca93ff06917983e41317bcae878bddf6", size = 2142297, upload-time = "2026-09-15T21:25:19.917Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/49/18fe80f64b1f6bcd422e649e40a78031f8624b2fde22ca54e950dfa6e25a/sqlalchemy-2.0.54-cp311-cp311-win_amd64.whl", hash = "sha256:1d887fbd5d248e250807bd801e697fc73e3b44866ce5f093dbc90512e75bde25", size = 2169222, upload-time = "2026-09-15T21:25:21.114Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/30/75504fd1d70458000e85a3e772333dd0fbf80254b0dc4d41c99922e4d112/sqlalchemy-2.0.54-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffba7eb2d67c7505e82a0902aa854d8824b74c28a183820d6a8bd3cfd0f812c2", size = 2187596, upload-time = "2026-09-15T22:32:37.031Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/3d/5dfbb9528a391186a99986daecc5cbe003f34408dee91db8f5cdcf917040/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63cae7210fea9899e0bf35c1f1ae55d3ddd9c6d47cae8b6b43d945afa79dd65b", size = 3448745, upload-time = "2026-09-15T22:40:18.168Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/93/34fdc4a4faced77037a6b3ba1db1acd92e9bd12c21e229b327edd1d3e881/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68d994e9b0d0423a02a20039631fa6fcbb7fa829a992f7605025774940305d19", size = 3466644, upload-time = "2026-09-15T22:35:37.7Z" },
+ { url = "https://files.pythonhosted.org/packages/66/68/4beab40ae60ac3d679dbbb46bc0d2bb277013264a0bde37ecaf4b6780e98/sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3de32cc6721eb42c3aad35bcfb244bb7a18f66c00f3582aae6281d6287a339b5", size = 3401519, upload-time = "2026-09-15T22:40:19.907Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/df/a24757e3249b1c7c1c5f0317666d16a8ab901ba239819ecef29eb4ed172e/sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d31a2bc06a854ee52dd86b455be4df7c750b28817e2d1b884e31fff126c4fd7b", size = 3432590, upload-time = "2026-09-15T22:35:39.434Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/0c/69559e3d90d200fbfe9c44aff39a9ff4e506b1a3e9557a203d69df721244/sqlalchemy-2.0.54-cp312-cp312-win32.whl", hash = "sha256:32de6deded25e8b9b11d07428d496ff24dfbc882b8e990c177266948cb5f3d9e", size = 2140825, upload-time = "2026-09-15T21:25:22.444Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/10/4a0f7113664c2877906db143709c52ddbd28a88e4d51f0b520bc30048ba7/sqlalchemy-2.0.54-cp312-cp312-win_amd64.whl", hash = "sha256:d65f8ca742ef1e1e14bc417ef59dc2ddf207a7b66b30cfdc6152447314e030cf", size = 2170005, upload-time = "2026-09-15T21:25:23.885Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1c/021ccd7a838425f52e67c02edc541e8d53be8a27a103e684e14b18aac17b/sqlalchemy-2.0.54-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b374e3bc91e246a942592a98ba6a23be76fff21358b00546ac8c0ebc0fd0e00b", size = 2184259, upload-time = "2026-09-15T22:28:59.153Z" },
+ { url = "https://files.pythonhosted.org/packages/74/8f/f95de908a4af7ac3a6925cdcb837a93274f608925a0fbc786df38811bccf/sqlalchemy-2.0.54-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d5458672a6f72db2c087f4a5098b3c8503ea0254186ff29205d63afa9401a4", size = 3398792, upload-time = "2026-09-15T22:29:35.218Z" },
+ { url = "https://files.pythonhosted.org/packages/84/26/bd327a1a6be223e438c98ac8faf299307d83797225a81f3b209607a9fd98/sqlalchemy-2.0.54-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cad78d04254967bdbcccbed5e631d88fe4868530946ab0929aa45e9032849518", size = 3417562, upload-time = "2026-09-15T22:40:32.637Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/b1/f13a8fe8e8e167d8e65484b91b827897b8a5ca1efd7ba6e87a5f63412fed/sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:48611087a75d26d798003645c688c7d3cfc26b89dbe4a2c568d6b378d330deae", size = 3349138, upload-time = "2026-09-15T22:29:36.729Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/5fd58f03281a74c57f02f509433e1be58a79d9f5b0e14020e665e0dcb614/sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6adf80277372a89910a0f3ccfe960b846d279dc55b366dd5c5ec07f41c84758", size = 3382943, upload-time = "2026-09-15T22:40:34.587Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/73/e29fa88dfc809857a55e6523911d345d00cab4adf87032c21ade8874223c/sqlalchemy-2.0.54-cp313-cp313-win32.whl", hash = "sha256:264460333ed0b177cbb1956355d0ee4e0cab83fb415c934ce12a25db2e7be39c", size = 2138931, upload-time = "2026-09-15T22:42:51.801Z" },
+ { url = "https://files.pythonhosted.org/packages/89/9d/885e7491836f3dad368c74f0d5b93551d37b0ec66c9e8753240ee988308f/sqlalchemy-2.0.54-cp313-cp313-win_amd64.whl", hash = "sha256:cf89e92bf0d4204a6afcc17af27b9271ed9c7e34e17d6f80c085d431ea4a1747", size = 2166496, upload-time = "2026-09-15T22:42:53.603Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/c0/4a6503c9d22d6d00a5631082ab1484222ecf7d573db791e0f53161bf7745/sqlalchemy-2.0.54-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abd6b21bc58e91c1932eb5d6d7f1bd44a551dfec7b6a7f517c3638ccd67233a0", size = 2185899, upload-time = "2026-09-15T22:29:00.581Z" },
+ { url = "https://files.pythonhosted.org/packages/12/28/f4424f618bd1f373761a32a821d53ce2c257350e9894bae9b968cb03d8fd/sqlalchemy-2.0.54-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5417322b3c025dd82918725d3bf09ec105fac95efc195722b8b06e1d9c381139", size = 3394763, upload-time = "2026-09-15T22:29:38.131Z" },
+ { url = "https://files.pythonhosted.org/packages/59/d2/7f0c77f8e042cb5f28275fea29c3080b4ac6fd4b3fdd7f59ff1ef3e28c11/sqlalchemy-2.0.54-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f84099e4b04a5c2d44500a2a8302eee5af4bc6fee63e8c6e9cf6786e747280e", size = 3402800, upload-time = "2026-09-15T22:40:36.509Z" },
+ { url = "https://files.pythonhosted.org/packages/af/32/3eaa930bcf71d17a72587081d2706a5fb97ab3f11a7e0fb838f581f7cff1/sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0956dc754d3884da7fe60097110ec7a8a105d26afa2f0844468f4b1598c6912", size = 3341469, upload-time = "2026-09-15T22:29:39.682Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/cc/cddb6cbd4408e5c55b3bf722be26b9d3b54d42509f901b7ecc15debd3d1f/sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:87ba8834318b0d8dc94fc6f405d071b5c08be32a6c3fd68107fd6952ee949615", size = 3373232, upload-time = "2026-09-15T22:40:39.181Z" },
+ { url = "https://files.pythonhosted.org/packages/34/2f/9c2aa5efc642b7f3b985d13565cd1a5e78856e079ef3022796fea5180498/sqlalchemy-2.0.54-cp314-cp314-win32.whl", hash = "sha256:842540e4382472f23c79589995752648d14696a8200d0807ed8c5c59c92ade44", size = 2142018, upload-time = "2026-09-15T22:42:55.118Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/0b/3594f1f51769feb3022d686135dc5d8682a12345ed15ae61d0c0ca42cbee/sqlalchemy-2.0.54-cp314-cp314-win_amd64.whl", hash = "sha256:f4e8f955d13af83fb4e35c3472e5377ee22d3445eada1e5e48199588edb69835", size = 2169220, upload-time = "2026-09-15T22:42:56.727Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/a5/c211a9a7af83222509519407e16a4db760c6df3d03be69ebc5414d465321/sqlalchemy-2.0.54-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ca05f4e7852cf48083b0cf157e4f9504b7068780422a50fa82f45353b8c5e14a", size = 2208458, upload-time = "2026-09-15T22:30:05.718Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/2e/490ad7b3731116cb48ba170f7722eaa99a89707193e54389ca84b7ad55af/sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a8b6417cbb7b735cf91c2b59453c2a554cefa0a8d7bd15aa35740739410d77", size = 3660585, upload-time = "2026-09-15T22:36:06.649Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/25/15dfe6814847eeda773bd58ab6cf42a94b0176e5cf25a578fc1165777160/sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e55a0b96a1577a1e108c91ccdeeb9cd92768f28ce206597311c3bf6d6423abd", size = 3624442, upload-time = "2026-09-15T22:36:38.377Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/19/724d0a6a2fb2a86ff2d6008e581c258f722d9b7d8e52adc7b79085febdd4/sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:69cab115c40fd02c5a22c68e4ee630fa6ef9a1650f1de944419aab1f7096fc4f", size = 3562972, upload-time = "2026-09-15T22:36:08.581Z" },
+ { url = "https://files.pythonhosted.org/packages/49/bb/9df1bd81c2f2d000cf5e7a1b1a9b331468a3aab939ad983355e701fa42b2/sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e08397c6c42f53b2488acde9108b8bfefd52d7afd1bf2f03d2ffcab7a204aceb", size = 3576479, upload-time = "2026-09-15T22:36:40.272Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c0/b5775465d3b89061d7c46057c31c56ff8fb6c509550b2b0c6570ffc248b3/sqlalchemy-2.0.54-cp314-cp314t-win32.whl", hash = "sha256:b9086b8ad48280ef6a7ba68262d5e44f7db1c4cb1973e8cdae8a9f467ae66f51", size = 2174794, upload-time = "2026-09-15T22:31:45.925Z" },
+ { url = "https://files.pythonhosted.org/packages/77/f8/296c2e46b4ccd3f29b00b954ef2f195dde32f98e352ed21de1d292cedc0d/sqlalchemy-2.0.54-cp314-cp314t-win_amd64.whl", hash = "sha256:b67c1744e453af833667fc1b84de07adb4a64f3536ef52a8ec5ac2b941d43970", size = 2211942, upload-time = "2026-09-15T22:31:47.368Z" },
+ { url = "https://files.pythonhosted.org/packages/24/a1/bd5e3e99bc9c8863b51ac5b9b03008a7f2da8c6b59695992f5c654e1265b/sqlalchemy-2.0.54-py3-none-any.whl", hash = "sha256:7e33a631ab1474f8fe6b910bd1a07b7b8009c4c78cdd3fb18001b03e3bc2e1d2", size = 1958015, upload-time = "2026-09-15T22:24:22.95Z" },
]
[[package]]
@@ -5439,24 +5472,24 @@ wheels = [
[[package]]
name = "tornado"
-version = "6.5.8"
+version = "6.5.10"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/10/d3/343e5bb989d6515b1646cf3d40135d73f3d5e45339bded401b56cdac24dd/tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f", size = 520493, upload-time = "2026-08-07T02:12:42.971Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/06/61/53d562a57b28c08eda40b258c0f975e360541943ad7c7bef897a40caafda/tornado-6.5.10.tar.gz", hash = "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687", size = 537910, upload-time = "2026-09-15T13:47:48.73Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f2/d5/007086fd8df5489338e204f65adce33fd4f21a4999dbb2b9cff2f897b5f4/tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403", size = 449487, upload-time = "2026-08-07T02:12:28.682Z" },
- { url = "https://files.pythonhosted.org/packages/70/c8/5a24a99495903f594f6a199dd7beead1cbc0a13e2cb9102727bcaaf2a997/tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb", size = 447649, upload-time = "2026-08-07T02:12:30.306Z" },
- { url = "https://files.pythonhosted.org/packages/6e/de/f2e733f386b85962d1b1dc82cd63d169b5b4580062b35397eac9244a41fe/tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58", size = 450707, upload-time = "2026-08-07T02:12:31.95Z" },
- { url = "https://files.pythonhosted.org/packages/0b/94/20efeee9a01c141e9ac47c397f81679dfda24b32768fc4fff24e76d36c2c/tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808", size = 451677, upload-time = "2026-08-07T02:12:33.512Z" },
- { url = "https://files.pythonhosted.org/packages/42/ec/a96ccb8ccf0de2b7bc2c5fa1608a4803735018242e90c4882365a9fd418f/tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22", size = 451510, upload-time = "2026-08-07T02:12:35.346Z" },
- { url = "https://files.pythonhosted.org/packages/29/b5/93185859245ad3f00e62175f29607346788b696369347f0146e0421286bb/tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195", size = 450917, upload-time = "2026-08-07T02:12:36.963Z" },
- { url = "https://files.pythonhosted.org/packages/97/cf/fe33cf062834487d34d1559746a4a12521033c22645b6d74d4bca702e018/tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836", size = 451952, upload-time = "2026-08-07T02:12:38.512Z" },
- { url = "https://files.pythonhosted.org/packages/cb/e1/468ad54333e92ccb62627e62cb88e5fc14a2171daa67ed47b1b8542d5b86/tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee", size = 452391, upload-time = "2026-08-07T02:12:39.971Z" },
- { url = "https://files.pythonhosted.org/packages/ad/3e/cd5e4f06e34cde33b8ef66cf36aa2b5ad46354cc1af7d2136bbe365fee1d/tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26", size = 451411, upload-time = "2026-08-07T02:12:41.469Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/5b/ff5fc58fa2427c30dea74c90053f4fc5eda1e7f3833ed3ecc7147fe2b311/tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7", size = 465883, upload-time = "2026-09-15T13:47:35.463Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/f5/cd7be26c34a3315532f3aef5f092465da8f59c334dd439d3c14aaef16461/tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1", size = 464046, upload-time = "2026-09-15T13:47:37.178Z" },
+ { url = "https://files.pythonhosted.org/packages/60/33/df6d7d04854a58619f8349a51e3edb138324130a7562b0bb21f115bb940f/tornado-6.5.10-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d", size = 467096, upload-time = "2026-09-15T13:47:38.559Z" },
+ { url = "https://files.pythonhosted.org/packages/29/17/cc35dff68272d685cffd8600ffafbd8067e7d05e7348d9f80caddffbbd5f/tornado-6.5.10-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676", size = 468067, upload-time = "2026-09-15T13:47:40.085Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/01/6e5349b4e1a53a4b4972a6716785e1fe7407f312063c3972690af8ff301b/tornado-6.5.10-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015", size = 467901, upload-time = "2026-09-15T13:47:41.576Z" },
+ { url = "https://files.pythonhosted.org/packages/28/5e/b4facf94370dba006819c8d304376f8b9fbec6b935b5e51bf45823a9790b/tornado-6.5.10-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828", size = 467308, upload-time = "2026-09-15T13:47:43.145Z" },
+ { url = "https://files.pythonhosted.org/packages/56/ae/047938e828cafc8eca4c908fafb6588fee944e3af39a0af9d7b602499ae5/tornado-6.5.10-cp39-abi3-win32.whl", hash = "sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72", size = 468387, upload-time = "2026-09-15T13:47:44.556Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/d4/5901517f05affd752490f6a654ba31b7474664e8dd80bd045a00c220bd88/tornado-6.5.10-cp39-abi3-win_amd64.whl", hash = "sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918", size = 468828, upload-time = "2026-09-15T13:47:45.961Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1a/fd497f3a7f7b74bb04f4b94536b5c9f80742b5d50501fd27977652ddec16/tornado-6.5.10-cp39-abi3-win_arm64.whl", hash = "sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694", size = 467847, upload-time = "2026-09-15T13:47:47.283Z" },
]
[[package]]
name = "tox"
-version = "4.61.4"
+version = "4.64.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cachetools" },
@@ -5472,9 +5505,9 @@ dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "virtualenv" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/33/57/5a24f6f81ee81a25d34773045db61ddef3a76ed43e6fbd0fca2657678ad6/tox-4.61.4.tar.gz", hash = "sha256:d3fa82dc2fe847fe45b576ed0614e850a674aadec3a664f409e2bf295c473e20", size = 308140, upload-time = "2026-09-09T01:37:12.183Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/88/b0fff358c90fc49e0e619604d17b1edd26ab931853ed73a559f3b5bdf0c8/tox-4.64.1.tar.gz", hash = "sha256:d10dd88c32e9f15f0ec831f9ebacb9b253f671fd1287d49bead2852fc6225271", size = 314417, upload-time = "2026-09-24T01:25:03.947Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4e/ca/1c3c0e8c21caa393648ec6d62ee14aa802ea0d94553ceb2dd71f50b85a10/tox-4.61.4-py3-none-any.whl", hash = "sha256:1ec27a26f485c408a3f128722959086099894955d1eb74ab973e233bd362e99e", size = 228143, upload-time = "2026-09-09T01:37:10.621Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/80/ad62157d57ee9f2e1e05c456f2e6ac1294ddd1fa52913f37fc4355e66598/tox-4.64.1-py3-none-any.whl", hash = "sha256:3089cfb112f8c5a1fcc652b49e4c708bb00cbaa7c908078ebe470e08f112de4c", size = 231589, upload-time = "2026-09-24T01:25:02.206Z" },
]
[[package]]
@@ -5526,42 +5559,42 @@ wheels = [
[[package]]
name = "typedb-driver"
-version = "3.12.3"
+version = "3.13.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "parse" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/87/86/6a06a91bbf8275c1464d1c38407fc8e2ac09fb59fd54c0c8e390a583e6f6/typedb_driver-3.12.3-py310-none-macosx_11_0_arm64.whl", hash = "sha256:37ef25026d5a9bc217681efb8c77c11702df0012f713aa60beb777504e39c44f", size = 5601838, upload-time = "2026-08-11T08:58:22.824Z" },
- { url = "https://files.pythonhosted.org/packages/ab/c4/39017ecda70a7664764669e8b2cfaa67a9a9988cf517099a3f0ebe97b613/typedb_driver-3.12.3-py310-none-macosx_11_0_x86_64.whl", hash = "sha256:da9cc070c3f213a02750bfae5588cc48b2294fb5ff9cd7d5f50dc488bac56f41", size = 5729489, upload-time = "2026-08-11T09:01:41.63Z" },
- { url = "https://files.pythonhosted.org/packages/4e/d3/3af880d186e5c5bca81facaa56f06cd817dcf18b82f100fc82b80d547b52/typedb_driver-3.12.3-py310-none-manylinux_2_17_aarch64.whl", hash = "sha256:a7576dd2741360d11746c9b9bb083765e465102e6d934348e1821589100a1b7d", size = 6968847, upload-time = "2026-08-11T09:05:11.621Z" },
- { url = "https://files.pythonhosted.org/packages/f7/86/161974fbe894b82ef946c97c66f95d88603591b19ab1caebf10ea02e998a/typedb_driver-3.12.3-py310-none-manylinux_2_17_x86_64.whl", hash = "sha256:6f76472276e74ad800421025df470b8963d0ebdf87e9d089dbbf6b879516ae10", size = 7119907, upload-time = "2026-08-11T09:03:39.086Z" },
- { url = "https://files.pythonhosted.org/packages/8f/43/362749b34a1c6d068167ee28df75afa592a8d955334a3fe8e0494e5ea3ac/typedb_driver-3.12.3-py310-none-win_amd64.whl", hash = "sha256:dc75bf9313653c4d9f24de51c459af273080fab5233cbf6721d64e99d4107957", size = 3350376, upload-time = "2026-08-11T09:04:36.048Z" },
- { url = "https://files.pythonhosted.org/packages/23/73/bb01715ee44cddd5c89ae9be10518fd04fca9213f172d240fd7d1c5376bb/typedb_driver-3.12.3-py311-none-macosx_11_0_arm64.whl", hash = "sha256:7d9a4faf03a817b647b905e9728cbf6d4bd6ea995e3c69729ea5ef76bb4cb56b", size = 5601838, upload-time = "2026-08-11T08:58:34.817Z" },
- { url = "https://files.pythonhosted.org/packages/e4/0c/02ee9ef97619870d423383e19c98d075eb6afc1cf621ff29da065ed8dcf8/typedb_driver-3.12.3-py311-none-macosx_11_0_x86_64.whl", hash = "sha256:59a8108832350586af212f2a428ce5ab32757ead8aacfda0c96c1e425be76ac8", size = 5729487, upload-time = "2026-08-11T09:02:00.284Z" },
- { url = "https://files.pythonhosted.org/packages/fb/96/b50902092e1b2e2c444fd97ccbc185da049a71f9243495074cdf6d16c11a/typedb_driver-3.12.3-py311-none-manylinux_2_17_aarch64.whl", hash = "sha256:0a0bdebce80217a04d948fa7ad466d03dd42a0302dcdf5d2d996b4c2219a6977", size = 6969088, upload-time = "2026-08-11T09:05:39.067Z" },
- { url = "https://files.pythonhosted.org/packages/c7/c9/41483f33f1bb6418c87f921ae6338f85c98d1aa2a769dd379fb6c541198b/typedb_driver-3.12.3-py311-none-manylinux_2_17_x86_64.whl", hash = "sha256:3674b4308537c74e6db83797741f6e183e1cae54d6c3d8b370af47d8218a226c", size = 7120037, upload-time = "2026-08-11T09:04:01.788Z" },
- { url = "https://files.pythonhosted.org/packages/42/6b/ec6d8e3f7451b7167c16ded29cbe3fe7d30f79a6c5a54c3974854cc7322b/typedb_driver-3.12.3-py311-none-win_amd64.whl", hash = "sha256:c9462d8a1de395b1c150f81f5e406d58cbeaeb99a255c26148ab1897864b7a82", size = 3350597, upload-time = "2026-08-11T09:05:27.367Z" },
- { url = "https://files.pythonhosted.org/packages/71/8a/611a64b4397cb33c6f185c5613b03e1bcceefe015ead11a6b48260d906e8/typedb_driver-3.12.3-py312-none-macosx_11_0_arm64.whl", hash = "sha256:505905acab3f105b776882b0b9c98aac206ae1878dc6349a911676fb6bbfa917", size = 5602031, upload-time = "2026-08-11T08:58:46.516Z" },
- { url = "https://files.pythonhosted.org/packages/a5/cf/2bdafc70c080d8b56eef89cfb2cb0f93e183f011390bce045490bc209a6f/typedb_driver-3.12.3-py312-none-macosx_11_0_x86_64.whl", hash = "sha256:72d6d9e49f912bb179d7d422dce99b8445692700578ea14341884d864afa1c33", size = 5730506, upload-time = "2026-08-11T09:02:15.084Z" },
- { url = "https://files.pythonhosted.org/packages/eb/7a/fe6ba14d67f31c2f2d5c9018ec195d89523534db51fc2717dd72db88b2bd/typedb_driver-3.12.3-py312-none-manylinux_2_17_aarch64.whl", hash = "sha256:cc1235f98c21a90c514feeda6411eed206fa3ac0b5ec9305eb16cf712eb4b2a8", size = 6970153, upload-time = "2026-08-11T09:06:06.56Z" },
- { url = "https://files.pythonhosted.org/packages/79/ab/6e267e38c85ff37355babfa81f11d56da387e8c5e858e16299f814f84b94/typedb_driver-3.12.3-py312-none-manylinux_2_17_x86_64.whl", hash = "sha256:f581fb24f44eb1ad0f844059d4ab30039cfb4fb4d63270bf89a0e97914cc4dfc", size = 7120201, upload-time = "2026-08-11T09:04:25.329Z" },
- { url = "https://files.pythonhosted.org/packages/ca/0b/a67ad0540e990433623466c4f0aca074ecc980d13cbb26caeb3810e85fc7/typedb_driver-3.12.3-py312-none-win_amd64.whl", hash = "sha256:a68f3298dcc12404923b97e714ba8b0b63f5ae3fe5737d4b06e381e83136eea3", size = 3350900, upload-time = "2026-08-11T09:06:17.051Z" },
- { url = "https://files.pythonhosted.org/packages/01/38/0675edc133823dbe90fceb6e1f6d590dfb94450c12d9592a8cef83a650d3/typedb_driver-3.12.3-py313-none-macosx_11_0_arm64.whl", hash = "sha256:6d2c1ef1b3dbb9ecbc9d9d2f85dc1dddd76978b85d4d87173d5871733989e1be", size = 5602037, upload-time = "2026-08-11T08:58:58.862Z" },
- { url = "https://files.pythonhosted.org/packages/68/19/02a009221ca84aa6de3256849d9a796ed01efa93a4eb8485200c5afefe88/typedb_driver-3.12.3-py313-none-macosx_11_0_x86_64.whl", hash = "sha256:81ff8a85ba1f2d575dbbbf198baedee265e038c3cb9a0779f9652f2c7119db3b", size = 5730510, upload-time = "2026-08-11T09:02:30.111Z" },
- { url = "https://files.pythonhosted.org/packages/c5/25/5f4587311319a50c4c5e722e63666cea5864ae0d30f6c884e6b712db33ac/typedb_driver-3.12.3-py313-none-manylinux_2_17_aarch64.whl", hash = "sha256:ec6908ab04bc39ff678d36e8c28f0d753c50d0a331a649ae0c3559685c20f4f0", size = 6970018, upload-time = "2026-08-11T09:06:34.47Z" },
- { url = "https://files.pythonhosted.org/packages/f6/01/90da9a45b69626b8d64c92d30d313fe5afbef2942518f0da914166828ab8/typedb_driver-3.12.3-py313-none-manylinux_2_17_x86_64.whl", hash = "sha256:d64b72853679269ffa96f3c5da569c31a95fcb949ac8e76cab511a9be48207f7", size = 7120204, upload-time = "2026-08-11T09:04:48.381Z" },
- { url = "https://files.pythonhosted.org/packages/5c/a6/d53bdb3a6ae28287c89c52bfe1981bd7cf84a609245a45dbf071be234f09/typedb_driver-3.12.3-py313-none-win_amd64.whl", hash = "sha256:25520782db2ce568cd0f74b8f36e46ebfa10ef4327168f3c8a6b36fcb2d6f1a1", size = 3350230, upload-time = "2026-08-11T09:06:56.742Z" },
- { url = "https://files.pythonhosted.org/packages/a5/2a/40ba00b9c571c9c31b2a7a8aea156c7ac0c9765414e0b4fb0da7813adf74/typedb_driver-3.12.3-py314-none-macosx_11_0_arm64.whl", hash = "sha256:87114e0e4d359cf08da5578d2bd569b9ff58313b1fc65e72d01b4d40644d27e8", size = 5603549, upload-time = "2026-08-11T08:59:13.075Z" },
- { url = "https://files.pythonhosted.org/packages/ee/98/3af6649b5a36cec478f63e50ac6ab6570a224f7d7379c87c3ae5b13e97a4/typedb_driver-3.12.3-py314-none-macosx_11_0_x86_64.whl", hash = "sha256:ecd38f3b81ec2ef38b7d5573156589adb54795dfd14a803cfcdb9222948ce6eb", size = 5731523, upload-time = "2026-08-11T09:02:45.924Z" },
- { url = "https://files.pythonhosted.org/packages/15/ea/981a936fc8506391aac17e21ce58f268076d7463de8c9dfdf028f5242afb/typedb_driver-3.12.3-py314-none-manylinux_2_17_aarch64.whl", hash = "sha256:ad1fc0dd933db02099bfb63414261e3c6e18e7e852112aee4fafd0076092504a", size = 6969831, upload-time = "2026-08-11T09:07:01.773Z" },
- { url = "https://files.pythonhosted.org/packages/99/8c/f96314878ba41cee7f2b1e54cb64f3a0e5ec330b22f9dad863aa6abfdf72/typedb_driver-3.12.3-py314-none-manylinux_2_17_x86_64.whl", hash = "sha256:f71274506eede0ab68d08ee33c5107fb037ba6642ae3e41059716b7ffe8067b1", size = 7120043, upload-time = "2026-08-11T09:05:12.615Z" },
- { url = "https://files.pythonhosted.org/packages/03/fe/b9e6cdd148588d11224631d2e357c73b72f7c30a1e22c0b432c6e483523c/typedb_driver-3.12.3-py314-none-win_amd64.whl", hash = "sha256:b50edcad2ec49ec028118d1775bce9d0b46a2eb96466f157a6e5c5f3acd62657", size = 3350419, upload-time = "2026-08-11T09:07:35.935Z" },
- { url = "https://files.pythonhosted.org/packages/4b/36/5a884b90bab73a61c9d7db21cabbaa014ec02a954e8272ea95784a9e34c9/typedb_driver-3.12.3-py39-none-macosx_11_0_arm64.whl", hash = "sha256:f10e47044e1c81fde8b8e96cbf13cffcfa2460ee30f23249176de6f30d686de9", size = 5602214, upload-time = "2026-08-11T08:58:11.035Z" },
- { url = "https://files.pythonhosted.org/packages/a6/63/bc25d689c6097d5623dc8e297baaa732f0f683229524dca892e5ea3d186f/typedb_driver-3.12.3-py39-none-macosx_11_0_x86_64.whl", hash = "sha256:180784e84a5bed951838a2377bab388dc853ad7c01891ec17c1020b12f954375", size = 5729612, upload-time = "2026-08-11T09:01:23.733Z" },
- { url = "https://files.pythonhosted.org/packages/35/46/0fdc0db8901a6e590afb483ae1e9bd9a849f68b18b67b91ca0603ead745c/typedb_driver-3.12.3-py39-none-manylinux_2_17_aarch64.whl", hash = "sha256:725323fe85ec1d2930f872289bd3416f6b60c5afbb3a70f69c45b5728664edae", size = 6969263, upload-time = "2026-08-11T09:04:42.992Z" },
- { url = "https://files.pythonhosted.org/packages/49/61/076f5b11686a68250650927abe49ad5a1a12759087cfa531469b72b3b731/typedb_driver-3.12.3-py39-none-manylinux_2_17_x86_64.whl", hash = "sha256:8cc32c6f98effa751f77760b30caf959cbafa54722bc7d319fa467e6601144eb", size = 7119455, upload-time = "2026-08-11T09:03:16.082Z" },
- { url = "https://files.pythonhosted.org/packages/20/4e/583a2ecbaa11a3682532250fac91f0c71fec8ef3c5654020f704c9c6f436/typedb_driver-3.12.3-py39-none-win_amd64.whl", hash = "sha256:c413fb496aa64fbedf39d6528119134634f060e7bdc63eba94fad6f251da8f99", size = 3350179, upload-time = "2026-08-11T09:03:50.17Z" },
+ { url = "https://files.pythonhosted.org/packages/60/b2/e57f7e1e752c5f5175bd53080f60429a99b115bc06991a4205785312be54/typedb_driver-3.13.6-py310-none-macosx_11_0_arm64.whl", hash = "sha256:b807c67b5366956020add5b3aac06fca53fee6bf38b65c7b8b162a46765271df", size = 5609454, upload-time = "2026-09-21T19:29:15.196Z" },
+ { url = "https://files.pythonhosted.org/packages/08/25/80f2d6bd907f0e37f79b6edc16bbf1ca298d33af326457adf0d40334ad66/typedb_driver-3.13.6-py310-none-macosx_11_0_x86_64.whl", hash = "sha256:ec70a0843e0c2ffe8ce7c728ea4d44b31ad381dd2a4467a1df8e9243f58d6532", size = 5732150, upload-time = "2026-09-21T19:32:50.551Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ea/3b1414f3fc189ef1aa9703b48fc80c3ea5de8467013e15549bf9201dae0d/typedb_driver-3.13.6-py310-none-manylinux_2_17_aarch64.whl", hash = "sha256:c4ee34e0e1da020c36be4f6d211381004ee2104ebd6dc4699f1469f1a5906146", size = 6975141, upload-time = "2026-09-21T19:35:20.759Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/4a/b993ddacc369c31fc1d3291228029cb430914367d84e5441bb8b3a991e64/typedb_driver-3.13.6-py310-none-manylinux_2_17_x86_64.whl", hash = "sha256:1469a21652728e8269a4df6d1bdaa401711211597e9ab9109070332fb3922a9d", size = 7115364, upload-time = "2026-09-21T19:34:24.377Z" },
+ { url = "https://files.pythonhosted.org/packages/16/4d/62fad5c54005cf3cb7da0c786929dc4ed767e5e7844174836bc43cbb68e0/typedb_driver-3.13.6-py310-none-win_amd64.whl", hash = "sha256:952cccb36589894a6a3c165d6f1fd5cb138aa66331da62cf6049e491055b4040", size = 3349496, upload-time = "2026-09-21T19:35:03.223Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/7a/6852b88ece2962902b49209e08e019d3e79d9f385357dd77efcfedf274c8/typedb_driver-3.13.6-py311-none-macosx_11_0_arm64.whl", hash = "sha256:bc683e0688da2b32c3d29f175f79bbc68ffd05907337c9d6767315700f0235db", size = 5609459, upload-time = "2026-09-21T19:29:25.891Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/bc/c874b9053179cc97c0edb56dedda54ef43df0b3d2bc5ea5c51c843e78916/typedb_driver-3.13.6-py311-none-macosx_11_0_x86_64.whl", hash = "sha256:fecf7ead8be3443e46149a45dfdbba0e9a3ebd5564344cd0b52e542e41a526e3", size = 5732152, upload-time = "2026-09-21T19:33:06.313Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/cd/11df19fcda2978709be84d67438cebaf5c79e314ab749790e62a3fc32e56/typedb_driver-3.13.6-py311-none-manylinux_2_17_aarch64.whl", hash = "sha256:8a94a7fd660c58a8a1b8b58622a861a95442de1e82750db79c1cfbaaeffb1f3b", size = 6975387, upload-time = "2026-09-21T19:35:39.156Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/46/fa6fee0f4fc2cd17539a9522600809a35c4aec250ed50096281d6f81f878/typedb_driver-3.13.6-py311-none-manylinux_2_17_x86_64.whl", hash = "sha256:6e473226335b1f8d8fe0f34c8574d62967df2b08bf3c69badfcceec07d5367f7", size = 7115178, upload-time = "2026-09-21T19:34:41.474Z" },
+ { url = "https://files.pythonhosted.org/packages/59/d6/a9fe128fb7c85d1d26afc5e0af1d5910fa56775a4133eb3dcbd49a24a72a/typedb_driver-3.13.6-py311-none-win_amd64.whl", hash = "sha256:a9cbcaec71964d52991b86fdbcb11586f0435c4c064b047bd8fd58021ce5c0e2", size = 3349396, upload-time = "2026-09-21T19:35:24.822Z" },
+ { url = "https://files.pythonhosted.org/packages/52/20/9498738b2247a3de55c870b850703a74a3fcfdad4c8a4a61a9ee58dc15ff/typedb_driver-3.13.6-py312-none-macosx_11_0_arm64.whl", hash = "sha256:e106def5b6b92b2b5c0fbf69f3c238d146f207208dcf85fd88320a1ce9505b4c", size = 5608252, upload-time = "2026-09-21T19:29:36.699Z" },
+ { url = "https://files.pythonhosted.org/packages/da/32/a2549807205c84c433181d595c56e4c816e43faed17ce849e052a06634d8/typedb_driver-3.13.6-py312-none-macosx_11_0_x86_64.whl", hash = "sha256:a23bfa6f98eb5c7843b5f3192278849396efd087e9bed7cedc06066bdb3441e7", size = 5733740, upload-time = "2026-09-21T19:33:22.47Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/5e/b81eb9d5bbc5049e3ebe0f4d53c7d0e6c988e1fb752066bbead7aa8a85d8/typedb_driver-3.13.6-py312-none-manylinux_2_17_aarch64.whl", hash = "sha256:48a1f4cc62b59efda52a6db878ae64177c83c8c43666283b80426779745e1969", size = 6976437, upload-time = "2026-09-21T19:35:57.956Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/57/8be5b574db95a77c9b2e182c9991df29c6ea02e3bbd5cd9f405838247622/typedb_driver-3.13.6-py312-none-manylinux_2_17_x86_64.whl", hash = "sha256:e973362490970b82c04b5239f96ea1689440f2d7ca6e5d28986944af266e982c", size = 7115516, upload-time = "2026-09-21T19:35:00.601Z" },
+ { url = "https://files.pythonhosted.org/packages/86/76/8d8bd4385521f9db08d42b1a52373c5cd31ec99f979535e99390ec6626c3/typedb_driver-3.13.6-py312-none-win_amd64.whl", hash = "sha256:07add5e3fe7a1962c5c45b9805b1556422da1330e62fbe1434d6d5e99b0f4b7c", size = 3350079, upload-time = "2026-09-21T19:35:54.05Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ec/3f82d9320cab8259b77752e012760c3cab4a1e4530c131728e1f7445f6f8/typedb_driver-3.13.6-py313-none-macosx_11_0_arm64.whl", hash = "sha256:f3d6916a11594e2e113b3c129a5caf25dbb93e4df1324081cc982820416c086b", size = 5608256, upload-time = "2026-09-21T19:29:47.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/aa/3213db200a4c0b0917abacd3fb9a15cfa0caa67799f5fd53d7479db487d3/typedb_driver-3.13.6-py313-none-macosx_11_0_x86_64.whl", hash = "sha256:74057c4e52f7e69d3b51b878e503dfec296c4fb855d1365c553960ca08a36eba", size = 5733736, upload-time = "2026-09-21T19:33:41.716Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/65/bc749fd845d2b3fef4a817bb83e6401adb9c706d27c2818225b347721e08/typedb_driver-3.13.6-py313-none-manylinux_2_17_aarch64.whl", hash = "sha256:f1e974592e52851a1a4e6ecd8c02c08bde7687c05cab867d4ca2d49a57e150a0", size = 6976507, upload-time = "2026-09-21T19:36:17.474Z" },
+ { url = "https://files.pythonhosted.org/packages/98/fc/e1ac50c83353d5fd09fe9d203abefed28739820ae409dbb6356ae6f6a626/typedb_driver-3.13.6-py313-none-manylinux_2_17_x86_64.whl", hash = "sha256:01bca56e46ad680e93be6921744c27c48b718b09c55240c45af42826e7d6c0e9", size = 7115509, upload-time = "2026-09-21T19:35:18.668Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/cd/c046a30b8ac602604fff33b92e1591140bcf4ccc734ed92f22f9bf8522b8/typedb_driver-3.13.6-py313-none-win_amd64.whl", hash = "sha256:53bf76070810c5f5a7befe2a56788ed634e8bcf148e32720a80a76f790738a1c", size = 3349438, upload-time = "2026-09-21T19:36:43.329Z" },
+ { url = "https://files.pythonhosted.org/packages/03/47/a3ba965326e282fe9a4052564ac0a98c2f6ce047b860ae0ed13d5693688e/typedb_driver-3.13.6-py314-none-macosx_11_0_arm64.whl", hash = "sha256:64f66837055646c6c4f306cb43e5e89d8665089567fb6e696f5a503982f948a1", size = 5609292, upload-time = "2026-09-21T19:29:58.459Z" },
+ { url = "https://files.pythonhosted.org/packages/79/da/1d7161dc83186ef6862791a821764cd7889b42feba4254f4b448823cde90/typedb_driver-3.13.6-py314-none-macosx_11_0_x86_64.whl", hash = "sha256:6dbf9769ea7182824de376968bd0822968bd9d4a4c70ff15d4c8aad5a6e56614", size = 5734850, upload-time = "2026-09-21T19:33:59.161Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/92/6f72b686cfc7191520f6ba850e6fda98f6b4531fed230b69102562d4c282/typedb_driver-3.13.6-py314-none-manylinux_2_17_aarch64.whl", hash = "sha256:2f6a2ada1f6822c8d083aeccb616aa07fc250d54bed62e9d0d3229cbc5ac12a8", size = 6976125, upload-time = "2026-09-21T19:36:36.316Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/dc/d43d68b12088e59af16c5dca45818fd4fca7e1646ce598a38e70b3b4c13a/typedb_driver-3.13.6-py314-none-manylinux_2_17_x86_64.whl", hash = "sha256:7bde2aeeb31815074cc92a4d8852c12d32e8c5d90a2bac5329998ee70fccbcd9", size = 7115518, upload-time = "2026-09-21T19:35:35.615Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/94/0de1fb67ab1c22bfbd951860f142a147e955cdcd768ba3a68ce96a751961/typedb_driver-3.13.6-py314-none-win_amd64.whl", hash = "sha256:3e19b9a22a65ef375ea5c034cc27b3d6895483fd556e02fbc488d90101170ad2", size = 3349125, upload-time = "2026-09-21T19:37:21.029Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/c6/e3933b588cc76348d52594e1bf42d893b8db6afcf33f1aa5d66cabbed9ed/typedb_driver-3.13.6-py39-none-macosx_11_0_arm64.whl", hash = "sha256:93b4a46d62fee89ee01cf1ba67a5660bcc3b0034d45345cac92157969ff36feb", size = 5609414, upload-time = "2026-09-21T19:29:02.088Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/40/57865b73b8adfa5d254349fb5536aaaa3f2303e774e85135b3dd4753ab1f/typedb_driver-3.13.6-py39-none-macosx_11_0_x86_64.whl", hash = "sha256:235e23be393b7b8438da40a1debd278924a4c949942f81638254a6c500672fa5", size = 5732261, upload-time = "2026-09-21T19:32:28.154Z" },
+ { url = "https://files.pythonhosted.org/packages/05/ee/f414294cf5d1b640a62beef9c5f4f44e0ee75c2a92919727740319633917/typedb_driver-3.13.6-py39-none-manylinux_2_17_aarch64.whl", hash = "sha256:bc6468202f778700f76dc958c9811b7f791ae51607395c21e1fc72d44db67fed", size = 6975077, upload-time = "2026-09-21T19:35:00.458Z" },
+ { url = "https://files.pythonhosted.org/packages/26/c4/6ef6ffc56efbbf139da7e54550187f728b0d37a3594ffb724be1efde667a/typedb_driver-3.13.6-py39-none-manylinux_2_17_x86_64.whl", hash = "sha256:82ad7bb2858821bfd5c559e530e6c2bff566b0d54d6c7dbf5b5b5eca326ef0f1", size = 7115763, upload-time = "2026-09-21T19:34:07.717Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/bc/e92b8eb0d046b998f11cb101e82df33ac1bcbbb4d9f5349743d56efebb34/typedb_driver-3.13.6-py39-none-win_amd64.whl", hash = "sha256:f6675e5f89307bdae5ccdabe3513860e3ac1a51901db4e5d125323fb8bd0ae90", size = 3349307, upload-time = "2026-09-21T19:34:32.772Z" },
]
[[package]]
@@ -5642,11 +5675,11 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.7.0"
+version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
]
[[package]]
@@ -5677,7 +5710,7 @@ wheels = [
[[package]]
name = "virtualenv"
-version = "21.5.1"
+version = "21.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
@@ -5686,9 +5719,9 @@ dependencies = [
{ name = "python-discovery" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ad/3b/02608bb39c6b6f7d59ca62b04d704cf61e85361a7123b6394bca275661e1/virtualenv-21.12.1.tar.gz", hash = "sha256:be5a0a62cb2d1529ff6999652e2e7826f95bf7faa9b96b88cc39847c023d90a0", size = 5397187, upload-time = "2026-09-24T19:03:48.546Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/8c/692cf235722ee45c612630d2b49b3d1765892f7e440055365ea1b59c659e/virtualenv-21.12.1-py3-none-any.whl", hash = "sha256:878db9e963d0ef0b38561a344a4f9b12dc5c7741b6245d7ed10e9291cfea28bc", size = 5486096, upload-time = "2026-09-24T19:03:45.985Z" },
]
[[package]]