This is an automated email from the ASF dual-hosted git repository. arm pushed a commit to branch cyclonedx_xml in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit a87902183160f79a6582aa1122b0026560cf5106 Author: Alastair McFarlane <[email protected]> AuthorDate: Wed Mar 25 17:51:46 2026 +0000 #901 - add support for XML in sbom tooling --- atr/db/interaction.py | 2 + atr/sbom/cli.py | 23 ++++++-- atr/sbom/conformance.py | 27 +++++---- atr/sbom/cyclonedx.py | 2 +- atr/sbom/licenses.py | 22 +++++--- atr/sbom/models/bom.py | 100 --------------------------------- atr/sbom/models/bundle.py | 10 ++-- atr/sbom/osv.py | 36 +++--------- atr/sbom/tool.py | 24 ++++---- atr/sbom/utilities.py | 51 +++++++++++++---- atr/tasks/sbom.py | 12 ++-- typestubs/py_serializable/__init__.pyi | 94 +++++++++++++++++++++++++++++++ 12 files changed, 217 insertions(+), 186 deletions(-) diff --git a/atr/db/interaction.py b/atr/db/interaction.py index 7f73f555..3af3ad6e 100644 --- a/atr/db/interaction.py +++ b/atr/db/interaction.py @@ -39,6 +39,8 @@ import atr.user as user import atr.util as util import atr.web as web +# Infra-provided service account with permission to run ATR workflows +# audit_guidance required actor for ATR distribution workflows; must not be used for project TP workflows. _GITHUB_TRUSTED_ROLE_NID: Final[int] = 254436773 diff --git a/atr/sbom/cli.py b/atr/sbom/cli.py index a9cbd801..117036ac 100644 --- a/atr/sbom/cli.py +++ b/atr/sbom/cli.py @@ -20,6 +20,10 @@ import pathlib import sys import yyjson +from cyclonedx.output import make_outputter +from cyclonedx.schema import OutputFormat +from cyclonedx.schema.schema import SchemaVersion1Dot7, SCHEMA_VERSIONS +from cyclonedx.model.bom import Bom from . import models, osv from .conformance import ntia_2021_issues @@ -57,10 +61,15 @@ def command_merge(bundle: models.bundle.Bundle) -> None: patch_ops = asyncio.run(bundle_to_ntia_patch(bundle)) if patch_ops: patch_data = patch_to_data(patch_ops) - merged = bundle.doc.patch(yyjson.Document(patch_data)) - print(merged.dumps()) + output = bundle.doc.patch(yyjson.Document(patch_data)) + else: + output = bundle.doc + if bundle.source_type == "json": + print(output.dumps()) else: - print(bundle.doc.dumps()) + print(make_outputter( + Bom.from_json(data=output.as_obj), OutputFormat.XML, bundle.spec_version + ).output_as_string(indent=2)) def command_missing(bundle: models.bundle.Bundle) -> None: @@ -154,11 +163,11 @@ def command_where(bundle: models.bundle.Bundle) -> None: case models.conformance.MissingComponentProperty(): components = bundle.bom.components primary_component = bundle.bom.metadata.component if bundle.bom.metadata else None - if (error.index is not None) and (components is not None): - print(components[error.index].model_dump_json(indent=2)) + if (error.index is not None) and len(components) > 0: + print(components[error.index].as_json(SchemaVersion1Dot7)) print() elif primary_component is not None: - print(primary_component.model_dump_json(indent=2)) + print(primary_component.as_json(SchemaVersion1Dot7)) print() @@ -172,6 +181,8 @@ def main() -> None: # noqa: C901 sys.exit(1) path = pathlib.Path(sys.argv[2]) bundle = path_to_bundle(path) + if not bundle: + raise RuntimeError("Could not load bundle") match sys.argv[1]: case "license": command_license(bundle) diff --git a/atr/sbom/conformance.py b/atr/sbom/conformance.py index 96b0f638..51a2ab2d 100644 --- a/atr/sbom/conformance.py +++ b/atr/sbom/conformance.py @@ -19,9 +19,14 @@ from __future__ import annotations import datetime import urllib.parse +from typing import TYPE_CHECKING import aiohttp import yyjson +from cyclonedx.model.component import Component + +if TYPE_CHECKING: + from cyclonedx.model.bom import Bom from . import constants, models from .maven import cache_read, cache_write @@ -234,7 +239,7 @@ def assemble_metadata_timestamp(doc: yyjson.Document, patch_ops: models.patch.Pa def ntia_2021_issues( - bom_value: models.bom.Bom, + bom_value: Bom, ) -> tuple[list[models.conformance.Missing], list[models.conformance.Missing]]: # 1. Supplier # ECMA-424 1st edition says that this is the supplier of the primary component @@ -271,12 +276,12 @@ def ntia_2021_issues( warnings: list[models.conformance.Missing] = [] errors: list[models.conformance.Missing] = [] - if bom_value.metadata is not None: + if bom_value.metadata: if bom_value.metadata.supplier is None: errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_SUPPLIER)) if bom_value.metadata.component is not None: - if bom_value.metadata.component.name is None: + if not bom_value.metadata.component.name: errors.append( models.conformance.MissingComponentProperty(property=models.conformance.ComponentProperty.NAME) ) @@ -299,19 +304,17 @@ def ntia_2021_issues( else: errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_COMPONENT)) - if bom_value.metadata.author is None: + if len(bom_value.metadata.authors) < 1: errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_AUTHOR)) - - if bom_value.metadata.timestamp is None: - errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_TIMESTAMP)) else: errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA)) - for index, component in enumerate(bom_value.components or []): + components: list[Component] = list(bom_value.components) + for index, component in enumerate(components): component_type = component.type component_friendly_name = component.name - if component_type is not None: - component_friendly_name = f"{component_type}: {component_friendly_name}" + if component_type: + component_friendly_name = f"{component_type.value}: {component_friendly_name}" if component.supplier is None: errors.append( models.conformance.MissingComponentProperty( @@ -321,7 +324,7 @@ def ntia_2021_issues( ) ) - if component.name is None: + if not component.name: errors.append( models.conformance.MissingComponentProperty( property=models.conformance.ComponentProperty.NAME, @@ -330,7 +333,7 @@ def ntia_2021_issues( ) ) - if component.version is None: + if not component.version: errors.append( models.conformance.MissingComponentProperty( property=models.conformance.ComponentProperty.VERSION, diff --git a/atr/sbom/cyclonedx.py b/atr/sbom/cyclonedx.py index 35459798..f47473c4 100644 --- a/atr/sbom/cyclonedx.py +++ b/atr/sbom/cyclonedx.py @@ -39,7 +39,7 @@ def validate_cli(bundle_value: models.bundle.Bundle) -> list[str] | None: "validate", "--fail-on-errors", "--input-format", - "json", + bundle_value.source_type, "--input-file", bundle_value.path.as_posix(), ] diff --git a/atr/sbom/licenses.py b/atr/sbom/licenses.py index 470571c8..dd8ee692 100644 --- a/atr/sbom/licenses.py +++ b/atr/sbom/licenses.py @@ -17,19 +17,27 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +from cyclonedx.model.component import Component +from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression + +if TYPE_CHECKING: + from cyclonedx.model.bom import Bom + from . import constants, models from .spdx import license_expression_atoms def check( - bom_value: models.bom.Bom, + bom_value: Bom, include_all: bool = False, ) -> tuple[list[models.licenses.Issue], list[models.licenses.Issue], list[models.licenses.Issue]]: warnings: list[models.licenses.Issue] = [] errors: list[models.licenses.Issue] = [] good: list[models.licenses.Issue] = [] - components = bom_value.components or [] + components: list[Component] = list(bom_value.components) if bom_value.metadata and bom_value.metadata.component: components = [bom_value.metadata.component, *components] @@ -45,16 +53,16 @@ def check( for license_choice in component.licenses: license_expr = None - if license_choice.expression: - license_expr = license_choice.expression - elif license_choice.license and license_choice.license.id: - license_expr = license_choice.license.id + if isinstance(license_choice, LicenseExpression): + license_expr = license_choice.value + elif isinstance(license_choice, DisjunctiveLicense): + license_expr = license_choice.id if not license_expr: continue parse_failed = False - if license_choice.expression: + if isinstance(license_choice, LicenseExpression): try: atoms = license_expression_atoms(license_expr) except ValueError: diff --git a/atr/sbom/models/bom.py b/atr/sbom/models/bom.py deleted file mode 100644 index e02dff26..00000000 --- a/atr/sbom/models/bom.py +++ /dev/null @@ -1,100 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. - -from __future__ import annotations - -import pydantic - -from .base import Lax - - -class Swid(Lax): - tag_id: str | None = pydantic.Field(default=None, alias="tagId") - - -class Supplier(Lax): - name: str | None = None - url: list[str] | None = None - - -class License(Lax): - id: str | None = None - name: str | None = None - url: str | None = None - - -class LicenseChoice(Lax): - license: License | None = None - expression: str | None = None - - -class Component(Lax): - bom_ref: str | None = pydantic.Field(default=None, alias="bom-ref") - name: str | None = None - version: str | None = None - supplier: Supplier | None = None - purl: str | None = None - cpe: str | None = None - swid: Swid | None = None - licenses: list[LicenseChoice] | None = None - scope: str | None = None - type: str | None = None - - -class ToolComponent(Lax): - name: str | None = None - version: str | None = None - description: str | None = None - supplier: Supplier | None = None - - -class ServiceComponent(Lax): - name: str | None = None - version: str | None = None - description: str | None = None - supplier: Supplier | None = None - authenticated: bool | None = None - - -class Tool(Lax): - name: str | None = None - version: str | None = None - description: str | None = None - - -class Tools(Lax): - components: list[ToolComponent] | None = None - services: list[ServiceComponent] | None = None - - -class Metadata(Lax): - author: str | None = None - timestamp: str | None = None - supplier: Supplier | None = None - component: Component | None = None - tools: Tools | list[Tool] | None = None - - -class Dependency(Lax): - ref: str - depends_on: list[str] | None = pydantic.Field(default=None, alias="dependsOn") - - -class Bom(Lax): - metadata: Metadata | None = None - components: list[Component] | None = None - dependencies: list[Dependency] | None = None diff --git a/atr/sbom/models/bundle.py b/atr/sbom/models/bundle.py index 800e70e0..5ab3d83f 100644 --- a/atr/sbom/models/bundle.py +++ b/atr/sbom/models/bundle.py @@ -18,19 +18,21 @@ from __future__ import annotations import dataclasses -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: import pathlib import yyjson - - from .bom import Bom + from cyclonedx.model.bom import Bom + from cyclonedx.schema import SchemaVersion @dataclasses.dataclass class Bundle: - doc: yyjson.Document + source_type: Literal["json", "xml"] + spec_version: SchemaVersion bom: Bom + doc: yyjson.Document path: pathlib.Path text: str diff --git a/atr/sbom/osv.py b/atr/sbom/osv.py index 227c7816..cb042f4a 100644 --- a/atr/sbom/osv.py +++ b/atr/sbom/osv.py @@ -28,6 +28,7 @@ from .utilities import get_pointer, osv_severity_to_cdx if TYPE_CHECKING: import aiohttp import yyjson + from cyclonedx.model.component import Component _DEBUG: bool = os.environ.get("DEBUG_SBOM_TOOL") == "1" _OSV_API_BASE: str = "https://api.osv.dev/v1" @@ -82,7 +83,7 @@ _SOURCE_DATABASE_NAMES = { async def scan_bundle(bundle: models.bundle.Bundle) -> tuple[list[models.osv.ComponentVulnerabilities], list[str]]: - components = bundle.bom.components or [] + components = list(bundle.bom.components) queries, ignored = _scan_bundle_build_queries(components) if _DEBUG: print(f"[DEBUG] Scanning {len(queries)} components for vulnerabilities") @@ -165,29 +166,6 @@ def _assemble_vulnerabilities(doc: yyjson.Document, patch_ops: models.patch.Patc ) -def _component_purl_with_version(component: models.bom.Component) -> str | None: - if component.purl is None: - return None - if component.version is None: - return None - version = component.version.strip() - if not version: - return None - purl = component.purl - split_index = len(purl) - question_index = purl.find("?") - if (question_index != -1) and (question_index < split_index): - split_index = question_index - hash_index = purl.find("#") - if (hash_index != -1) and (hash_index < split_index): - split_index = hash_index - if "@" in purl[:split_index]: - return purl - base = purl[:split_index] - suffix = purl[split_index:] - return f"{base}@{version}{suffix}" - - async def _fetch_vulnerabilities_for_batch( session: aiohttp.ClientSession, queries: list[dict[str, Any]], @@ -256,18 +234,18 @@ async def _paginate_query( def _scan_bundle_build_queries( - components: list[models.bom.Component], + components: list[Component], ) -> tuple[list[tuple[str, dict[str, Any]]], list[str]]: queries: list[tuple[str, dict[str, Any]]] = [] ignored = [] for component in components: - purl_with_version = _component_purl_with_version(component) + purl_with_version = component.purl if purl_with_version is None: ignored.append(component.name) continue - query = {"package": {"purl": purl_with_version}} - if component.bom_ref is not None: - queries.append((component.bom_ref, query)) + query = {"package": {"purl": str(purl_with_version)}} + if component.bom_ref: + queries.append((str(component.bom_ref), query)) return queries, ignored diff --git a/atr/sbom/tool.py b/atr/sbom/tool.py index 08e98802..4ca1447b 100644 --- a/atr/sbom/tool.py +++ b/atr/sbom/tool.py @@ -17,7 +17,6 @@ from __future__ import annotations -import datetime from typing import TYPE_CHECKING, Any, Final import semver @@ -27,6 +26,8 @@ from . import maven, models, utilities if TYPE_CHECKING: from collections.abc import Callable + from cyclonedx.model.bom import Bom, BomMetaData + _KNOWN_TOOLS: Final[dict[str, models.tool.Tool]] = { # name in file: ( canonical name, friendly name, version callable ) @@ -59,22 +60,17 @@ def outdated_version_core( return None -def plugin_outdated_version(bom_value: models.bom.Bom) -> list[models.tool.Outdated] | None: - if bom_value.metadata is None: +def plugin_outdated_version(bom_value: Bom) -> list[models.tool.Outdated] | None: + if _metadata_is_empty(bom_value.metadata): return [models.tool.OutdatedMissingMetadata()] - timestamp = bom_value.metadata.timestamp - if timestamp is None: - # This quite often isn't available - # We could use the file mtime, but that's extremely heuristic - # return OutdatedMissingTimestamp() - timestamp = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") + timestamp = bom_value.metadata.timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") tools: list[Any] = [] tools_value = bom_value.metadata.tools if isinstance(tools_value, list): tools = tools_value elif tools_value: - tools = tools_value.components or [] - services = tools_value.services or [] + tools = list(tools_value.components) + services = list(tools_value.services) tools.extend(services) errors = [] for tool in tools: @@ -102,3 +98,9 @@ def version_parse(version_str: str) -> semver.VersionInfo | None: return semver.VersionInfo.parse(version_str.lstrip("v")) except ValueError: return None + + +def _metadata_is_empty(metadata: BomMetaData) -> bool: + if metadata.component is None and metadata.supplier is None: + return True + return False diff --git a/atr/sbom/utilities.py b/atr/sbom/utilities.py index 5d4e94a7..96b0f4a8 100644 --- a/atr/sbom/utilities.py +++ b/atr/sbom/utilities.py @@ -17,10 +17,16 @@ from __future__ import annotations +import json import re -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import cvss +from cyclonedx.model import Property +from cyclonedx.model.bom import Bom +from cyclonedx.output import BaseOutput, make_outputter +from cyclonedx.schema import OutputFormat, SchemaVersion +from defusedxml import ElementTree if TYPE_CHECKING: import pathlib @@ -111,14 +117,10 @@ def get_pointer(doc: yyjson.Document, path: str) -> Any | None: raise -def get_props_from_bundle(bundle_value: models.bundle.Bundle) -> tuple[int, list[dict[str, str]]]: - version: int | None = get_pointer(bundle_value.doc, "/version") - if version is None: - version = 0 - properties: list[dict[str, str]] | None = get_pointer(bundle_value.doc, "/properties") - if properties is None: - return version, [] - return version, [p for p in properties if "asf:atr:" in p.get("name", "")] +def get_props_from_bundle(bundle_value: models.bundle.Bundle) -> tuple[int, list[Property]]: + version: int = bundle_value.bom.version + properties = bundle_value.bom.properties + return version, [p for p in properties if "asf:atr:" in p.name] def osv_severity_to_cdx(severity: list[dict[str, Any]] | None, textual: str) -> list[dict[str, str | float]] | None: @@ -138,10 +140,35 @@ def patch_to_data(patch_ops: models.patch.Patch) -> list[dict[str, Any]]: return [op.model_dump(by_alias=True, exclude_none=True) for op in patch_ops] -def path_to_bundle(path: pathlib.Path) -> models.bundle.Bundle: +def path_to_bundle(path: pathlib.Path) -> models.bundle.Bundle | None: text = path.read_text(encoding="utf-8") - bom = models.bom.Bom.model_validate_json(text) - return models.bundle.Bundle(doc=yyjson.Document(text), bom=bom, path=path, text=text) + bom: Bom | None = None + source_type: Literal["json", "xml"] | None = None + # Default to latest schema version + version_str: str | None = None + spec_version: SchemaVersion | None = None + if path.name.endswith(".json"): + bom_json: dict[str, Any] = json.loads(text) + bom = Bom.from_json(data=bom_json) + source_type = "json" + version_str = bom_json.get('specVersion', '1.7') + spec_version = SchemaVersion.from_version(version_str) + elif path.name.endswith(".xml"): + bom_xml = ElementTree.fromstring(text) + bom = Bom.from_xml(bom_xml) + tag = re.match(r"\{http://cyclonedx.org/schema/bom/(.+)}", bom_xml.tag) + spec_version = SchemaVersion.from_version(tag.group(1) or '1.7') + source_type = "xml" + if bom: + outputter: BaseOutput = make_outputter( + bom=bom, output_format=OutputFormat.JSON, schema_version=spec_version + ) + text: str = outputter.output_as_string() + if not bom or not source_type or not spec_version: + raise ValueError("Error importing BOM") + return models.bundle.Bundle( + doc=yyjson.Document(text), bom=bom, path=path, text=text, source_type=source_type, spec_version=spec_version + ) def _extract_cdx_score(type: str, score_str: str) -> dict[str, str | float]: diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py index c96e6a6e..273a4af6 100644 --- a/atr/tasks/sbom.py +++ b/atr/tasks/sbom.py @@ -83,12 +83,10 @@ class ScoreArgs(FileArgs): @checks.with_model(FileArgs) async def augment(args: FileArgs) -> results.Results | None: - project_str = str(args.project_key) - version_str = str(args.version_key) revision_str = str(args.revision_number) path_str = str(args.file_path) - base_dir = paths.get_unfinished_dir() / project_str / version_str / revision_str + base_dir = paths.get_unfinished_dir_for(args.project_key, args.version_key, args.revision_number) if not await aiofiles.os.path.isdir(base_dir): raise SBOMScoringError("Revision directory does not exist", {"base_dir": str(base_dir)}) full_path = base_dir / path_str @@ -97,6 +95,8 @@ async def augment(args: FileArgs) -> results.Results | None: raise SBOMScoringError("SBOM file does not exist", {"file_path": path_str}) # Read from the old revision bundle = sbom.utilities.path_to_bundle(full_path) + if not bundle: + raise SBOMScoringError("Could not load bundle") patch_ops = await sbom.utilities.bundle_to_ntia_patch(bundle) new_full_path: pathlib.Path | None = None new_full_path_str: str | None = None @@ -159,6 +159,8 @@ async def osv_scan(args: FileArgs) -> results.Results | None: if not (full_path_str.endswith(".cdx.json") and await aiofiles.os.path.isfile(full_path)): raise SBOMScanningError("SBOM file does not exist", {"file_path": path_str}) bundle = sbom.utilities.path_to_bundle(full_path) + if not bundle: + raise SBOMScanningError("Could not load bundle") vulnerabilities, ignored = await sbom.osv.scan_bundle(bundle) patch_ops = await sbom.utilities.bundle_to_vuln_patch(bundle, vulnerabilities) components = [] @@ -259,6 +261,8 @@ async def score_tool(args: ScoreArgs) -> results.Results | None: if not (full_path_str.endswith(".cdx.json") and await aiofiles.os.path.isfile(full_path)): raise SBOMScoringError("SBOM file does not exist", {"file_path": path_str}) bundle = sbom.utilities.path_to_bundle(full_path) + if not bundle: + raise SBOMScoringError("Could not load bundle") version, properties = sbom.utilities.get_props_from_bundle(bundle) warnings, errors = sbom.conformance.ntia_2021_issues(bundle.bom) # TODO: Could update the ATR version with a constant showing last change to the augment/scan @@ -302,7 +306,7 @@ async def score_tool(args: ScoreArgs) -> results.Results | None: vulnerabilities=[v.model_dump_json() for v in vulnerabilities], prev_licenses=[w.model_dump_json() for w in prev_licenses] if prev_licenses else None, prev_vulnerabilities=[v.model_dump_json() for v in prev_vulnerabilities] if prev_vulnerabilities else None, - atr_props=properties, + atr_props=[{p.name: p.value or ""} for p in properties], cli_errors=cli_errors, ) diff --git a/typestubs/py_serializable/__init__.pyi b/typestubs/py_serializable/__init__.pyi new file mode 100644 index 00000000..da458fc6 --- /dev/null +++ b/typestubs/py_serializable/__init__.pyi @@ -0,0 +1,94 @@ +from collections.abc import Callable +from enum import Enum +from io import TextIOBase +from typing import Any, Iterable, Self, TypeVar, overload +from xml.etree.ElementTree import Element + +_T = TypeVar("_T") +_E = TypeVar("_E") +_F = TypeVar("_F", bound=Callable[..., Any]) + + +class ViewType: ... + + +class SerializationType(str, Enum): + JSON = "JSON" + XML = "XML" + + +class XmlArraySerializationType(Enum): ... + + +class XmlStringSerializationType(Enum): ... + + +class _JsonSerializable: + @classmethod + def from_json(cls, data: dict[str, Any]) -> Self | None: ... + def as_json(self, view_: type[ViewType] | None = ...) -> str: ... + + +class _XmlSerializable: + @classmethod + def from_xml( + cls, + data: TextIOBase | Element, + default_namespace: str | None = ..., + ) -> Self | None: ... + def as_xml( + self, + view_: type[ViewType] | None = ..., + as_string: bool = ..., + element_name: str | None = ..., + xmlns: str | None = ..., + ) -> Element | str: ... + + +class ObjectMetadataLibrary: ... + + +# Typed as a no-op to avoid the Union[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]] +# that py_serializable produces using Union as a stand-in for the unsupported Intersection type. +# The serialization methods are added to individual classes via their own stubs. +@overload +def serializable_class( + cls: None = ..., + *, + name: str | None = ..., + serialization_types: Iterable[SerializationType] | None = ..., + ignore_during_deserialization: Iterable[str] | None = ..., + ignore_unknown_during_deserialization: bool = ..., +) -> Callable[[type[_T]], type[_T]]: ... +@overload +def serializable_class( + cls: type[_T], + *, + name: str | None = ..., + serialization_types: Iterable[SerializationType] | None = ..., + ignore_during_deserialization: Iterable[str] | None = ..., + ignore_unknown_during_deserialization: bool = ..., +) -> type[_T]: ... + + +@overload +def serializable_enum(cls: None = ...) -> Callable[[type[_E]], type[_E]]: ... +@overload +def serializable_enum(cls: type[_E]) -> type[_E]: ... + + +def type_mapping(type_: type) -> Callable[[_F], _F]: ... +def include_none( + view_: type[ViewType] | None = ..., none_value: Any | None = ... +) -> Callable[[_F], _F]: ... +def json_name(name: str) -> Callable[[_F], _F]: ... +def string_format(format_: str) -> Callable[[_F], _F]: ... +def view(view_: type[ViewType]) -> Callable[[_F], _F]: ... +def xml_attribute() -> Callable[[_F], _F]: ... +def xml_array( + array_type: XmlArraySerializationType, child_name: str +) -> Callable[[_F], _F]: ... +def xml_string(string_type: XmlStringSerializationType) -> Callable[[_F], _F]: ... +def xml_name(name: str) -> Callable[[_F], _F]: ... +def xml_sequence(sequence: int) -> Callable[[_F], _F]: ... +def allow_none_value_for_dict(key: str) -> Callable[[_F], _F]: ... --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
