Skip to content

Commit 10d849f

Browse files
committed
add basic sbom support; added pypijson representation for use in downstream
1 parent dd03afe commit 10d849f

22 files changed

Lines changed: 7181 additions & 10 deletions

‎depgather/models/__init__.py‎

Whitespace-only changes.

‎depgather/models/cyclonedx.py‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from __future__ import annotations
2+
3+
from datetime import UTC, datetime
4+
from enum import StrEnum
5+
6+
from pydantic import Field, HttpUrl
7+
8+
from depgather.models.defaultonnone import DefaultOnNoneModel
9+
10+
DEFAULT_URL = HttpUrl("https://example.invalid")
11+
12+
13+
class ComponentType(StrEnum):
14+
application = "application"
15+
framework = "framework"
16+
library = "library"
17+
container = "container"
18+
operating_system = "operating-system"
19+
device = "device"
20+
firmware = "firmware"
21+
file = "file"
22+
23+
24+
class Hash(DefaultOnNoneModel):
25+
alg: str = ""
26+
content: str = ""
27+
28+
29+
class License(DefaultOnNoneModel):
30+
id: str = ""
31+
name: str = ""
32+
33+
34+
class LicenseChoice(DefaultOnNoneModel):
35+
license: License = License()
36+
37+
38+
class OrganizationalEntity(DefaultOnNoneModel):
39+
name: str = ""
40+
urls: list[HttpUrl] = Field(default_factory=list)
41+
contacts: list[str] = Field(default_factory=list)
42+
43+
44+
class ExternalReference(DefaultOnNoneModel):
45+
type: str = ""
46+
url: HttpUrl = DEFAULT_URL
47+
comment: str = ""
48+
49+
50+
class Tool(DefaultOnNoneModel):
51+
vendor: str = ""
52+
name: str = ""
53+
version: str = ""
54+
55+
56+
class Component(DefaultOnNoneModel):
57+
type: ComponentType = ComponentType.application
58+
name: str = ""
59+
60+
version: str = ""
61+
bom_ref: str = Field("", alias="bom-ref")
62+
purl: str = ""
63+
cpe: str = ""
64+
65+
supplier: OrganizationalEntity = OrganizationalEntity()
66+
67+
hashes: list[Hash] = Field(default_factory=list)
68+
licenses: list[LicenseChoice] = Field(default_factory=list)
69+
external_references: list[ExternalReference] = Field(
70+
default_factory=list,
71+
alias="externalReferences",
72+
)
73+
74+
components: list[Component] = Field(default_factory=list)
75+
76+
77+
class Metadata(DefaultOnNoneModel):
78+
timestamp: datetime = datetime.now(tz=UTC)
79+
tools: list[Tool] = Field(default_factory=list)
80+
component: Component = Component()
81+
82+
83+
class Bom(DefaultOnNoneModel):
84+
bom_format: str = Field(alias="bomFormat", default="")
85+
spec_version: str = Field(alias="specVersion", default="")
86+
serial_number: str | None = Field(None, alias="serialNumber")
87+
version: int = 1
88+
89+
metadata: Metadata = Metadata()
90+
components: list[Component] = Field(default_factory=list)

‎depgather/models/defaultonnone.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import Any
2+
3+
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator
4+
from pydantic.fields import FieldInfo
5+
from pydantic_core import PydanticUndefined
6+
7+
8+
class DefaultOnNoneModel(BaseModel):
9+
@model_validator(mode="before")
10+
@classmethod
11+
def default_on_none(cls, values: Any) -> Any | dict[Any, Any]:
12+
if not isinstance(values, dict):
13+
return values
14+
15+
result = dict(values)
16+
17+
for name, field in cls.model_fields.items():
18+
if name in result and result[name] is None:
19+
if field.default_factory is not None:
20+
result[name] = field.default_factory()
21+
elif field.default is not None:
22+
result[name] = field.default
23+
24+
return result
25+
26+
@field_validator("*", mode="wrap")
27+
@classmethod
28+
def fallback_to_default(
29+
cls,
30+
value: Any,
31+
handler,
32+
info: ValidationInfo,
33+
) -> Any:
34+
try:
35+
return handler(value)
36+
except Exception:
37+
field: FieldInfo = cls.model_fields[info.field_name]
38+
39+
if field.default_factory is not None:
40+
return field.default_factory()
41+
42+
if field.default is not PydanticUndefined:
43+
return field.default
44+
45+
raise

