Skip to content

Commit

Permalink
feat: license factories
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Kowalleck <[email protected]>
  • Loading branch information
jkowalleck committed Sep 13, 2022
1 parent 92aea8d commit 033bad2
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 1 deletion.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ html/
/.mypy_cache

# Exlude built docs
docs/_build
docs/_build/
docs/autoapi/

45 changes: 45 additions & 0 deletions cyclonedx/exception/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# encoding: utf-8

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

"""
Exceptions relating to specific conditions that occur when factoring a model.
"""

from . import CycloneDxException


class CycloneDxFactoryException(CycloneDxException):
"""
Base exception that covers all exceptions that may be thrown during model factoring..
"""
pass


class LicenseChoiceFactoryException(CycloneDxFactoryException):
pass


class InvalidSpdxLicenseException(LicenseChoiceFactoryException):
pass


class LicenseFactoryException(CycloneDxFactoryException):
pass


class InvalidLicenseExpressionException(LicenseFactoryException):
pass
16 changes: 16 additions & 0 deletions cyclonedx/factory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# encoding: utf-8

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
88 changes: 88 additions & 0 deletions cyclonedx/factory/license.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# encoding: utf-8

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from typing import Optional

from ..exception.factory import InvalidLicenseExpressionException, InvalidSpdxLicenseException
from ..model import AttachedText, License, LicenseChoice, XsUri
from ..spdx import fixup as spdx_fixup


class LicenseFactory:
"""Factory for :class:`cyclonedx.model.License`."""

def make_from_string(self, name_or_spdx: str, *,
license_text: Optional[AttachedText] = None,
license_url: Optional[XsUri] = None) -> License:
"""Make a :class:`cyclonedx.model.License` from a string."""
try:
return self.make_with_id(name_or_spdx, license_text=license_text, license_url=license_url)
except InvalidSpdxLicenseException:
return self.make_with_name(name_or_spdx, license_text=license_text, license_url=license_url)

def make_with_id(self, spdx_id: str, *,
license_text: Optional[AttachedText] = None,
license_url: Optional[XsUri] = None) -> License:
"""Make a :class:`cyclonedx.model.License` from an SPDX-ID.
:raises InvalidSpdxLicenseException: if `spdx_id` was not known/supported SPDX-ID
"""
spdx_license_id = spdx_fixup(spdx_id)
if spdx_license_id is None:
raise InvalidSpdxLicenseException(spdx_id)
return License(spdx_license_id=spdx_license_id, license_text=license_text, license_url=license_url)

def make_with_name(self, name: str, *,
license_text: Optional[AttachedText] = None,
license_url: Optional[XsUri] = None) -> License:
"""Make a :class:`cyclonedx.model.License` with a name."""
return License(license_name=name, license_text=license_text, license_url=license_url)


class LicenseChoiceFactory:
"""Factory for :class:`cyclonedx.model.LicenseChoice`."""

def __init__(self, *, license_factory: LicenseFactory) -> None:
self.license_factory = license_factory

def make_from_string(self, expression_or_name_or_spdx: str) -> LicenseChoice:
"""Make a :class:`cyclonedx.model.LicenseChoice` from a string."""
try:
return self.make_with_compound_expression(expression_or_name_or_spdx)
except InvalidLicenseExpressionException:
return self.make_with_license(expression_or_name_or_spdx)

def make_with_compound_expression(self, compound_expression: str) -> LicenseChoice:
"""Make a :class:`cyclonedx.model.LicenseChoice` with a compound expression.
:raises InvalidLicenseExpressionException: if `expression` is not known/supported license expression
.. note::
Uses a best-effort detection of SPDX compound expression according to `SPDX license expression spec`_.
.. _SPDX license expression spec: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
"""
if compound_expression.startswith('(') and compound_expression.endswith(')'):
return LicenseChoice(license_expression=compound_expression)
raise InvalidLicenseExpressionException(compound_expression)

def make_with_license(self, name_or_spdx: str, *,
license_text: Optional[AttachedText] = None,
license_url: Optional[XsUri] = None) -> LicenseChoice:
"""Make a :class:`cyclonedx.model.LicenseChoice` with a license (name or SPDX-ID)."""
return LicenseChoice(license_=self.license_factory.make_from_string(
name_or_spdx, license_text=license_text, license_url=license_url))
46 changes: 46 additions & 0 deletions cyclonedx/spdx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# encoding: utf-8

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

__all__ = ['is_supported', 'fixup']

from json import load as json_load
from os.path import dirname, join as path_join
from typing import Dict, Optional, Set

# region init
# python's internal module loader will assure that this init-part runs only once.

# !!! this requires to ship the actual schema data with the package.
with open(path_join(dirname(__file__), 'schema', 'spdx.schema.json')) as schema:
__IDS: Set[str] = set(json_load(schema).get('enum', []))
assert len(__IDS) > 0, 'known SPDX-IDs should be non-empty set'

__IDS_LOWER_MAP: Dict[str, str] = dict((id_.lower(), id_) for id_ in __IDS)


# endregion

def is_supported(value: str) -> bool:
"""Validate a SPDX-ID according to current spec."""
return value in __IDS


def fixup(value: str) -> Optional[str]:
"""Fixup a SPDX-ID.
:returns: repaired value string, or `None` if fixup was unable to help.
"""
return __IDS_LOWER_MAP.get(value.lower())
30 changes: 30 additions & 0 deletions tests/test_factory_license.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# encoding: utf-8

# This file is part of CycloneDX Python Lib
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from unittest import TestCase

from cyclonedx.factory.license import License, LicenseChoice


class TestFactoryLicense(TestCase):
pass


class TestFactoryLicenseChoice(TestCase):
pass
26 changes: 26 additions & 0 deletions tests/test_spdx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# encoding: utf-8

# This file is part of CycloneDX Python Lib
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from unittest import TestCase

from cyclonedx.spdx import is_supported, fixup


class TestSpdx(TestCase):
pass

0 comments on commit 033bad2

Please sign in to comment.