diff --git a/pulp_rust/app/migrations/0001_initial.py b/pulp_rust/app/migrations/0001_initial.py new file mode 100644 index 0000000..b4ef764 --- /dev/null +++ b/pulp_rust/app/migrations/0001_initial.py @@ -0,0 +1,90 @@ +# Generated by Django 4.2.26 on 2025-12-02 04:28 + +from django.db import migrations, models +import django.db.models.deletion +import pulpcore.app.util + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('core', '0106_alter_artifactdistribution_distribution_ptr_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='RustContent', + fields=[ + ('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.content')), + ('name', models.CharField(db_index=True, max_length=255)), + ('vers', models.CharField(max_length=64)), + ('cksum', models.CharField(max_length=64)), + ('yanked', models.BooleanField(default=False)), + ('features', models.JSONField(blank=True, default=dict)), + ('features2', models.JSONField(blank=True, default=dict, null=True)), + ('links', models.CharField(blank=True, max_length=255, null=True)), + ('rust_version', models.CharField(blank=True, max_length=32, null=True)), + ('v', models.IntegerField(default=1)), + ('_pulp_domain', models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain')), + ], + options={ + 'default_related_name': '%(app_label)s_%(model_name)s', + 'unique_together': {('name', 'vers', '_pulp_domain')}, + }, + bases=('core.content',), + ), + migrations.CreateModel( + name='RustDistribution', + fields=[ + ('distribution_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.distribution')), + ('allow_uploads', models.BooleanField(default=True)), + ], + options={ + 'default_related_name': '%(app_label)s_%(model_name)s', + }, + bases=('core.distribution',), + ), + migrations.CreateModel( + name='RustRemote', + fields=[ + ('remote_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.remote')), + ], + options={ + 'default_related_name': '%(app_label)s_%(model_name)s', + }, + bases=('core.remote',), + ), + migrations.CreateModel( + name='RustRepository', + fields=[ + ('repository_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.repository')), + ], + options={ + 'default_related_name': '%(app_label)s_%(model_name)s', + }, + bases=('core.repository',), + ), + migrations.CreateModel( + name='RustDependency', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('req', models.CharField(max_length=255)), + ('features', models.JSONField(blank=True, default=list)), + ('optional', models.BooleanField(default=False)), + ('default_features', models.BooleanField(default=True)), + ('target', models.CharField(blank=True, max_length=255, null=True)), + ('kind', models.CharField(choices=[('normal', 'Normal'), ('dev', 'Development'), ('build', 'Build')], default='normal', max_length=16)), + ('registry', models.CharField(blank=True, max_length=512, null=True)), + ('package', models.CharField(blank=True, max_length=255, null=True)), + ('content', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependencies', to='rust.rustcontent')), + ], + options={ + 'verbose_name_plural': 'rust dependencies', + 'default_related_name': '%(app_label)s_%(model_name)s', + 'indexes': [models.Index(fields=['content', 'kind'], name='rust_rustde_content_a46e30_idx'), models.Index(fields=['name'], name='rust_rustde_name_6a2db4_idx')], + }, + ), + ] diff --git a/pulp_rust/app/models.py b/pulp_rust/app/models.py index 660b1dd..2f9e41c 100755 --- a/pulp_rust/app/models.py +++ b/pulp_rust/app/models.py @@ -1,20 +1,11 @@ -""" -Check `Plugin Writer's Guide`_ for more details. - -.. _Plugin Writer's Guide: - https://pulpproject.org/pulpcore/docs/dev/ -""" - from logging import getLogger from django.db import models from pulpcore.plugin.models import ( Content, - ContentArtifact, Remote, Repository, - Publication, Distribution, ) from pulpcore.plugin.util import get_domain_pk @@ -24,43 +15,144 @@ class RustContent(Content): """ - The "rust" content type. + The "rust" content type representing a Cargo package version. + + This model represents a single version of a Rust crate as defined in the + Cargo registry index specification. Each instance corresponds to one line + in a package's index file. + + Fields: + name: The package name (crate name) + vers: The semantic version string (SemVer 2.0.0) + cksum: SHA256 checksum of the .crate file (tarball) + yanked: Whether this version has been yanked (removed from normal use) + features: JSON object mapping feature names to their dependencies + features2: JSON object with extended feature syntax support + links: Value from Cargo.toml manifest 'links' field (for native library linking) + rust_version: Minimum Rust version required to compile this package + v: Schema version of the index format (integer) + """ - Define fields you need for your new content type and - specify uniqueness constraint to identify unit of this type. + TYPE = "rust" + repo_key_fields = ("name", "vers") - For example:: + # Package name - alphanumeric characters, hyphens, and underscores allowed + name = models.CharField(max_length=255, blank=False, null=False, db_index=True) - field1 = models.TextField() - field2 = models.IntegerField() - field3 = models.CharField() + # Semantic version string following SemVer 2.0.0 specification + vers = models.CharField(max_length=64, blank=False, null=False) - class Meta: - default_related_name = "%(app_label)s_%(model_name)s" - unique_together = ("field1", "field2") - """ + # SHA256 checksum (hex-encoded) of the .crate tarball file for verification + cksum = models.CharField(max_length=64, blank=False, null=False) - TYPE = "rust" + # Indicates if this version has been yanked (deprecated/removed from use) + # Yanked versions can still be used by existing Cargo.lock files but won't be selected + # for new builds + yanked = models.BooleanField(default=False) + + # Feature flags and compatibility + # Maps feature names to lists of features/dependencies they enable + # Example: {"default": ["std"], "std": [], "serde": ["dep:serde"]} + features = models.JSONField(default=dict, blank=True) + + # Extended feature syntax introduced in newer registry versions + # Supports more complex feature dependency expressions + features2 = models.JSONField(default=dict, blank=True, null=True) + + # Name of native library this package links to (from Cargo.toml 'links' field) + # Used to prevent multiple packages from linking the same native library + links = models.CharField(max_length=255, blank=True, null=True) + + # Minimum Rust compiler version required (MSRV - Minimum Supported Rust Version) + # Example: "1.56.0" + rust_version = models.CharField(max_length=32, blank=True, null=True) + + # Schema version of the index entry format + # Allows for future format evolution while maintaining backward compatibility + v = models.IntegerField(default=1) - name = models.CharField(blank=False, null=False) _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT) class Meta: default_related_name = "%(app_label)s_%(model_name)s" - unique_together = ("name", "_pulp_domain") + unique_together = (("name", "vers", "_pulp_domain"),) -class RustPublication(Publication): +class RustDependency(models.Model): """ - A Publication for RustContent. - - Define any additional fields for your new publication if needed. + Represents a dependency of a Cargo package version. + + Each RustContent (package version) can have multiple dependencies. + Dependencies are stored as separate records to enable efficient querying + and relationship tracking. + + Fields: + content: The package version that has this dependency + name: The dependency name as used in code (may be renamed via 'package') + req: Version requirement string (e.g., "^1.0", ">=0.2.3,<0.3") + features: List of feature flags to enable for this dependency + optional: Whether this is an optional dependency + default_features: Whether to enable the dependency's default features + target: Platform-specific conditional compilation target (e.g., "cfg(unix)") + kind: Dependency type - "normal", "dev", or "build" + registry: Alternative registry URL if dependency is from a different registry + package: Original package name if dependency was renamed in Cargo.toml """ - TYPE = "rust" + # The package version that declares this dependency + content = models.ForeignKey(RustContent, on_delete=models.CASCADE, related_name="dependencies") + + # Name of the dependency as used in the code (may differ from package name if renamed) + name = models.CharField(max_length=255, blank=False, null=False) + + # Version requirement string using Cargo's version requirement syntax + # Examples: "1.0", "^1.2.3", ">=1.0.0,<2.0.0", "*" + req = models.CharField(max_length=255, blank=False, null=False) + + # List of feature flags to enable for this dependency + # Example: ["serde", "std"] + features = models.JSONField(default=list, blank=True) + + # If true, this dependency is only included when explicitly requested via features + # Optional dependencies can be enabled as features themselves + optional = models.BooleanField(default=False) + + # Whether to enable the dependency's default feature set + # Setting to false allows for minimal builds + default_features = models.BooleanField(default=True) + + # Platform-specific target configuration (cfg expression) + # Example: "cfg(windows)", "cfg(target_arch = \"x86_64\")" + # If set, dependency only applies when the target matches + target = models.CharField(max_length=255, blank=True, null=True) + + # Type of dependency - determines when it's required during the build process + kind = models.CharField( + max_length=16, + choices=[ + ("normal", "Normal"), # Regular runtime dependency + ("dev", "Development"), # Development/test-only dependency + ("build", "Build"), # Build script dependency + ], + default="normal", + ) + + # @TODO: I suspect this isn't needed + # URL of alternative registry if dependency comes from a non-default registry + # Null means the dependency is from the same registry as the parent package + registry = models.CharField(max_length=512, blank=True, null=True) + + # Original crate name if the dependency was renamed + # Example: if 'use foo' but package is 'bar', name='foo', package='bar' + package = models.CharField(max_length=255, blank=True, null=True) class Meta: default_related_name = "%(app_label)s_%(model_name)s" + verbose_name_plural = "rust dependencies" + indexes = [ + models.Index(fields=["content", "kind"]), + models.Index(fields=["name"]), + ] class RustRemote(Remote): @@ -100,5 +192,7 @@ class RustDistribution(Distribution): TYPE = "rust" + allow_uploads = models.BooleanField(default=True) + class Meta: default_related_name = "%(app_label)s_%(model_name)s" diff --git a/pulp_rust/app/serializers.py b/pulp_rust/app/serializers.py index 4b90d0c..9bc6c64 100755 --- a/pulp_rust/app/serializers.py +++ b/pulp_rust/app/serializers.py @@ -1,51 +1,184 @@ -""" -Check `Plugin Writer's Guide`_ for more details. - -.. _Plugin Writer's Guide: - https://pulpproject.org/pulpcore/docs/dev/ -""" - from gettext import gettext as _ from rest_framework import serializers -from pulpcore.plugin import serializers as platform +from pulpcore.plugin import models as core_models +from pulpcore.plugin import serializers as core_serializers from . import models -# FIXME: SingleArtifactContentSerializer might not be the right choice for you. -# If your content type has no artifacts per content unit, use "NoArtifactContentSerializer". -# If your content type has many artifacts per content unit, use "MultipleArtifactContentSerializer" -# If you want create content through upload, use "SingleArtifactContentUploadSerializer" -# If you change this, make sure to do so on "fields" below, also. -# Make sure your choice here matches up with the create() method of your viewset. -class RustContentSerializer(platform.SingleArtifactContentSerializer): +class RustDependencySerializer(serializers.ModelSerializer): """ - A Serializer for RustContent. + Serializer for RustDependency. - Add serializers for the new fields defined in RustContent and - add those fields to the Meta class keeping fields from the parent class as well. + Represents a single dependency entry from the Cargo package index. + """ - For example:: + name = serializers.CharField( + help_text=_("Dependency name as used in code (may be renamed via 'package' field)") + ) - field1 = serializers.TextField() - field2 = serializers.IntegerField() - field3 = serializers.CharField() + req = serializers.CharField( + help_text=_("Version requirement string (e.g., '^1.0', '>=0.2.3,<0.3')") + ) + + features = serializers.ListField( + child=serializers.CharField(), + default=list, + required=False, + help_text=_("List of feature flags to enable for this dependency"), + ) + + optional = serializers.BooleanField( + default=False, required=False, help_text=_("Whether this is an optional dependency") + ) + + default_features = serializers.BooleanField( + default=True, + required=False, + help_text=_("Whether to enable the dependency's default features"), + ) + + target = serializers.CharField( + allow_null=True, + required=False, + help_text=_("Platform-specific target (e.g., 'cfg(unix)', 'cfg(windows)')"), + ) + + kind = serializers.ChoiceField( + choices=[("normal", "Normal"), ("dev", "Development"), ("build", "Build")], + default="normal", + required=False, + help_text=_( + "Dependency type: 'normal' (runtime), 'dev' (development), or 'build' (build script)" + ), + ) + + registry = serializers.CharField( + allow_null=True, + required=False, + help_text=_("Alternative registry URL if dependency is from a different registry"), + ) + + package = serializers.CharField( + allow_null=True, + required=False, + help_text=_("Original crate name if the dependency was renamed"), + ) class Meta: - fields = platform.SingleArtifactContentSerializer.Meta.fields + ( - 'field1', 'field2', 'field3' + model = models.RustDependency + fields = ( + "name", + "req", + "features", + "optional", + "default_features", + "target", + "kind", + "registry", + "package", ) - model = models.RustContent + + +class RustContentSerializer(core_serializers.SingleArtifactContentSerializer): + """ + Serializer for RustContent (Cargo package version). + + Represents a single version of a Rust crate as defined in the Cargo registry + index specification. Includes package metadata, dependencies, and features. """ + name = serializers.CharField(help_text=_("Package name (crate name)")) + + vers = serializers.CharField(help_text=_("Semantic version string (SemVer 2.0.0)")) + + dependencies = RustDependencySerializer( + many=True, required=False, help_text=_("List of dependencies for this package version") + ) + + cksum = serializers.CharField(help_text=_("SHA256 checksum of the .crate file (tarball)")) + + features = serializers.JSONField( + default=dict, + required=False, + help_text=_( + "Feature flags mapping - maps feature names to lists of features/dependencies " + "they enable" + ), + ) + + features2 = serializers.JSONField( + default=dict, + required=False, + allow_null=True, + help_text=_("Extended feature syntax support (newer registry format)"), + ) + + yanked = serializers.BooleanField( + default=False, + required=False, + help_text=_("Whether this version has been yanked (removed from normal use)"), + ) + + links = serializers.CharField( + allow_null=True, + required=False, + help_text=_("Name of native library this package links to (from Cargo.toml 'links' field)"), + ) + + v = serializers.IntegerField( + default=1, required=False, help_text=_("Schema version of the index entry format") + ) + rust_version = serializers.CharField( + allow_null=True, + required=False, + help_text=_("Minimum Rust compiler version required (MSRV)"), + ) + + def create(self, validated_data): + """Create RustContent and related dependencies.""" + dependencies_data = validated_data.pop("dependencies", []) + content = super().create(validated_data) + + # Create dependency records + for dep_data in dependencies_data: + models.RustDependency.objects.create(content=content, **dep_data) + + return content + + def update(self, instance, validated_data): + """Update RustContent and related dependencies.""" + dependencies_data = validated_data.pop("dependencies", None) + + instance = super().update(instance, validated_data) + + if dependencies_data is not None: + # Replace all dependencies + instance.dependencies.all().delete() + for dep_data in dependencies_data: + models.RustDependency.objects.create(content=instance, **dep_data) + + return instance + class Meta: - fields = platform.SingleArtifactContentSerializer.Meta.fields + fields = core_serializers.SingleArtifactContentSerializer.Meta.fields + ( + "name", + "vers", + "dependencies", + "cksum", + "features", + "features2", + "yanked", + "links", + "v", + "rust_version", + ) model = models.RustContent -class RustRemoteSerializer(platform.RemoteSerializer): +class RustRemoteSerializer(core_serializers.RemoteSerializer): """ A Serializer for RustRemote. @@ -56,11 +189,12 @@ class RustRemoteSerializer(platform.RemoteSerializer): For example:: class Meta: - validators = platform.RemoteSerializer.Meta.validators + [myValidator1, myValidator2] + validators = core_serializers.RemoteSerializer.Meta.validators + + [myValidator1, myValidator2] - By default the 'policy' field in platform.RemoteSerializer only validates the choice - 'immediate'. To add on-demand support for more 'policy' options, e.g. 'streamed' or 'on_demand', - re-define the 'policy' option as follows:: + By default the 'policy' field in core_serializers.RemoteSerializer only validates the choice + 'immediate'. To add on-demand support for more 'policy' options, e.g. 'streamed' or + 'on_demand', re-define the 'policy' option as follows:: policy = serializers.ChoiceField( help_text="The policy to use when downloading content. The possible values include: " @@ -71,11 +205,11 @@ class Meta: """ class Meta: - fields = platform.RemoteSerializer.Meta.fields + fields = core_serializers.RemoteSerializer.Meta.fields model = models.RustRemote -class RustRepositorySerializer(platform.RepositorySerializer): +class RustRepositorySerializer(core_serializers.RepositorySerializer): """ A Serializer for RustRepository. @@ -86,34 +220,16 @@ class RustRepositorySerializer(platform.RepositorySerializer): For example:: class Meta: - validators = platform.RepositorySerializer.Meta.validators + [myValidator1, myValidator2] + validators = core_serializers.RepositorySerializer.Meta.validators + + [myValidator1, myValidator2] """ class Meta: - fields = platform.RepositorySerializer.Meta.fields + fields = core_serializers.RepositorySerializer.Meta.fields model = models.RustRepository -class RustPublicationSerializer(platform.PublicationSerializer): - """ - A Serializer for RustPublication. - - Add any new fields if defined on RustPublication. - Similar to the example above, in RustContentSerializer. - Additional validators can be added to the parent validators list - - For example:: - - class Meta: - validators = platform.PublicationSerializer.Meta.validators + [myValidator1, myValidator2] - """ - - class Meta: - fields = platform.PublicationSerializer.Meta.fields - model = models.RustPublication - - -class RustDistributionSerializer(platform.DistributionSerializer): +class RustDistributionSerializer(core_serializers.DistributionSerializer): """ A Serializer for RustDistribution. @@ -124,23 +240,21 @@ class RustDistributionSerializer(platform.DistributionSerializer): For example:: class Meta: - validators = platform.DistributionSerializer.Meta.validators + [ + validators = core_serializers.DistributionSerializer.Meta.validators + [ myValidator1, myValidator2] """ - publication = platform.DetailRelatedField( + allow_uploads = serializers.BooleanField( + default=True, help_text=_("Allow packages to be uploaded to this index.") + ) + remote = core_serializers.DetailRelatedField( required=False, - help_text=_("Publication to be served"), - view_name_pattern=r"publications(-.*/.*)?-detail", - queryset=models.Publication.objects.exclude(complete=False), + help_text=_("Remote that can be used to fetch content when using pull-through caching."), + view_name_pattern=r"remotes(-.*/.*)?-detail", + queryset=core_models.Remote.objects.all(), allow_null=True, ) - # uncomment these lines and remove the publication field if not using publications - # repository_version = RepositoryVersionRelatedField( - # required=False, help_text=_("RepositoryVersion to be served"), allow_null=True - # ) - class Meta: - fields = platform.DistributionSerializer.Meta.fields + ("publication",) + fields = core_serializers.DistributionSerializer.Meta.fields + ("allow_uploads", "remote") model = models.RustDistribution diff --git a/pulp_rust/app/tasks/__init__.py b/pulp_rust/app/tasks/__init__.py index c097287..72ae454 100755 --- a/pulp_rust/app/tasks/__init__.py +++ b/pulp_rust/app/tasks/__init__.py @@ -1,2 +1 @@ -from .publishing import publish # noqa from .synchronizing import synchronize # noqa diff --git a/pulp_rust/app/tasks/publishing.py b/pulp_rust/app/tasks/publishing.py deleted file mode 100755 index ef15742..0000000 --- a/pulp_rust/app/tasks/publishing.py +++ /dev/null @@ -1,54 +0,0 @@ -import logging -import tempfile -from gettext import gettext as _ - -from pulpcore.plugin.models import ( - RepositoryVersion, - PublishedArtifact, - PublishedMetadata, - RemoteArtifact, -) - -from pulp_rust.app.models import RustPublication - - -log = logging.getLogger(__name__) - - -def publish(repository_version_pk): - """ - Create a Publication based on a RepositoryVersion. - - Args: - repository_version_pk (str): Create a publication from this repository version. - """ - repository_version = RepositoryVersion.objects.get(pk=repository_version_pk) - - log.info( - _("Publishing: repository={repo}, version={ver}").format( - repo=repository_version.repository.name, - ver=repository_version.number, - ) - ) - with tempfile.TemporaryDirectory("."): - with RustPublication.create(repository_version) as publication: - # Write any Artifacts (files) to the file system, and the database. - # - # artifact = YourArtifactWriter.write(relative_path) - # published_artifact = PublishedArtifact( - # relative_path=artifact.relative_path, - # publication=publication, - # content_artifact=artifact) - # published_artifact.save() - - # Write any metadata files to the file system, and the database. - # - # metadata = YourMetadataWriter.write(relative_path) - # metadata = PublishedMetadata( - # relative_path=os.path.basename(manifest.relative_path), - # publication=publication, - # file=File(open(manifest.relative_path, "rb"))) - # metadata.save() - pass - - log.info(_("Publication: {publication} created").format(publication=publication.pk)) diff --git a/pulp_rust/app/tasks/synchronizing.py b/pulp_rust/app/tasks/synchronizing.py index ddb7e6f..8f2064b 100755 --- a/pulp_rust/app/tasks/synchronizing.py +++ b/pulp_rust/app/tasks/synchronizing.py @@ -1,7 +1,7 @@ from gettext import gettext as _ import logging -from pulpcore.plugin.models import Artifact, ProgressReport, Remote, Repository +from pulpcore.plugin.models import Artifact, ProgressReport, Remote, Repository # noqa from pulpcore.plugin.stages import ( DeclarativeArtifact, DeclarativeContent, diff --git a/pulp_rust/app/viewsets.py b/pulp_rust/app/viewsets.py index f3501ce..c9a1adf 100755 --- a/pulp_rust/app/viewsets.py +++ b/pulp_rust/app/viewsets.py @@ -1,11 +1,5 @@ -""" -Check `Plugin Writer's Guide`_ for more details. - -.. _Plugin Writer's Guide: - https://pulpproject.org/pulpcore/docs/dev/ -""" - from django.db import transaction +from django_filters import CharFilter, BooleanFilter from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.decorators import action @@ -26,82 +20,82 @@ class RustContentFilter(core.ContentFilter): """ - FilterSet for RustContent. + FilterSet for RustContent (Cargo packages). + + Provides filtering capabilities for package name, version, and yanked status. """ + # Filter by exact package name + name = CharFilter(field_name="name") + + # Filter by exact version string + vers = CharFilter(field_name="vers") + + # Filter by checksum + cksum = CharFilter(field_name="cksum") + + # Filter by yanked status + yanked = BooleanFilter(field_name="yanked") + + # Filter by minimum Rust version requirement + rust_version = CharFilter(field_name="rust_version") + class Meta: model = models.RustContent fields = [ - # ... + "name", + "vers", + "cksum", + "yanked", + "rust_version", ] class RustContentViewSet(core.ContentViewSet): """ - A ViewSet for RustContent. + ViewSet for RustContent (Cargo package versions). - Define endpoint name which will appear in the API endpoint for this content type. - For example:: - https://pulp.example.com/pulp/api/v3/content/rust/units/ + Provides CRUD operations for Cargo package metadata including: + - Package name and version + - Dependencies with version requirements + - Feature flags + - Checksum verification + - Yanked status - Also specify queryset and serializer for RustContent. + API endpoint: /pulp/api/v3/content/rust/packages/ """ - endpoint_name = "rust" - queryset = models.RustContent.objects.all() + endpoint_name = "packages" + queryset = models.RustContent.objects.prefetch_related("dependencies").all() serializer_class = serializers.RustContentSerializer filterset_class = RustContentFilter @transaction.atomic def create(self, request): """ - Perform bookkeeping when saving Content. + Create a new RustContent (Cargo package version). - "Artifacts" need to be popped off and saved indpendently, as they are not actually part - of the Content model. + This handles creation of the package metadata along with its associated + artifact (.crate file) and dependencies. """ - return Response({}, status=status.HTTP_501_NOT_IMPLEMENTED) - # This requires some choice. Depending on the properties of your content type - whether it - # can have zero, one, or many artifacts associated with it, and whether any properties of - # the artifact bleed into the content type (such as the digest), you may want to make - # those changes here. - serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - # A single artifact per content, serializer subclasses SingleArtifactContentSerializer - # ====================================== - # _artifact = serializer.validated_data.pop("_artifact") - # # you can save model fields directly, e.g. .save(digest=_artifact.sha256) - # content = serializer.save() - # - # if content.pk: - # ContentArtifact.objects.create( - # artifact=artifact, - # content=content, - # relative_path= ?? - # ) - # ======================================= - - # Many artifacts per content, serializer subclasses MultipleArtifactContentSerializer - # ======================================= - # _artifacts = serializer.validated_data.pop("_artifacts") - # content = serializer.save() - # - # if content.pk: - # # _artifacts is a dictionary of {"relative_path": "artifact"} - # for relative_path, artifact in _artifacts.items(): - # ContentArtifact.objects.create( - # artifact=artifact, - # content=content, - # relative_path=relative_path - # ) - # ======================================== - - # No artifacts, serializer subclasses NoArtifactContentSerialier - # ======================================== - # content = serializer.save() - # ======================================== + # Extract artifact from validated data + _artifact = serializer.validated_data.pop("_artifact", None) + + # Create the content (this also creates dependencies via serializer) + content = serializer.save() + + # Associate the .crate file artifact with the content + if content.pk and _artifact: + # The relative path for the .crate file follows Cargo's naming convention: + # {name}/{name}-{version}.crate + relative_path = f"{content.name}/{content.name}-{content.vers}.crate" + + ContentArtifact.objects.create( + artifact=_artifact, content=content, relative_path=relative_path + ) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) @@ -183,43 +177,6 @@ class RustRepositoryVersionViewSet(core.RepositoryVersionViewSet): parent_viewset = RustRepositoryViewSet -class RustPublicationViewSet(core.PublicationViewSet): - """ - A ViewSet for RustPublication. - - Similar to the RustContentViewSet above, define endpoint_name, - queryset and serializer, at a minimum. - """ - - endpoint_name = "rust" - queryset = models.RustPublication.objects.exclude(complete=False) - serializer_class = serializers.RustPublicationSerializer - - # This decorator is necessary since a publish operation is asyncrounous and returns - # the id and href of the publish task. - @extend_schema( - description="Trigger an asynchronous task to publish content", - responses={202: AsyncOperationResponseSerializer}, - ) - def create(self, request): - """ - Publishes a repository. - - Either the ``repository`` or the ``repository_version`` fields can - be provided but not both at the same time. - """ - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - repository_version = serializer.validated_data.get("repository_version") - - result = dispatch( - tasks.publish, - [repository_version.repository], - kwargs={"repository_version_pk": str(repository_version.pk)}, - ) - return core.OperationPostponedResponse(result, request) - - class RustDistributionViewSet(core.DistributionViewSet): """ A ViewSet for RustDistribution. diff --git a/pyproject.toml b/pyproject.toml index b016350..6d26cc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ version = "0.0.0.dev" description = "pulp-rust plugin for the Pulp Project" readme = "README.md" authors = [ - {name="AUTHOR", email="author@email.here"}, + {name="Pulp Rpm Plugin Project Developers", email="pulp-dev@redhat.com"} ] classifiers = [ "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", @@ -30,11 +30,11 @@ dependencies = [ ] [project.urls] -Homepage = "https://example.com" -Documentation = "https://example.com" +Homepage = "https://pulpproject.org" +Documentation = "https://pulpproject.org/pulp_rust/" Repository = "https://github.com/pulp/pulp_rust" "Bug Tracker" = "https://github.com/pulp/pulp_rust/issues" -Changelog = "https://example.com/changes/" +Changelog = "https://pulpproject.org/pulp_rust/changes/" [project.entry-points."pulpcore.plugin"]