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/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/generators/shacl.rst b/docs/generators/shacl.rst index 3e88f0090f..ef88e0a0c7 100644 --- a/docs/generators/shacl.rst +++ b/docs/generators/shacl.rst @@ -84,6 +84,88 @@ Example Output: shacl:targetClass . +Class Expressions +^^^^^^^^^^^^^^^^^ + +Class-level boolean expressions become the SHACL logical constraint components +their metamodel definitions map to (`SHACL §4.6 +`__): + +================== ===================================================== +LinkML SHACL, on the class's ``sh:NodeShape`` +================== ===================================================== +``any_of`` ``sh:or`` over the member shapes +``all_of`` ``sh:and`` over the member shapes +``exactly_one_of`` ``sh:xone`` over the member shapes +``none_of`` one ``sh:not`` per member +================== ===================================================== + +Each member becomes an anonymous node shape. ``is_a`` gives ``sh:class``, and +nested expressions recurse. Each entry of ``slot_conditions`` gives an +``sh:property`` whose path is that of the slot as induced for the class, so +``slot_usage`` applies: + +* ``required``, ``value_presence`` and the cardinalities give ``sh:minCount`` / + ``sh:maxCount``; +* ``minimum_value`` / ``maximum_value`` give ``sh:minInclusive`` / + ``sh:maxInclusive``, and ``equals_number`` gives both, so that ``5`` also + matches ``5.0``; +* ``pattern`` gives ``sh:pattern``; +* ``equals_string`` and ``equals_string_in`` give ``sh:in``; on an enum slot the + values are the permissible values as the enum renders them, the IRI of their + ``meaning`` where they have one; +* ``range`` gives the same class, type or enum constraint as a slot's range. + +SHACL allows ``sh:minInclusive``, ``sh:maxInclusive``, ``sh:in`` and +``sh:pattern`` at most once per shape. Where one condition needs one of them +twice, for example ``minimum_value`` next to ``equals_number``, the second value +goes into an ``sh:and`` member of the property shape, where it applies to the +same values. + +A slot condition constrains only the values that are present, so it also holds +when the slot is absent - unless ``required: true``, ``value_presence: PRESENT`` +or a minimum or exact cardinality of at least 1 requires the slot. Inside +``none_of``, at any depth, a condition that constrains values requires the slot, +so that an absent slot is not rejected by the negation - unless the condition +decides presence itself, through ``required``, ``value_presence`` or a maximum +or exact cardinality of 0. The JSON Schema generator requires the slot in a +class's own ``none_of`` for every condition that sets neither ``required`` nor +``value_presence``. + +.. code-block:: yaml + + GeodeticReferenceSystem: + slots: [code, name] + any_of: + - slot_conditions: + code: + required: true + - slot_conditions: + name: + required: true + +.. code-block:: turtle + + ex:GeodeticReferenceSystem a sh:NodeShape ; + sh:or ( [ sh:property [ sh:path ex:code ; sh:minCount 1 ] ] + [ sh:property [ sh:path ex:name ; sh:minCount 1 ] ] ) ; + ... + +An expression is attached to the shape of the class that declares it. Like +every ``sh:targetClass``, it reaches instances of subclasses where the data +graph states the ``rdfs:subClassOf`` (`SHACL §2.1.3.2 +`__); the ``sh:class`` that ``is_a`` +gives recognises instances of subclasses the same way, as it does for a slot's +range. + +An operator whose members use anything else is skipped as a whole and logged as +a warning, because leaving out one member would change what the operator +admits. That covers, for example, ``has_member`` or a slot-level ``any_of`` +inside a slot condition, a condition on a name that is not a slot, a condition +on the identifier slot (the node's IRI rather than a property), and +``equals_string`` on a slot whose range does not hold strings. + + Command Line ^^^^^^^^^^^^ 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..298f6244dc 100644 --- a/packages/linkml/pyproject.toml +++ b/packages/linkml/pyproject.toml @@ -59,13 +59,13 @@ 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", ] [dependency-groups] @@ -97,7 +97,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 +114,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..7ba15df672 100644 --- a/packages/linkml/src/linkml/generators/owlgen.py +++ b/packages/linkml/src/linkml/generators/owlgen.py @@ -1556,7 +1556,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)) diff --git a/packages/linkml/src/linkml/generators/shaclgen.py b/packages/linkml/src/linkml/generators/shaclgen.py index 4731b9f0b8..bb3bc14324 100644 --- a/packages/linkml/src/linkml/generators/shaclgen.py +++ b/packages/linkml/src/linkml/generators/shaclgen.py @@ -2,7 +2,7 @@ import os import string from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import dataclass, fields import click from jsonasobj2 import JsonObj, as_dict @@ -16,7 +16,14 @@ from linkml.generators.shacl.shacl_ifabsent_processor import ShaclIfAbsentProcessor from linkml.utils.generator import Generator, shared_arguments from linkml.utils.language_tags import LanguageTagResolver -from linkml_runtime.linkml_model.meta import ClassDefinition, ElementName, PresenceEnum +from linkml_runtime.linkml_model.meta import ( + AnonymousClassExpression, + ClassDefinition, + CommonMetadata, + ElementName, + PresenceEnum, + SlotDefinition, +) from linkml_runtime.utils.formatutils import underscore from linkml_runtime.utils.rdf_canonicalize import canonicalize_rdf_graph from linkml_runtime.utils.yamlutils import TypedNode, extended_float, extended_int, extended_str @@ -248,12 +255,7 @@ def shape_pv(p, v): self._add_annotations(shape_pv, c) order = 0 for s in sv.class_induced_slots(c.name): - # fixed in linkml-runtime 1.1.3 - if s.name in sv.element_by_schema_map(): - slot_uri = URIRef(sv.get_uri(s, expand=True)) - else: - pfx = sv.schema.default_prefix - slot_uri = URIRef(sv.expand_curie(f"{pfx}:{underscore(s.name)}")) + slot_uri = self._slot_path(s) pnode = BNode() shape_pv(SH.property, pnode) @@ -371,21 +373,7 @@ def st_node_pv(p, v): f" require range 'string' and not '{r}'" ) - if r in all_classes: - cls_def = sv.get_class(r) - is_any = cls_def and getattr(cls_def, "class_uri", None) == "linkml:Any" - self._add_class(prop_pv, r) - if not is_any: - if sv.get_identifier_slot(r) is not None: - prop_pv(SH.nodeKind, SH.IRI) - else: - prop_pv(SH.nodeKind, SH.BlankNodeOrIRI) - elif r in sv.all_types(): - self._add_type(prop_pv, r) - elif r in sv.all_enums(): - self._add_enum(g, prop_pv, r) - else: - add_simple_data_type(prop_pv, r) + self._add_range(g, prop_pv, r) if s.pattern: prop_pv(SH.pattern, Literal(s.pattern)) if s.equals_string: @@ -405,6 +393,8 @@ def st_node_pv(p, v): if default_value: prop_pv(SH.defaultValue, default_value) + self._add_class_expressions(g, class_uri_with_suffix, c) + if self.emit_rules: self._add_rules(g, class_uri_with_suffix, c) @@ -412,6 +402,310 @@ def st_node_pv(p, v): LINKML_ANY_URI = "https://w3id.org/linkml/Any" + # ------------------------------------------------------------------- + # Class expressions → sh:or / sh:and / sh:xone / sh:not + # ------------------------------------------------------------------- + + # (metamodel operator, SHACL logical constraint component taking a list) + _LIST_OPERATORS = ( + ("any_of", SH["or"]), + ("all_of", SH["and"]), + ("exactly_one_of", SH.xone), + ) + _CLASS_EXPRESSION_OPERATORS = ("any_of", "all_of", "exactly_one_of", "none_of") + + # The fields of an anonymous class expression that carry meaning; every other + # field is common metadata. Derived from the metamodel rather than listed, so + # a new semantic field is reported as untranslatable instead of being ignored. + _CLASS_EXPRESSION_FIELDS = frozenset({"is_a", "slot_conditions", *_CLASS_EXPRESSION_OPERATORS}) + _METADATA_FIELDS = frozenset(f.name for f in fields(CommonMetadata)) | {"extensions", "annotations"} + + # Slot-condition fields translated by _slot_condition_shape. + _SLOT_CONDITION_FIELDS = frozenset( + { + "required", + "value_presence", + "minimum_cardinality", + "maximum_cardinality", + "exact_cardinality", + "minimum_value", + "maximum_value", + "pattern", + "equals_string", + "equals_string_in", + "equals_number", + "range", + } + ) + # Bookkeeping a loader may fill in on a slot condition; none of it constrains values. + _SLOT_CONDITION_BOOKKEEPING = frozenset( + {"name", "definition_uri", "owner", "domain_of", "is_usage_slot", "usage_slot_name"} + ) + # Slot-condition fields that constrain the slot's values, as opposed to its presence. + _SLOT_CONDITION_VALUE_FIELDS = frozenset( + {"minimum_value", "maximum_value", "pattern", "equals_string", "equals_string_in", "equals_number", "range"} + ) + # Parameters SHACL allows at most once per shape (SHACL §4); a condition that needs + # one of them twice gets the second value in an sh:and member. + _SINGLE_VALUE_PARAMETERS = frozenset( + {SH.minInclusive, SH.maxInclusive, SH["in"], SH.pattern, SH.datatype, SH.nodeKind, SH.hasValue} + ) + + def _add_class_expressions(self, g: Graph, shape_uri: URIRef, cls: ClassDefinition) -> None: + """Emit the class-level boolean expressions of *cls* as SHACL logical constraints. + + Each operator is mapped to the SHACL logical constraint component with the + same semantics (`SHACL §4.6 `_): + + * ``any_of`` → ``sh:or``, ``all_of`` → ``sh:and``, ``exactly_one_of`` → + ``sh:xone``, each over a list of the member shapes; + * ``none_of`` → one ``sh:not`` per member. A shape's values of ``sh:not`` + are separate constraints that all apply (SHACL §2.1.1), so the node must + conform to none of the members. + + Every member becomes an anonymous node shape: ``is_a`` gives ``sh:class``, + each slot condition gives an ``sh:property`` on the path of the slot as + induced for *cls* (so ``slot_usage`` applies), and nested expressions + recurse. A slot condition constrains the values that are present; only + ``required: true``, ``value_presence: PRESENT`` and a minimum or exact + cardinality of at least 1 require the slot to be present. Inside + ``none_of``, at any depth, a condition that constrains values requires the + slot, so that an absent slot does not satisfy the member vacuously and so + get rejected by the negation - unless the condition decides presence + itself, through ``required``, ``value_presence`` or a maximum or exact + cardinality of 0. The JSON Schema generator requires the slot in a class's + own ``none_of`` for every condition that sets neither ``required`` nor + ``value_presence``. + + An operator whose members use anything that cannot be translated is + skipped as a whole, with a warning: dropping one member would change what + the operator admits. + """ + for operator in self._CLASS_EXPRESSION_OPERATORS: + members = getattr(cls, operator, None) or [] + if not members: + continue + reason = next(filter(None, (self._untranslatable(cls, m) for m in members)), None) + if reason is not None: + logger.warning( + "Class %r: %s is not translated to SHACL, because it uses %s.", cls.name, operator, reason + ) + continue + self._add_logical_constraint(g, shape_uri, cls, operator, members, presence_required=False) + + def _add_logical_constraint( + self, + g: Graph, + subject: URIRef | BNode, + cls: ClassDefinition, + operator: str, + members: list[AnonymousClassExpression], + presence_required: bool, + ) -> None: + if operator == "none_of": + for member in members: + g.add((subject, SH["not"], self._class_expression_shape(g, cls, member, presence_required=True))) + return + predicate = dict(self._LIST_OPERATORS)[operator] + shapes = [self._class_expression_shape(g, cls, m, presence_required) for m in members] + list_node = BNode() + Collection(g, list_node, shapes) + g.add((subject, predicate, list_node)) + + def _class_expression_shape( + self, g: Graph, cls: ClassDefinition, expr: AnonymousClassExpression, presence_required: bool + ) -> BNode: + """Build the anonymous node shape for one class expression *expr*.""" + node = BNode() + + def node_pv(p, v): + if v is not None: + g.add((node, p, v)) + + if expr.title is not None: + node_pv(RDFS.label, Literal(expr.title, lang=self._resolve_language(expr))) + if expr.description is not None: + node_pv(RDFS.comment, Literal(expr.description, lang=self._resolve_language(expr))) + if expr.is_a is not None: + self._add_class(node_pv, expr.is_a) + for slot_name, condition in expr.slot_conditions.items(): + node_pv(SH.property, self._slot_condition_shape(g, cls, slot_name, condition, presence_required)) + for operator in self._CLASS_EXPRESSION_OPERATORS: + members = getattr(expr, operator) or [] + if members: + self._add_logical_constraint(g, node, cls, operator, members, presence_required) + return node + + def _slot_condition_shape( + self, g: Graph, cls: ClassDefinition, slot_name: str, condition: SlotDefinition, presence_required: bool + ) -> BNode: + """Build the property shape for the condition on *slot_name*.""" + slot = self._condition_slot(cls, slot_name) + pnode = BNode() + repeated = [] + + def prop_pv(p, v): + if v is None: + return + if p in self._SINGLE_VALUE_PARAMETERS and (pnode, p, None) in g: + repeated.append((p, v)) + else: + g.add((pnode, p, v)) + + prop_pv(SH.path, self._slot_path(slot)) + if condition.title is not None: + prop_pv(SH.name, Literal(condition.title, lang=self._resolve_language(condition))) + if condition.description is not None: + prop_pv(SH.description, Literal(condition.description, lang=self._resolve_language(condition))) + + # value_presence takes precedence over required, as in the JSON Schema generator. + min_counts, max_counts = [], [] + if condition.value_presence is not None: + if condition.value_presence == PresenceEnum(PresenceEnum.PRESENT): + min_counts.append(1) + elif condition.value_presence == PresenceEnum(PresenceEnum.ABSENT): + max_counts.append(0) + elif condition.required: + min_counts.append(1) + if presence_required and self._condition_needs_presence(condition): + min_counts.append(1) + for bound, target in ( + (condition.minimum_cardinality, min_counts), + (condition.exact_cardinality, min_counts), + (condition.maximum_cardinality, max_counts), + (condition.exact_cardinality, max_counts), + ): + if bound is not None: + target.append(int(bound)) + if min_counts: + prop_pv(SH.minCount, Literal(max(min_counts))) + if max_counts: + prop_pv(SH.maxCount, Literal(min(max_counts))) + + if condition.minimum_value is not None: + prop_pv(SH.minInclusive, Literal(condition.minimum_value)) + if condition.maximum_value is not None: + prop_pv(SH.maxInclusive, Literal(condition.maximum_value)) + if condition.pattern is not None: + prop_pv(SH.pattern, Literal(condition.pattern)) + value_range = condition.range or slot.range + for values in ( + [condition.equals_string] if condition.equals_string is not None else [], + condition.equals_string_in, + ): + if values: + in_node = BNode() + Collection(g, in_node, self._string_value_terms(value_range, values)) + prop_pv(SH["in"], in_node) + if condition.equals_number is not None: + # A value comparison, unlike the slot loop's sh:hasValue: 5 matches 5.0, + # and like every other value constraint in a condition it holds when the + # slot is absent. + prop_pv(SH.minInclusive, Literal(condition.equals_number)) + prop_pv(SH.maxInclusive, Literal(condition.equals_number)) + if condition.range is not None: + self._add_range(g, prop_pv, condition.range) + if repeated: + # Each repeated parameter in a member shape of its own: all of them hold + # for every value, as they would on the property shape itself. + members = [] + for p, v in repeated: + member = BNode() + g.add((member, p, v)) + members.append(member) + and_node = BNode() + Collection(g, and_node, members) + g.add((pnode, SH["and"], and_node)) + return pnode + + def _condition_needs_presence(self, condition: SlotDefinition) -> bool: + """Whether a condition inside ``none_of`` must require its slot. + + Only a value constraint holds vacuously for an absent slot, so only a + condition with one needs it; one that decides presence itself does not. + """ + if condition.required is not None or condition.value_presence is not None: + return False + if 0 in (condition.maximum_cardinality, condition.exact_cardinality): + return False + return any(getattr(condition, f) not in (None, []) for f in self._SLOT_CONDITION_VALUE_FIELDS) + + def _string_value_terms(self, r: ElementName | None, values: list[str]) -> list: + """The RDF terms of the ``equals_string`` / ``equals_string_in`` *values* of a slot with range *r*. + + Permissible values of an enum are rendered as :meth:`_add_enum` renders them, as + the IRI of their ``meaning`` where they have one; anything else is a plain literal. + """ + sv = self.schemaview + if r in sv.all_enums(): + pvs = sv.get_enum(r).permissible_values + return [ + URIRef(sv.expand_curie(pvs[v].meaning)) if v in pvs and pvs[v].meaning else Literal(v) for v in values + ] + return [Literal(v) for v in values] + + def _condition_slot(self, cls: ClassDefinition, slot_name: str) -> SlotDefinition | None: + """The slot a condition of *cls* names, as induced for *cls*, or ``None`` if there is none.""" + try: + return self.schemaview.induced_slot(slot_name, cls.name) + except ValueError: + return None + + def _is_string_range(self, r: ElementName | None) -> bool: + """Whether a slot with range *r* holds strings, which ``equals_string`` compares against.""" + sv = self.schemaview + if r is None or r in sv.all_enums(): + return True + if r in sv.all_types(): + return sv.get_uri(sv.induced_type(r), expand=True) == str(XSD.string) + return r == "string" + + def _untranslatable(self, cls: ClassDefinition, expr: AnonymousClassExpression) -> str | None: + """Return what in class expression *expr* of *cls* cannot be translated, or ``None``.""" + sv = self.schemaview + unknown = _set_fields(expr) - self._CLASS_EXPRESSION_FIELDS - self._METADATA_FIELDS + if unknown: + return f"'{sorted(unknown)[0]}'" + if expr.is_a is not None and expr.is_a not in sv.all_classes(): + return f"is_a '{expr.is_a}', which is not a class" + for slot_name, condition in expr.slot_conditions.items(): + unknown = ( + _set_fields(condition) + - self._SLOT_CONDITION_FIELDS + - self._METADATA_FIELDS + - self._SLOT_CONDITION_BOOKKEEPING + ) + if unknown: + return f"'{sorted(unknown)[0]}' in the condition on slot '{slot_name}'" + slot = self._condition_slot(cls, slot_name) + if slot is None: + return f"a condition on '{slot_name}', which is not a slot" + if slot.identifier: + # An identifier is the node's IRI, not a property arc. + return f"a condition on the identifier slot '{slot_name}'" + if condition.range is not None and not self._is_known_range(condition.range): + return f"the unknown range '{condition.range}' in the condition on slot '{slot_name}'" + value_range = condition.range or slot.range + if (condition.equals_string is not None or condition.equals_string_in) and not self._is_string_range( + value_range + ): + return f"equals_string on slot '{slot_name}', whose range '{value_range}' does not hold strings" + for operator in self._CLASS_EXPRESSION_OPERATORS: + for member in getattr(expr, operator) or []: + reason = self._untranslatable(cls, member) + if reason is not None: + return reason + return None + + def _is_known_range(self, r: ElementName) -> bool: + sv = self.schemaview + return ( + r in sv.all_classes() + or r in sv.all_types() + or r in sv.all_enums() + or any(datatype.linkml_type == r for datatype in ShaclDataType) + ) + # ------------------------------------------------------------------- # Rules → sh:sparql # ------------------------------------------------------------------- @@ -671,6 +965,33 @@ def _add_class(self, func: Callable, r: ElementName) -> None: range_ref += self.suffix func(SH["node"], URIRef(range_ref)) + def _slot_path(self, s: SlotDefinition) -> URIRef: + """The property IRI of slot *s*, the ``sh:path`` of its property shapes.""" + sv = self.schemaview + # fixed in linkml-runtime 1.1.3 + if s.name in sv.element_by_schema_map(): + return URIRef(sv.get_uri(s, expand=True)) + return URIRef(sv.expand_curie(f"{sv.schema.default_prefix}:{underscore(s.name)}")) + + def _add_range(self, g: Graph, func: Callable, r: ElementName) -> None: + """Add the value-type constraint for range *r*: a class, type, enum or built-in datatype.""" + sv = self.schemaview + if r in sv.all_classes(): + cls_def = sv.get_class(r) + is_any = cls_def and getattr(cls_def, "class_uri", None) == "linkml:Any" + self._add_class(func, r) + if not is_any: + if sv.get_identifier_slot(r) is not None: + func(SH.nodeKind, SH.IRI) + else: + func(SH.nodeKind, SH.BlankNodeOrIRI) + elif r in sv.all_types(): + self._add_type(func, r) + elif r in sv.all_enums(): + self._add_enum(g, func, r) + else: + add_simple_data_type(func, r) + def _add_enum(self, g: Graph, func: Callable, r: ElementName) -> None: sv = self.schemaview enum = sv.get_enum(r) @@ -852,6 +1173,11 @@ def add_simple_data_type(func: Callable, r: ElementName) -> None: func(SH.datatype, datatype.uri_ref) +def _set_fields(element) -> set[str]: + """Names of the fields of metamodel *element* that hold a value.""" + return {f.name for f in fields(element) if getattr(element, f.name) not in (None, [], {})} + + @shared_arguments(ShaclGenerator) @click.command(name="shacl") @click.option( 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/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/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_compliance/test_boolean_slot_compliance.py b/tests/linkml/test_compliance/test_boolean_slot_compliance.py index fd900bf938..e73ff46851 100644 --- a/tests/linkml/test_compliance/test_boolean_slot_compliance.py +++ b/tests/linkml/test_compliance/test_boolean_slot_compliance.py @@ -505,7 +505,7 @@ def test_class_any_of(framework, data_name, s1value, s2value, is_valid): core_elements=["any_of", "ClassDefinition"], ) expected_behavior = ValidationBehavior.IMPLEMENTS - if framework not in [OWL]: + if framework not in [OWL, SHACL]: # TODO: rdflib transformer has issues around ranges expected_behavior = ValidationBehavior.INCOMPLETE # TODO: rdflib transformer has issues around ranges @@ -632,8 +632,22 @@ def test_class_any_of_with_required(framework, nest, op, name, family_name, give core_elements=[op, "ClassDefinition"], ) expected_behavior = ValidationBehavior.IMPLEMENTS - if framework not in [JSON_SCHEMA]: + if framework not in [JSON_SCHEMA, SHACL]: expected_behavior = ValidationBehavior.INCOMPLETE + elif framework == SHACL and 5 in (name, family_name, given_name): + # SHACL validation makes its instances through python dataclasses, which coerce + # the integer to a string, so the range violation never reaches the shapes. A + # row can then only be detected through the operator itself. + present = [value is not None for value in (name, family_name, given_name)] + members = [present[0], present[1] and present[2]] + operator_holds = { + "any_of": any(members), + "all_of": all(members), + "exactly_one_of": sum(members) == 1, + "none_of": not any(members), + }[op] + if operator_holds: + expected_behavior = ValidationBehavior.INCOMPLETE data = {SLOT_S1: name, SLOT_S2: family_name, SLOT_S3: given_name} if nest: 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..26eedc5282 100644 --- a/tests/linkml/test_generators/test_shaclgen.py +++ b/tests/linkml/test_generators/test_shaclgen.py @@ -2784,3 +2784,942 @@ 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 + + +# --------------------------------------------------------------------------- +# Class expressions → sh:or / sh:and / sh:xone / sh:not +# --------------------------------------------------------------------------- + +EX_CE = rdflib.Namespace("https://example.org/class-expressions/") + +_CLASS_EXPRESSION_HEADER = """ +id: https://example.org/class-expressions +name: class_expressions +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/class-expressions/ +imports: + - linkml:types +default_prefix: ex +default_range: string +""" + +_REFERENCE_SYSTEM_SCHEMA = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + codeEPSG: + range: integer + coordinateSystemName: {{}} +classes: + ReferenceSystem: + class_uri: ex:ReferenceSystem + slots: [codeEPSG, coordinateSystemName] + {operator}: + - slot_conditions: + codeEPSG: + required: true + - slot_conditions: + coordinateSystemName: + required: true +""" +) + +_LOGICAL_PREDICATES = (SH["or"], SH["and"], SH.xone, SH["not"]) + + +def _list_members(g, shape, predicate): + """Members of the single SHACL list that *shape* has for *predicate*.""" + lists = list(g.objects(shape, predicate)) + assert len(lists) == 1, f"expected one {predicate} list on {shape}, got {len(lists)}" + return list(Collection(g, lists[0])) + + +def _condition(g, member, path): + """The property shape for *path* inside the member shape *member*.""" + shapes = [p for p in g.objects(member, SH.property) if (p, SH.path, path) in g] + assert len(shapes) == 1, f"expected one condition on {path}, got {len(shapes)}" + return shapes[0] + + +def _conforms(shacl_ttl: str, data_ttl: str) -> bool: + """Validate *data_ttl*; meta_shacl makes pyshacl fail on an ill-formed shapes graph.""" + import pyshacl + + conforms, _, _ = pyshacl.validate( + data_graph=data_ttl, + shacl_graph=shacl_ttl, + data_graph_format="turtle", + shacl_graph_format="turtle", + advanced=True, + meta_shacl=True, + ) + return conforms + + +@pytest.mark.parametrize( + "operator,predicate", + [("any_of", SH["or"]), ("all_of", SH["and"]), ("exactly_one_of", SH.xone)], +) +def test_class_expression_list_operator_generates_logical_constraint(operator, predicate): + """any_of, all_of and exactly_one_of become sh:or, sh:and and sh:xone over the member shapes.""" + g = _parse_shacl(_REFERENCE_SYSTEM_SCHEMA.format(operator=operator)) + + members = _list_members(g, EX_CE.ReferenceSystem, predicate) + assert len(members) == 2 + for member, path in zip(members, (EX_CE.codeEPSG, EX_CE.coordinateSystemName)): + condition = _condition(g, member, path) + assert (condition, SH.minCount, Literal(1)) in g + assert (member, SH.targetClass, None) not in g + others = set(_LOGICAL_PREDICATES) - {predicate} + assert not any((EX_CE.ReferenceSystem, p, None) in g for p in others) + + +def test_class_expression_none_of_generates_one_sh_not_per_member(): + """none_of becomes one sh:not per member; the negated constraints all apply (SHACL §2.1.1).""" + g = _parse_shacl(_REFERENCE_SYSTEM_SCHEMA.format(operator="none_of")) + + negated = list(g.objects(EX_CE.ReferenceSystem, SH["not"])) + assert len(negated) == 2 + paths = {path for member in negated for p in g.objects(member, SH.property) for path in g.objects(p, SH.path)} + assert paths == {EX_CE.codeEPSG, EX_CE.coordinateSystemName} + + +@pytest.mark.parametrize( + "operator,properties,expected", + [ + ("any_of", 'ex:codeEPSG 4326 ; ex:coordinateSystemName "WGS 84"', True), + ("any_of", "ex:codeEPSG 4326", True), + ("any_of", "", False), + ("exactly_one_of", "ex:codeEPSG 4326", True), + ("exactly_one_of", 'ex:codeEPSG 4326 ; ex:coordinateSystemName "WGS 84"', False), + ("exactly_one_of", "", False), + ("all_of", 'ex:codeEPSG 4326 ; ex:coordinateSystemName "WGS 84"', True), + ("all_of", "ex:codeEPSG 4326", False), + ("none_of", "", True), + ("none_of", "ex:codeEPSG 4326", False), + ], +) +def test_class_expression_pyshacl_end_to_end(operator, properties, expected): + """End-to-end: each operator admits exactly the instances the metamodel says it holds for.""" + shacl_ttl = ShaclGenerator(_REFERENCE_SYSTEM_SCHEMA.format(operator=operator), mergeimports=False).serialize() + data = f""" + @prefix ex: . + ex:rs a ex:ReferenceSystem {";" if properties else ""} {properties} . + """ + assert _conforms(shacl_ttl, data) is expected + + +_FORMAT_PROFILES_SCHEMA = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + fileFormat: {} + formatType: {} + version: {} + hasChannel: + range: Channel + multivalued: true + inlined: true +classes: + Channel: + class_uri: ex:Channel + Format: + class_uri: ex:Format + slots: [fileFormat, formatType, version, hasChannel] + any_of: + - description: A single-channel trace. + slot_conditions: + fileFormat: + equals_string_in: [OSI, TXTH] + formatType: + required: true + version: + required: true + - description: A multi-channel container. + slot_conditions: + fileFormat: + required: true + equals_string: MCAP + hasChannel: + required: true +""" +) + + +@pytest.mark.parametrize( + "properties,expected", + [ + ('ex:fileFormat "OSI" ; ex:formatType "SensorView" ; ex:version "3.7.0"', True), + ('ex:fileFormat "MCAP" ; ex:hasChannel ex:ch', True), + ('ex:fileFormat "MCAP" ; ex:formatType "SensorView" ; ex:version "3.7.0"', False), + ('ex:fileFormat "OSI" ; ex:formatType "SensorView" ; ex:hasChannel ex:ch', False), + ], +) +def test_class_expression_any_of_profiles_pyshacl_end_to_end(properties, expected): + """End-to-end: a class-level any_of selects between two complete profiles of a class.""" + shacl_ttl = ShaclGenerator(_FORMAT_PROFILES_SCHEMA, mergeimports=False).serialize() + data = f""" + @prefix ex: . + ex:ch a ex:Channel . + ex:f a ex:Format ; {properties} . + """ + assert _conforms(shacl_ttl, data) is expected + + +def test_class_expression_member_metadata(): + """A member's title and description annotate its shape, as they do for a class's NodeShape.""" + g = _parse_shacl(_FORMAT_PROFILES_SCHEMA) + + comments = {str(c) for m in _list_members(g, EX_CE.Format, SH["or"]) for c in g.objects(m, RDFS.comment)} + assert comments == {"A single-channel trace.", "A multi-channel container."} + + +_CONDITIONS_SCHEMA = ( + _CLASS_EXPRESSION_HEADER + + """ +enums: + ColourEnum: + permissible_values: + red: {} + blue: {} +slots: + label: {} + size: + range: integer + count: + multivalued: true + exact: + multivalued: true + colour: {} + target: {} + note: {} +classes: + Target: + class_uri: ex:Target + Thing: + class_uri: ex:Thing + slots: [label, size, count, exact, colour, target, note] + all_of: + - title: every condition + slot_conditions: + label: + description: Starts upper case. + required: true + pattern: "^[A-Z]" + equals_string_in: [Alpha, Beta] + size: + minimum_value: 1 + maximum_value: 10 + equals_number: 5 + count: + required: true + minimum_cardinality: 2 + maximum_cardinality: 4 + exact: + exact_cardinality: 3 + colour: + range: ColourEnum + target: + value_presence: PRESENT + range: Target + note: + value_presence: ABSENT +""" +) + + +def test_class_expression_slot_condition_fields(): + """Each supported slot-condition field maps to the SHACL constraint the slot loop uses for it.""" + g = _parse_shacl(_CONDITIONS_SCHEMA) + (member,) = _list_members(g, EX_CE.Thing, SH["and"]) + assert (member, RDFS.label, Literal("every condition")) in g + + def values(path, predicate): + return set(g.objects(_condition(g, member, path), predicate)) + + def in_list(path): + (node,) = values(path, SH["in"]) + return list(Collection(g, node)) + + assert values(EX_CE.label, SH.minCount) == {Literal(1)} + assert values(EX_CE.label, SH.pattern) == {Literal("^[A-Z]")} + assert values(EX_CE.label, SH.description) == {Literal("Starts upper case.")} + assert in_list(EX_CE.label) == [Literal("Alpha"), Literal("Beta")] + + # equals_number is a value comparison; SHACL allows one sh:minInclusive and one + # sh:maxInclusive per shape, so next to the bounds it moves into an sh:and member + assert values(EX_CE.size, SH.minInclusive) == {Literal(1)} + assert values(EX_CE.size, SH.maxInclusive) == {Literal(10)} + (and_node,) = values(EX_CE.size, SH["and"]) + repeated = {(p, o) for m in Collection(g, and_node) for p, o in g.predicate_objects(m)} + assert repeated == {(SH.minInclusive, Literal(5)), (SH.maxInclusive, Literal(5))} + assert values(EX_CE.size, SH["in"]) == set() + assert values(EX_CE.size, SH.minCount) == set() + assert values(EX_CE.size, SH.hasValue) == set() + + # required and a cardinality give one sh:minCount, the stricter of the two + assert values(EX_CE["count"], SH.minCount) == {Literal(2)} + assert values(EX_CE["count"], SH.maxCount) == {Literal(4)} + assert values(EX_CE.exact, SH.minCount) == {Literal(3)} + assert values(EX_CE.exact, SH.maxCount) == {Literal(3)} + + assert in_list(EX_CE.colour) == [Literal("red"), Literal("blue")] + + assert values(EX_CE.target, SH.minCount) == {Literal(1)} + assert values(EX_CE.target, SH["class"]) == {EX_CE.Target} + assert values(EX_CE.target, SH.nodeKind) == {SH.BlankNodeOrIRI} + + assert values(EX_CE.note, SH.maxCount) == {Literal(0)} + assert values(EX_CE.note, SH.minCount) == set() + + +@pytest.mark.parametrize( + "condition,properties,expected", + [ + # outside none_of a value constraint says nothing about presence + ("any_of", "", True), + ("any_of", 'ex:label "b"', False), + # inside none_of it requires the slot, so an absent slot is not rejected + ("none_of", "", True), + ("none_of", 'ex:label "A"', False), + ("none_of", 'ex:label "B"', True), + ], +) +def test_class_expression_presence_semantics(condition, properties, expected): + """A condition holds vacuously for an absent slot, except under none_of, as in the JSON Schema generator.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + f""" +slots: + label: {{}} +classes: + Thing: + class_uri: ex:Thing + slots: [label] + {condition}: + - slot_conditions: + label: + {"pattern: '^[A-Z]'" if condition == "any_of" else "equals_string: A"} +""" + ) + shacl_ttl = ShaclGenerator(schema, mergeimports=False).serialize() + data = f""" + @prefix ex: . + ex:t a ex:Thing {";" if properties else ""} {properties} . + """ + assert _conforms(shacl_ttl, data) is expected + + +def test_class_expression_nested_and_is_a(): + """Nested expressions recurse into the member shape; is_a gives sh:class.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + a: {} + b: {} + c: {} +classes: + Marker: + class_uri: ex:Marker + Thing: + class_uri: ex:Thing + slots: [a, b, c] + any_of: + - all_of: + - slot_conditions: + a: + required: true + - slot_conditions: + b: + required: true + - is_a: Marker + slot_conditions: + c: + required: true +""" + ) + # open shapes: the instance is also a Marker, whose own shape declares no slots + shacl_ttl = ShaclGenerator(schema, mergeimports=False, closed=False).serialize() + g = rdflib.Graph().parse(data=shacl_ttl) + first, second = _list_members(g, EX_CE.Thing, SH["or"]) + assert len(_list_members(g, first, SH["and"])) == 2 + assert (second, SH["class"], EX_CE.Marker) in g + + prefix = "@prefix ex: ." + assert _conforms(shacl_ttl, f'{prefix} ex:t a ex:Thing ; ex:a "1" ; ex:b "2" .') + assert not _conforms(shacl_ttl, f'{prefix} ex:t a ex:Thing ; ex:a "1" .') + assert _conforms(shacl_ttl, f'{prefix} ex:t a ex:Thing, ex:Marker ; ex:c "3" .') + assert not _conforms(shacl_ttl, f'{prefix} ex:t a ex:Thing ; ex:c "3" .') + + +def test_class_expression_reaches_subclass_instances_through_the_target(): + """The constraint sits on the declaring class's shape only and reaches subclass instances via sh:targetClass.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + a: {} + b: {} +classes: + Parent: + class_uri: ex:Parent + slots: [a, b] + any_of: + - slot_conditions: + a: + required: true + - slot_conditions: + b: + required: true + Child: + class_uri: ex:Child + is_a: Parent +""" + ) + shacl_ttl = ShaclGenerator(schema, mergeimports=False, closed=False).serialize() + g = rdflib.Graph().parse(data=shacl_ttl) + assert (EX_CE.Parent, SH["or"], None) in g + assert (EX_CE.Child, SH["or"], None) not in g + + data = """ + @prefix ex: . + @prefix rdfs: . + ex:Child rdfs:subClassOf ex:Parent . + ex:c a ex:Child . + """ + assert not _conforms(shacl_ttl, data) + + +def test_class_expression_untranslatable_operator_skipped_with_warning(caplog): + """An operator with an untranslatable member is skipped whole, with a warning; the others are kept.""" + import logging + + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + tags: + multivalued: true + a: {} +classes: + Thing: + class_uri: ex:Thing + slots: [tags, a] + any_of: + - slot_conditions: + tags: + has_member: + equals_string: x + - slot_conditions: + a: + required: true + none_of: + - slot_conditions: + a: + equals_string: forbidden +""" + ) + with caplog.at_level(logging.WARNING, logger="linkml.generators.shaclgen"): + g = _parse_shacl(schema) + + assert (EX_CE.Thing, SH["or"], None) not in g + assert (EX_CE.Thing, SH["not"], None) in g + assert any( + "any_of" in rec.message and "has_member" in rec.message and "tags" in rec.message for rec in caplog.records + ) + + +def test_class_expression_condition_on_identifier_skipped_with_warning(caplog): + """An identifier is the node's IRI, not a property arc, so a condition on it is not translated.""" + import logging + + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + id: + identifier: true + a: {} +classes: + Thing: + class_uri: ex:Thing + slots: [id, a] + exactly_one_of: + - slot_conditions: + id: + pattern: "^ex:" + - slot_conditions: + a: + required: true +""" + ) + with caplog.at_level(logging.WARNING, logger="linkml.generators.shaclgen"): + g = _parse_shacl(schema) + + assert (EX_CE.Thing, SH.xone, None) not in g + assert any("exactly_one_of" in rec.message and "identifier" in rec.message for rec in caplog.records) + + +def test_class_expression_absent_leaves_node_shapes_unchanged(): + """Without class-level expressions no node shape gets a logical constraint; slot-level any_of is unaffected.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + value: + any_of: + - range: integer + - range: string +classes: + Thing: + class_uri: ex:Thing + slots: [value] +""" + ) + g = _parse_shacl(schema) + + for shape in g.subjects(SH.targetClass, None): + assert not any((shape, p, None) in g for p in _LOGICAL_PREDICATES) + (value_shape,) = [p for p in g.objects(EX_CE.Thing, SH.property) if (p, SH.path, EX_CE.value) in g] + assert (value_shape, SH["or"], None) in g + + +def test_class_expression_condition_path_is_the_slot_induced_for_the_class(): + """A condition's sh:path is the one the class's own property shape uses, slot_usage and attributes included.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + code: + slot_uri: ex:baseCode + exact mappings: + slot_uri: ex:exactMatch +classes: + Thing: + class_uri: ex:Thing + slots: [code, exact mappings] + slot_usage: + code: + slot_uri: ex:thingCode + attributes: + loc: + slot_uri: ex:thingLoc + any_of: + - slot_conditions: + code: + required: true + - slot_conditions: + exact_mappings: + required: true + - slot_conditions: + loc: + required: true + Other: + class_uri: ex:Other + attributes: + loc: + slot_uri: ex:otherLoc +""" + ) + g = _parse_shacl(schema) + + class_paths = {path for p in g.objects(EX_CE.Thing, SH.property) for path in g.objects(p, SH.path)} + condition_paths = [ + path + for member in _list_members(g, EX_CE.Thing, SH["or"]) + for p in g.objects(member, SH.property) + for path in g.objects(p, SH.path) + ] + assert condition_paths and set(condition_paths) <= class_paths + assert set(condition_paths) == {EX_CE.thingCode, EX_CE.exactMatch, EX_CE.thingLoc} + + +def test_class_expression_nested_none_of_requires_the_slot(): + """A none_of nested in another operator requires the slot too, so it reads like a class's own none_of.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + label: {} +classes: + Thing: + class_uri: ex:Thing + slots: [label] + all_of: + - none_of: + - slot_conditions: + label: + equals_string: A +""" + ) + shacl_ttl = ShaclGenerator(schema, mergeimports=False).serialize() + prefix = "@prefix ex: ." + assert _conforms(shacl_ttl, f"{prefix} ex:t a ex:Thing .") + assert _conforms(shacl_ttl, f'{prefix} ex:t a ex:Thing ; ex:label "B" .') + assert not _conforms(shacl_ttl, f'{prefix} ex:t a ex:Thing ; ex:label "A" .') + + +@pytest.mark.parametrize( + "condition,properties,expected", + [ + # a cardinality is taken literally inside none_of: "not at most zero values" means present + ("maximum_cardinality: 0", "", False), + ("maximum_cardinality: 0", 'ex:tag "x"', True), + ("maximum_cardinality: 1", 'ex:tag "x"', False), + ("maximum_cardinality: 1", 'ex:tag "x", "y"', True), + ], +) +def test_class_expression_none_of_takes_cardinality_literally(condition, properties, expected): + """Inside none_of only a condition silent on presence and cardinality is made to require the slot.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + f""" +slots: + tag: + multivalued: true +classes: + Thing: + class_uri: ex:Thing + slots: [tag] + none_of: + - slot_conditions: + tag: + {condition} +""" + ) + shacl_ttl = ShaclGenerator(schema, mergeimports=False).serialize() + data = f""" + @prefix ex: . + ex:t a ex:Thing {";" if properties else ""} {properties} . + """ + assert _conforms(shacl_ttl, data) is expected + + +def test_class_expression_equals_string_on_enum_uses_the_meaning(): + """equals_string on an enum slot compares against the permissible value as _add_enum renders it.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +enums: + FormatEnum: + permissible_values: + OSI: + meaning: ex:OSI + MCAP: {} +slots: + fileFormat: + range: FormatEnum + hasChannel: {} +classes: + Format: + class_uri: ex:Format + slots: [fileFormat, hasChannel] + any_of: + - slot_conditions: + fileFormat: + equals_string: OSI + - slot_conditions: + fileFormat: + equals_string: MCAP + hasChannel: + required: true +""" + ) + shacl_ttl = ShaclGenerator(schema, mergeimports=False).serialize() + g = rdflib.Graph().parse(data=shacl_ttl) + first, second = _list_members(g, EX_CE.Format, SH["or"]) + (osi_in,) = g.objects(_condition(g, first, EX_CE.fileFormat), SH["in"]) + assert list(Collection(g, osi_in)) == [EX_CE.OSI] + (mcap_in,) = g.objects(_condition(g, second, EX_CE.fileFormat), SH["in"]) + assert list(Collection(g, mcap_in)) == [Literal("MCAP")] + + prefix = "@prefix ex: ." + assert _conforms(shacl_ttl, f"{prefix} ex:f a ex:Format ; ex:fileFormat ex:OSI .") + assert not _conforms(shacl_ttl, f'{prefix} ex:f a ex:Format ; ex:fileFormat "MCAP" .') + + +@pytest.mark.parametrize("value,expected", [("5.0", True), ("5", True), ("6.0", False)]) +def test_class_expression_equals_number_compares_values(value, expected): + """equals_number matches the value, whatever numeric datatype carries it.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + size: + range: float + label: {} +classes: + Thing: + class_uri: ex:Thing + slots: [size, label] + any_of: + - slot_conditions: + size: + equals_number: 5 + - slot_conditions: + label: + required: true +""" + ) + shacl_ttl = ShaclGenerator(schema, mergeimports=False, closed=False).serialize() + # sh:datatype xsd:float on the class's own property shape would reject an xsd:decimal, + # so every value is given as xsd:float + data = f""" + @prefix ex: . + @prefix xsd: . + ex:t a ex:Thing ; ex:size "{value}"^^xsd:float . + """ + assert _conforms(shacl_ttl, data) is expected + + +_UNTRANSLATABLE_CONDITIONS = { + # an identifier is the node's IRI; here it becomes one only through slot_usage + "identifier": """ +slots: + id: {} + a: {} +classes: + Thing: + class_uri: ex:Thing + slots: [id, a] + slot_usage: + id: + identifier: true + any_of: + - slot_conditions: + id: + required: true + - slot_conditions: + a: + required: true +""", + "'undefined', which is not a slot": """ +slots: + a: {} +classes: + Thing: + class_uri: ex:Thing + slots: [a] + any_of: + - slot_conditions: + undefined: + required: true + - slot_conditions: + a: + required: true +""", + "does not hold strings": """ +slots: + count: + range: integer + a: {} +classes: + Thing: + class_uri: ex:Thing + slots: [count, a] + any_of: + - slot_conditions: + count: + equals_string: "5" + - slot_conditions: + a: + required: true +""", +} + + +@pytest.mark.parametrize("reason", list(_UNTRANSLATABLE_CONDITIONS)) +def test_class_expression_untranslatable_condition_skipped_with_warning(caplog, reason): + """A condition on an identifier, on no slot, or equals_string on a non-string range is not translated.""" + import logging + + with caplog.at_level(logging.WARNING, logger="linkml.generators.shaclgen"): + g = _parse_shacl(_CLASS_EXPRESSION_HEADER + _UNTRANSLATABLE_CONDITIONS[reason]) + + assert (EX_CE.Thing, SH["or"], None) not in g + assert any("any_of" in rec.message and reason in rec.message for rec in caplog.records) + + +def test_class_expression_is_a_with_native_names_uses_sh_node(): + """With native names, is_a references the member class's shape, suffix included, as a range does.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + """ +slots: + a: {} +classes: + Marker: + class_uri: ex:Marker + Thing: + class_uri: ex:Thing + slots: [a] + any_of: + - is_a: Marker + - slot_conditions: + a: + required: true +""" + ) + g = _parse_shacl(schema, use_class_uri_names=False, suffix="Shape") + + (first, _) = _list_members(g, EX_CE.ThingShape, SH["or"]) + assert set(g.objects(first, SH.node)) == {EX_CE.MarkerShape} + assert (first, SH["class"], None) not in g + + +_REPEATED_PARAMETERS_SCHEMA = ( + _CLASS_EXPRESSION_HEADER + + """ +types: + Code: + typeof: string + pattern: "^[A-Z]+$" +enums: + LetterEnum: + permissible_values: + A: {} + B: {} + C: {} +slots: + size: + range: integer + label: {} + letter: {} + code: {} + other: {} +classes: + Thing: + class_uri: ex:Thing + slots: [size, label, letter, code, other] + any_of: + - slot_conditions: + size: + minimum_value: 3 + equals_number: 5 + label: + equals_string: A + equals_string_in: [A, B] + letter: + range: LetterEnum + equals_string: B + code: + range: Code + pattern: "^AB" + - slot_conditions: + other: + required: true +""" +) + + +@pytest.mark.parametrize( + "properties,expected", + [ + ('ex:size 5 ; ex:label "A" ; ex:letter "B" ; ex:code "ABC"', True), + ("ex:size 4", False), + ('ex:label "B"', False), + ('ex:letter "A"', False), + ('ex:code "ABc"', False), + ('ex:code "XY"', False), + ('ex:size 4 ; ex:other "x"', True), + ], +) +def test_class_expression_repeated_parameters_stay_well_formed(properties, expected): + """A parameter SHACL allows once per shape, needed twice by one condition, goes into sh:and. + + _conforms runs pyshacl with meta_shacl, which fails on an ill-formed shapes graph. + """ + shacl_ttl = ShaclGenerator(_REPEATED_PARAMETERS_SCHEMA, mergeimports=False).serialize() + data = f""" + @prefix ex: . + ex:t a ex:Thing ; {properties} . + """ + assert _conforms(shacl_ttl, data) is expected + + +@pytest.mark.parametrize( + "extra,properties,expected", + [ + ("", "", True), + ("maximum_cardinality: 5", "", True), + ("minimum_cardinality: 0", "", True), + ("maximum_cardinality: 5", 'ex:tag "A"', False), + ("maximum_cardinality: 5", 'ex:tag "B"', True), + ], +) +def test_class_expression_none_of_presence_is_monotonic(extra, properties, expected): + """Inside none_of, a cardinality that an absent slot satisfies does not flip an absent slot to rejected.""" + schema = ( + _CLASS_EXPRESSION_HEADER + + f""" +slots: + tag: + multivalued: true +classes: + Thing: + class_uri: ex:Thing + slots: [tag] + none_of: + - slot_conditions: + tag: + equals_string: A + {extra} +""" + ) + shacl_ttl = ShaclGenerator(schema, mergeimports=False).serialize() + data = f""" + @prefix ex: . + ex:t a ex:Thing {";" if properties else ""} {properties} . + """ + assert _conforms(shacl_ttl, data) is expected 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_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_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..91c43e8e95 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 = [ @@ -1172,7 +1172,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 +1714,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 +1742,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 +1773,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 +2147,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 +2166,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]] @@ -2511,7 +2511,7 @@ requires-dist = [ { 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,7 +2524,7 @@ 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" }, ] @@ -2563,7 +2563,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 = [ @@ -2607,7 +2607,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]] @@ -3175,7 +3175,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 +3185,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 +3673,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 +4357,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 +4964,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 +4998,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 +5190,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 +5444,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 +5477,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 +5531,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 +5647,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 +5682,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.5.1" +version = "21.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -5686,9 +5691,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]]