|
73 | 73 | from commoncode.fileutils import parent_directory |
74 | 74 | from cyclonedx import model as cyclonedx_model |
75 | 75 | from cyclonedx.model import component as cyclonedx_component |
| 76 | +from cyclonedx.model import contact as cyclonedx_contact |
76 | 77 | from cyclonedx.model import license as cyclonedx_license |
77 | 78 | from extractcode import EXTRACT_SUFFIX |
78 | 79 | from licensedcode.cache import build_spdx_license_expression |
@@ -3666,6 +3667,42 @@ class AbstractPackage(models.Model): |
3666 | 3667 | class Meta: |
3667 | 3668 | abstract = True |
3668 | 3669 |
|
| 3670 | + def extract_from_parties(self, roles): |
| 3671 | + """ |
| 3672 | + Extract parties matching the given roles, deduplicated by name. |
| 3673 | +
|
| 3674 | + Args: |
| 3675 | + roles: Tuple of role strings to filter by. |
| 3676 | +
|
| 3677 | + Returns: |
| 3678 | + List of party dicts matching the specified roles, unique by name. |
| 3679 | +
|
| 3680 | + """ |
| 3681 | + seen_names = set() |
| 3682 | + results = [] |
| 3683 | + for party in self.parties or []: |
| 3684 | + if party.get("role") in roles: |
| 3685 | + name = party.get("name") |
| 3686 | + if name and name not in seen_names: |
| 3687 | + seen_names.add(name) |
| 3688 | + results.append(party) |
| 3689 | + return results |
| 3690 | + |
| 3691 | + def get_author_names(self, roles=("author", "maintainer")): |
| 3692 | + """ |
| 3693 | + Return a sorted list of party names matching the specified roles. |
| 3694 | +
|
| 3695 | + Args: |
| 3696 | + roles: Tuple of role strings to filter by. |
| 3697 | + Defaults to ("author", "maintainer"). |
| 3698 | +
|
| 3699 | + Returns: |
| 3700 | + Sorted list of party names. |
| 3701 | +
|
| 3702 | + """ |
| 3703 | + parties = self.extract_from_parties(roles=roles) |
| 3704 | + return sorted(party["name"] for party in parties) |
| 3705 | + |
3669 | 3706 |
|
3670 | 3707 | class DiscoveredPackage( |
3671 | 3708 | ProjectRelatedModel, |
@@ -3952,6 +3989,14 @@ def as_cyclonedx(self): |
3952 | 3989 | if (hash_value := getattr(self, field_name)) |
3953 | 3990 | ] |
3954 | 3991 |
|
| 3992 | + authors = [ |
| 3993 | + cyclonedx_contact.OrganizationalContact( |
| 3994 | + name=party.get("name", ""), |
| 3995 | + email=party.get("email", ""), |
| 3996 | + ) |
| 3997 | + for party in self.extract_from_parties(roles=("author", "maintainer")) |
| 3998 | + ] |
| 3999 | + |
3955 | 4000 | # Those fields are not supported natively by CycloneDX but are required to |
3956 | 4001 | # load the BOM without major data loss. |
3957 | 4002 | # See https://github.com/nexB/aboutcode-cyclonedx-taxonomy |
@@ -4012,6 +4057,7 @@ def as_cyclonedx(self): |
4012 | 4057 | properties=properties, |
4013 | 4058 | external_references=external_references, |
4014 | 4059 | evidence=evidence, |
| 4060 | + authors=authors, |
4015 | 4061 | ) |
4016 | 4062 |
|
4017 | 4063 |
|
|
0 commit comments