Skip to content

Commit a9745be

Browse files
committed
Add PackageMetadataFile model to store metadata files in packagedb
- Add PackageMetadataFile model with ForeignKey to Package - Store filename, filetype, content, download_url and sha1 - Add migration for new model - Add test for PackageMetadataFile creation Closes #840
1 parent 469c506 commit a9745be

File tree

3 files changed

+165
-1
lines changed

3 files changed

+165
-1
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Generated by Django 5.1.13 on 2026-03-20 11:29
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("packagedb", "0094_package_packagedb_p_package_d39839_idx"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="PackageMetadataFile",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
(
27+
"filename",
28+
models.CharField(
29+
help_text="Name of the metadata file, e.g. 'package.json'",
30+
max_length=255,
31+
),
32+
),
33+
(
34+
"filetype",
35+
models.CharField(
36+
blank=True,
37+
help_text="Type of metadata file, e.g. 'npm', 'pypi', 'maven'",
38+
max_length=64,
39+
null=True,
40+
),
41+
),
42+
(
43+
"content",
44+
models.TextField(
45+
blank=True,
46+
help_text="The raw text content of the metadata file",
47+
null=True,
48+
),
49+
),
50+
(
51+
"download_url",
52+
models.CharField(
53+
blank=True,
54+
help_text="URL from which this metadata file was retrieved",
55+
max_length=2048,
56+
null=True,
57+
),
58+
),
59+
(
60+
"sha1",
61+
models.CharField(
62+
blank=True,
63+
db_index=True,
64+
help_text="SHA1 checksum of the file content",
65+
max_length=40,
66+
null=True,
67+
),
68+
),
69+
(
70+
"package",
71+
models.ForeignKey(
72+
help_text="The Package this metadata file belongs to",
73+
on_delete=django.db.models.deletion.CASCADE,
74+
related_name="metadata_files",
75+
to="packagedb.package",
76+
),
77+
),
78+
],
79+
options={
80+
"ordering": ["id"],
81+
"unique_together": {("package", "filename")},
82+
},
83+
),
84+
]

packagedb/models.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,3 +1446,66 @@ class PackageActivity(FederatedCodePackageActivityMixin):
14461446
is_processed = models.BooleanField(
14471447
default=False, help_text=_("True if this activity has been processed.")
14481448
)
1449+
1450+
class PackageMetadataFile(models.Model):
1451+
"""
1452+
Stores a metadata file associated with a Package,
1453+
such as package.json, setup.py, pom.xml, etc.
1454+
These can be federated and defederated alongside purls.
1455+
"""
1456+
1457+
package = models.ForeignKey(
1458+
Package,
1459+
related_name="metadata_files",
1460+
on_delete=models.CASCADE,
1461+
help_text=_("The Package this metadata file belongs to"),
1462+
)
1463+
1464+
filename = models.CharField(
1465+
max_length=255,
1466+
help_text=_("Name of the metadata file, e.g. 'package.json'"),
1467+
)
1468+
1469+
filetype = models.CharField(
1470+
max_length=64,
1471+
blank=True,
1472+
null=True,
1473+
help_text=_("Type of metadata file, e.g. 'npm', 'pypi', 'maven'"),
1474+
)
1475+
1476+
content = models.TextField(
1477+
blank=True,
1478+
null=True,
1479+
help_text=_("The raw text content of the metadata file"),
1480+
)
1481+
1482+
download_url = models.CharField(
1483+
max_length=2048,
1484+
blank=True,
1485+
null=True,
1486+
help_text=_("URL from which this metadata file was retrieved"),
1487+
)
1488+
1489+
sha1 = models.CharField(
1490+
max_length=40,
1491+
blank=True,
1492+
null=True,
1493+
db_index=True,
1494+
help_text=_("SHA1 checksum of the file content"),
1495+
)
1496+
1497+
class Meta:
1498+
unique_together = [("package", "filename")]
1499+
ordering = ["id"]
1500+
1501+
def __str__(self):
1502+
return f"{self.filename} for {self.package.package_url}"
1503+
1504+
def to_dict(self):
1505+
return {
1506+
"filename": self.filename,
1507+
"filetype": self.filetype,
1508+
"content": self.content,
1509+
"download_url": self.download_url,
1510+
"sha1": self.sha1,
1511+
}

packagedb/tests/test_models.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from dateutil.parser import parse as dateutil_parse
1717

18-
from packagedb.models import DependentPackage
18+
from packagedb.models import DependentPackage, PackageMetadataFile
1919
from packagedb.models import Package
2020
from packagedb.models import PackageWatch
2121
from packagedb.models import Party
@@ -494,3 +494,20 @@ def test_get_or_none(self):
494494
package = Package.objects.filter(download_url="http://a.ab").get_or_none()
495495
assert package
496496
assert Package.objects.filter(download_url="http://a.ab-foobar").get_or_none() is None
497+
def test_package_metadata_file_creation(self):
498+
package = Package.objects.create(
499+
download_url="https://example.com/package.tar.gz",
500+
type="pypi",
501+
name="example-pkg",
502+
version="1.0.0",
503+
)
504+
metadata_file = PackageMetadataFile.objects.create(
505+
package=package,
506+
filename="setup.py",
507+
filetype="pypi",
508+
content="from setuptools import setup\nsetup(name='example-pkg')",
509+
sha1="da39a3ee5e6b4b0d3255bfef95601890afd80709",
510+
)
511+
assert metadata_file.filename == "setup.py"
512+
assert metadata_file.package == package
513+
assert str(metadata_file) == "setup.py for pkg:pypi/example-pkg@1.0.0"

0 commit comments

Comments
 (0)