‎depgather/models/pypijson.py‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from datetime import UTC, datetime
2+
3+
from pydantic import Field, HttpUrl
4+
5+
from depgather.models.defaultonnone import DefaultOnNoneModel
6+
7+
DEFAULT_URL = HttpUrl("https://example.invalid")
8+
9+
10+
class Downloads(DefaultOnNoneModel):
11+
last_day: int = 0
12+
last_month: int = 0
13+
last_week: int = 0
14+
15+
16+
class Digests(DefaultOnNoneModel):
17+
blake2b_256: str = ""
18+
md5: str = ""
19+
sha256: str = ""
20+
21+
22+
class Info(DefaultOnNoneModel):
23+
author: str = ""
24+
author_email: str = ""
25+
bugtrack_url: HttpUrl = DEFAULT_URL
26+
classifiers: list[str] = Field(default_factory=list)
27+
description: str = ""
28+
description_content_type: str = ""
29+
docs_url: HttpUrl = DEFAULT_URL
30+
download_url: HttpUrl = DEFAULT_URL
31+
downloads: Downloads = Downloads()
32+
dynamic: list[str] = Field(default_factory=list)
33+
home_page: str = ""
34+
keywords: str = ""
35+
license: str = ""
36+
license_expression: str = ""
37+
license_files: list[str] | None = None
38+
maintainer: str = ""
39+
maintainer_email: str = ""
40+
name: str = ""
41+
package_url: HttpUrl = DEFAULT_URL
42+
platform: str = ""
43+
project_url: HttpUrl = DEFAULT_URL
44+
project_urls: dict[str, HttpUrl] = Field(default_factory=dict)
45+
provides_extra: list[str] = Field(default_factory=list)
46+
release_url: HttpUrl = DEFAULT_URL
47+
requires_dist: list[str] | None = None
48+
requires_python: str = ""
49+
summary: str = ""
50+
version: str = ""
51+
yanked: bool = False
52+
yanked_reason: str = ""
53+
54+
55+
class File(DefaultOnNoneModel):
56+
comment_text: str = ""
57+
digests: Digests = Digests()
58+
downloads: int = 0
59+
filename: str = ""
60+
has_sig: bool = False
61+
md5_digest: str = ""
62+
packagetype: str = ""
63+
python_version: str = ""
64+
requires_python: str = ""
65+
size: int | None = None
66+
upload_time: datetime = datetime(1970, 1, 1, tzinfo=UTC)
67+
upload_time_iso_8601: datetime = datetime(1970, 1, 1, tzinfo=UTC)
68+
url: HttpUrl = DEFAULT_URL
69+
yanked: bool = False
70+
yanked_reason: str = ""
71+
72+
73+
class OwnershipRole(DefaultOnNoneModel):
74+
role: str = ""
75+
user: str = ""
76+
77+
78+
class Ownership(DefaultOnNoneModel):
79+
roles: list[OwnershipRole]
80+
organization: str = ""
81+
82+
83+
class Vulnerability(DefaultOnNoneModel):
84+
# PyPI's schema can evolve; keep flexible unless you need validation.
85+
pass
86+
87+
88+
class ProjectResponse(DefaultOnNoneModel):
89+
info: Info = Info()
90+
last_serial: int = 1
91+
urls: list[File] = Field(default_factory=list)
92+
vulnerabilities: list[dict] = Field(default_factory=list)
93+
ownership: Ownership | None = None

