diff --git a/.gitignore b/.gitignore index 185fe46..2f3c741 100644 --- a/.gitignore +++ b/.gitignore @@ -161,5 +161,5 @@ cython_debug/ #.idea/ src/spdx_python_model/bindings/ -gen/*.py -gen/*.pyi +gen/**/*.py +gen/**/*.pyi diff --git a/README.md b/README.md index 6c78b1c..492e473 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,19 @@ from spdx_python_model import v3_0_1 as spdx_3_0 p = spdx_3_0.Person() ``` +You can also have the bindings automatically detect the correct version to use +using the `load()` API: + +```python +import spdx_python_model + +path = Path("/path/to/file.spdx3.json") + +model, objset = spdx_python_model.load(path) + +p = model.Person() +``` + ## Testing This repository has support for running tests against the bindings using `pytest`. diff --git a/gen/generate-bindings b/gen/generate-bindings index e1edfbc..c1a1c1a 100755 --- a/gen/generate-bindings +++ b/gen/generate-bindings @@ -11,21 +11,26 @@ mkdir -p "gen" echo "# Import all versions" > __init__.py +get_context_url() { + echo "https://spdx.org/rdf/$1/spdx-context.jsonld" +} + for v in $SPDX_VERSIONS; do MODNAME="v$(echo "$v" | sed 's/[^a-zA-Z0-9_]/_/g')" + CONTEXT_URL="$(get_context_url "$v")" if [ -n "${SHACL2CODE_SPDX_DIR}" ] && [ -d "${SHACL2CODE_SPDX_DIR}/$v" ] then shacl2code generate --input "file://${SHACL2CODE_SPDX_DIR}/$v/spdx-model.ttl" \ --input "file://${SHACL2CODE_SPDX_DIR}/$v/spdx-json-serialize-annotations.ttl" \ - --context-url "file://${SHACL2CODE_SPDX_DIR}/$v/spdx-context.jsonld" https://spdx.org/rdf/$v/spdx-context.jsonld \ + --context-url "file://${SHACL2CODE_SPDX_DIR}/$v/spdx-context.jsonld" $CONTEXT_URL \ --license Apache-2.0 \ python \ --output "$MODNAME" else shacl2code generate --input https://spdx.org/rdf/$v/spdx-model.ttl \ --input https://spdx.org/rdf/$v/spdx-json-serialize-annotations.ttl \ - --context https://spdx.org/rdf/$v/spdx-context.jsonld \ + --context $CONTEXT_URL \ --license Apache-2.0 \ python \ --output "$MODNAME" @@ -33,3 +38,16 @@ for v in $SPDX_VERSIONS; do echo "from . import $MODNAME" >> __init__.py done + +MODNAME="" +CONTEXT_URL="" + +echo >> __init__.py +echo "# Generate context table" >> __init__.py +echo "_CONTEXT_TABLE = {" >> __init__.py +for v in $SPDX_VERSIONS; do + MODNAME="v$(echo "$v" | sed 's/[^a-zA-Z0-9_]/_/g')" + CONTEXT_URL="$(get_context_url "$v")" + echo " '$CONTEXT_URL': $MODNAME" >> __init__.py +done +echo "}" >> __init__.py diff --git a/src/spdx_python_model/__init__.py b/src/spdx_python_model/__init__.py index c3918ed..4eb3237 100644 --- a/src/spdx_python_model/__init__.py +++ b/src/spdx_python_model/__init__.py @@ -4,3 +4,77 @@ from .bindings import * from .version import VERSION + +from .bindings import _CONTEXT_TABLE + +from pathlib import Path +import json + + +class LoadError(Exception): + pass + + +def load_data(data): + """ + Automatically load a SPDX 3 JSON document with the correct model based on + its context + + :param data: The decoded JSON data as a Python dict + + :returns: A tuple that contains the model and the decoded SHACLObjectSet + + :raises LoadError: If the data is missing a context or if the context is + not recognized + :raises TypeError: If the data is not a dictionary + """ + + if not isinstance(data, dict): + raise TypeError("Data must be a dictionary") + + if "@context" not in data: + raise LoadError("No @context in data") + + context_url = None + + if isinstance(data["@context"], str): + context_url = data["@context"] + elif isinstance(data["@context"], list): + for item in data["@context"]: + if isinstance(item, str): + context_url = item + break + + if not context_url: + raise LoadError("No valid @context URL string found in data") + + if context_url not in _CONTEXT_TABLE: + raise LoadError(f"Unknown context URL '{context}'") + + model = _CONTEXT_TABLE[context_url] + + d = model.JSONLDDeserializer() + objset = model.SHACLObjectSet() + + d.deserialize_data(data, objset) + + return model, objset + + +def load(path: Path): + """ + Automatically load a SPDX 3 JSON document with the correct model based on + its context + + :param data: The path to the SPDX 3 JSON file + + :returns: A tuple that contains the model and the decoded SHACLObjectSet + + :raises LoadError: If the data is missing a context or if the context is + not recognized + :raises TypeError: If the data is not a dictionary + """ + with path.open("r") as f: + data = json.load(f) + + return load_data(data) diff --git a/tests/data/3.0.1/example.spdx3.json b/tests/data/3.0.1/example.spdx3.json new file mode 100644 index 0000000..9098061 --- /dev/null +++ b/tests/data/3.0.1/example.spdx3.json @@ -0,0 +1,238 @@ +{ + "@context" : "https://spdx.org/rdf/3.0.1/spdx-context.jsonld", + "@graph" : [ { + "@id" : "_:creationInfo_0", + "type" : "CreationInfo", + "specVersion" : "3.0.1", + "createdBy" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0" ], + "createdUsing" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/additionalToolSPDXRef-gnrtd2", "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/additionalToolSPDXRef-gnrtd1" ], + "created" : "2021-08-26T01:46:00Z" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd3", + "type" : "Relationship", + "relationshipType" : "describes", + "completeness" : "noAssertion", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/document0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd5", + "type" : "Relationship", + "relationshipType" : "contains", + "completeness" : "noAssertion", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd6" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd7", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd6", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd9", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd6", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd10", + "type" : "Relationship", + "relationshipType" : "contains", + "completeness" : "noAssertion", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd11" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd12", + "type" : "Relationship", + "relationshipType" : "generates", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd11" ], + "completeness" : "noAssertion", + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd13", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd15", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd13", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd16", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd13", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd17", + "type" : "Relationship", + "relationshipType" : "generates", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd11" ], + "completeness" : "noAssertion", + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd6", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd18", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd11", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd19", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd20" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd11", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd21", + "type" : "Relationship", + "relationshipType" : "contains", + "completeness" : "noAssertion", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd13" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd22", + "type" : "Relationship", + "relationshipType" : "hasConcludedLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd23", + "type" : "Relationship", + "relationshipType" : "hasDeclaredLicense", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8" ], + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/document0", + "type" : "SpdxDocument", + "dataLicense" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd24", + "rootElement" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4" ], + "name" : "hello", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/additionalToolSPDXRef-gnrtd1", + "type" : "Tool", + "name" : "github.com/spdx/tools-golang/builder", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/additionalToolSPDXRef-gnrtd2", + "type" : "Tool", + "name" : "github.com/spdx/tools-golang/idsearcher", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd8", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "GPL-3.0-or-later", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd20", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "NOASSERTION", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd24", + "type" : "simplelicensing_LicenseExpression", + "simplelicensing_licenseExpression" : "CC0-1.0", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd14", + "type" : "LifecycleScopedRelationship", + "relationshipType" : "usesTool", + "scope" : "build", + "to" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd13" ], + "completeness" : "noAssertion", + "from" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0", + "type" : "Person", + "externalIdentifier" : [ { + "type" : "ExternalIdentifier", + "identifier" : "steve@swinslow.net", + "externalIdentifierType" : "email" + } ], + "name" : "Steve Winslow", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd6", + "type" : "software_File", + "software_copyrightText" : "Copyright Contributors to the spdx-examples project.", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "md5", + "hashValue" : "935054fe899ca782e11003bbae5e166c" + }, { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "20862a6d08391d07d09344029533ec644fac6b21" + }, { + "type" : "Hash", + "algorithm" : "sha256", + "hashValue" : "b4e5ca56d1f9110ca94ed0bf4e6d9ac11c2186eb7cd95159c6fdb50e8db5a823" + } ], + "name" : "./src/hello.c", + "software_primaryPurpose" : "source", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd11", + "type" : "software_File", + "software_copyrightText" : "NOASSERTION", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "20291a81ef065ff891b537b64d4fdccaf6f5ac02" + }, { + "type" : "Hash", + "algorithm" : "sha256", + "hashValue" : "83a33ff09648bb5fc5272baca88cf2b59fd81ac4cc6817b86998136af368708e" + }, { + "type" : "Hash", + "algorithm" : "md5", + "hashValue" : "08a12c966d776864cc1eb41fd03c3c3d" + } ], + "name" : "./build/hello", + "contentType" : "application/octet-stream", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd13", + "type" : "software_File", + "software_copyrightText" : "NOASSERTION", + "verifiedUsing" : [ { + "type" : "Hash", + "algorithm" : "sha1", + "hashValue" : "69a2e85696fff1865c3f0686d6c3824b59915c80" + }, { + "type" : "Hash", + "algorithm" : "sha256", + "hashValue" : "5da19033ba058e322e21c90e6d6d859c90b1b544e7840859c12cae5da005e79c" + }, { + "type" : "Hash", + "algorithm" : "md5", + "hashValue" : "559424589a4f3f75fd542810473d8bc1" + } ], + "name" : "./src/Makefile", + "software_primaryPurpose" : "source", + "creationInfo" : "_:creationInfo_0" + }, { + "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd4", + "type" : "software_Package", + "software_copyrightText" : "NOASSERTION", + "software_downloadLocation" : "git+https://github.com/swinslow/spdx-examples.git#example1/content", + "verifiedUsing" : [ { + "type" : "PackageVerificationCode", + "algorithm" : "sha1", + "hashValue" : "9d20237bb72087e87069f96afb41c6ca2fa2a342" + } ], + "name" : "hello", + "creationInfo" : "_:creationInfo_0" + } ] +} \ No newline at end of file diff --git a/tests/test_import.py b/tests/test_import.py index b9da40d..83de4c2 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -110,3 +110,15 @@ def test_exist_vocab_entry(): assert not hasattr( r, "hasOutputs" ), "3.0.1: 'hasOutputs' entry should not be presented in 'RelationshipType'" + + +def test_direct_create(): + import spdx_python_model + + p = spdx_python_model.v3_0_1.Person() + + +def test_alias(): + from spdx_python_model import v3_0_1 as spdx + + p = spdx.Person() diff --git a/tests/test_load.py b/tests/test_load.py new file mode 100644 index 0000000..9f31eb4 --- /dev/null +++ b/tests/test_load.py @@ -0,0 +1,31 @@ +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +import importlib +import re +from pathlib import Path + +import pytest + +DATA_DIR = Path(__file__).parent / "data" + + +@pytest.mark.parametrize( + "datapath,version", + [ + pytest.param( + DATA_DIR / "3.0.1" / "example.spdx3.json", + "3.0.1", + id="3.0.1", + ), + ], +) +def test_load(datapath, version): + import spdx_python_model + + modname = "v" + re.sub(r"[^a-zA-Z0-9_]", "_", version) + + model, objset = spdx_python_model.load(datapath) + + assert isinstance(objset, model.SHACLObjectSet) + assert model is getattr(spdx_python_model, modname)