‎depgather/models/spdx.py‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from __future__ import annotations
2+
3+
from datetime import UTC, datetime
4+
from enum import StrEnum
5+
6+
from pydantic import Field, HttpUrl
7+
8+
from depgather.models.defaultonnone import DefaultOnNoneModel
9+
10+
DEFAULT_URL = HttpUrl("https://example.invalid")
11+
12+
13+
class SPDXDataLicense(StrEnum):
14+
CC0_1_0 = "CC0-1.0"
15+
16+
17+
class RelationshipType(StrEnum):
18+
depends_on = "DEPENDS_ON"
19+
dependency_of = "DEPENDENCY_OF"
20+
contains = "CONTAINS"
21+
contained_by = "CONTAINED_BY"
22+
describes = "DESCRIBES"
23+
24+
25+
class Checksum(DefaultOnNoneModel):
26+
algorithm: str = ""
27+
checksum_value: str = Field("", alias="checksumValue")
28+
29+
30+
class License(DefaultOnNoneModel):
31+
id: str = ""
32+
name: str = ""
33+
34+
35+
class ExternalReference(DefaultOnNoneModel):
36+
reference_type: str = Field("", alias="referenceType")
37+
locator: HttpUrl = DEFAULT_URL
38+
comment: str = ""
39+
40+
41+
class CreationInfo(DefaultOnNoneModel):
42+
created: datetime = datetime.now(tz=UTC)
43+
creators: list[str] = Field(default_factory=list)
44+
license_list_version: str = Field("", alias="licenseListVersion")
45+
46+
47+
class Package(DefaultOnNoneModel):
48+
name: str = ""
49+
spdx_id: str = Field("", alias="SPDXID")
50+
51+
version_info: str = Field("", alias="versionInfo")
52+
supplier: str = ""
53+
originator: str = ""
54+
55+
download_location: HttpUrl = DEFAULT_URL
56+
57+
license_concluded: str = Field("", alias="licenseConcluded")
58+
license_declared: str = Field("", alias="licenseDeclared")
59+
60+
copyright_text: str = Field("", alias="copyrightText")
61+
62+
checksums: list[Checksum] = Field(default_factory=list)
63+
external_refs: list[ExternalReference] = Field(
64+
default_factory=list,
65+
alias="externalRefs",
66+
)
67+
68+
69+
class File(DefaultOnNoneModel):
70+
file_name: str = Field("", alias="fileName")
71+
spdx_id: str = Field("", alias="SPDXID")
72+
73+
checksums: list[Checksum] = Field(default_factory=list)
74+
license_concluded: str = Field("", alias="licenseConcluded")
75+
76+
77+
class Relationship(DefaultOnNoneModel):
78+
spdx_element_id: str = Field("", alias="spdxElementId")
79+
relationship_type: RelationshipType = Field(
80+
RelationshipType.depends_on,
81+
alias="relationshipType",
82+
)
83+
related_spdx_element: str = Field(
84+
"",
85+
alias="relatedSpdxElement",
86+
)
87+
88+
89+
class SPDX(DefaultOnNoneModel):
90+
spdx_version: str = Field("SPDX-2.3", alias="spdxVersion")
91+
data_license: SPDXDataLicense = Field(
92+
SPDXDataLicense.CC0_1_0,
93+
alias="dataLicense",
94+
)
95+
96+
name: str = ""
97+
document_namespace: HttpUrl = Field(
98+
DEFAULT_URL,
99+
alias="documentNamespace",
100+
)
101+
102+
creation_info: CreationInfo = Field(
103+
default_factory=CreationInfo,
104+
alias="creationInfo",
105+
)
106+
107+
packages: list[Package] = Field(default_factory=list)
108+
files: list[File] = Field(default_factory=list)
109+
relationships: list[Relationship] = Field(default_factory=list)

0 commit comments

Comments
 (0)