Skip to content

Commit c36e4ef

Browse files
committed
First cut of API
Not totally functional, mostly framework code, bits and pieces
1 parent 81a55c1 commit c36e4ef

9 files changed

Lines changed: 625 additions & 17 deletions

File tree

pulp_rust/app/serializers.py

Lines changed: 220 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
from gettext import gettext as _
1+
import logging
22

3+
from gettext import gettext as _
4+
from django.db.utils import IntegrityError
5+
from pydantic import TypeAdapter, ValidationError
36
from rest_framework import serializers
47

58
from pulpcore.plugin import models as core_models
69
from pulpcore.plugin import serializers as core_serializers
10+
from pulpcore.plugin.util import get_domain
711

812
from . import models
913

14+
log = logging.getLogger(__name__)
15+
16+
17+
class IndexRootSerializer(serializers.Serializer):
18+
"""
19+
A Serializer for summary information of an index.
20+
"""
21+
22+
dl = serializers.CharField(help_text=_("URL of the index root"), read_only=True)
23+
api = serializers.CharField(help_text=_("URL of the API root"), read_only=True)
24+
auth_required = serializers.BooleanField(
25+
help_text=_(
26+
"Indicates whether this is a private registry that requires all operations to be authenticated"
27+
),
28+
read_only=True,
29+
)
30+
1031

1132
class RustDependencySerializer(serializers.ModelSerializer):
1233
"""
@@ -178,31 +199,187 @@ class Meta:
178199
model = models.RustContent
179200

180201

181-
class RustRemoteSerializer(core_serializers.RemoteSerializer):
202+
class RustDependencySerializer(serializers.ModelSerializer):
182203
"""
183-
A Serializer for RustRemote.
204+
Serializer for RustDependency.
184205
185-
Add any new fields if defined on RustRemote.
186-
Similar to the example above, in RustContentSerializer.
187-
Additional validators can be added to the parent validators list
206+
Represents a single dependency entry from the Cargo package index.
207+
"""
188208

189-
For example::
209+
name = serializers.CharField(
210+
help_text=_("Dependency name as used in code (may be renamed via 'package' field)")
211+
)
212+
213+
req = serializers.CharField(
214+
help_text=_("Version requirement string (e.g., '^1.0', '>=0.2.3,<0.3')")
215+
)
216+
217+
features = serializers.ListField(
218+
child=serializers.CharField(),
219+
default=list,
220+
required=False,
221+
help_text=_("List of feature flags to enable for this dependency"),
222+
)
223+
224+
optional = serializers.BooleanField(
225+
default=False, required=False, help_text=_("Whether this is an optional dependency")
226+
)
227+
228+
default_features = serializers.BooleanField(
229+
default=True,
230+
required=False,
231+
help_text=_("Whether to enable the dependency's default features"),
232+
)
233+
234+
target = serializers.CharField(
235+
allow_null=True,
236+
required=False,
237+
help_text=_("Platform-specific target (e.g., 'cfg(unix)', 'cfg(windows)')"),
238+
)
239+
240+
kind = serializers.ChoiceField(
241+
choices=[("normal", "Normal"), ("dev", "Development"), ("build", "Build")],
242+
default="normal",
243+
required=False,
244+
help_text=_(
245+
"Dependency type: 'normal' (runtime), 'dev' (development), or 'build' (build script)"
246+
),
247+
)
248+
249+
registry = serializers.CharField(
250+
allow_null=True,
251+
required=False,
252+
help_text=_("Alternative registry URL if dependency is from a different registry"),
253+
)
254+
255+
package = serializers.CharField(
256+
allow_null=True,
257+
required=False,
258+
help_text=_("Original crate name if the dependency was renamed"),
259+
)
190260

191261
class Meta:
192-
validators = core_serializers.RemoteSerializer.Meta.validators
193-
+ [myValidator1, myValidator2]
262+
model = models.RustDependency
263+
fields = (
264+
"name",
265+
"req",
266+
"features",
267+
"optional",
268+
"default_features",
269+
"target",
270+
"kind",
271+
"registry",
272+
"package",
273+
)
274+
275+
276+
class RustContentSerializer(core_serializers.SingleArtifactContentSerializer):
277+
"""
278+
Serializer for RustContent (Cargo package version).
279+
280+
Represents a single version of a Rust crate as defined in the Cargo registry
281+
index specification. Includes package metadata, dependencies, and features.
282+
"""
283+
284+
name = serializers.CharField(help_text=_("Package name (crate name)"))
285+
286+
vers = serializers.CharField(help_text=_("Semantic version string (SemVer 2.0.0)"))
194287

195-
By default the 'policy' field in core_serializers.RemoteSerializer only validates the choice
196-
'immediate'. To add on-demand support for more 'policy' options, e.g. 'streamed' or
197-
'on_demand', re-define the 'policy' option as follows::
288+
dependencies = RustDependencySerializer(
289+
many=True, required=False, help_text=_("List of dependencies for this package version")
290+
)
291+
292+
cksum = serializers.CharField(help_text=_("SHA256 checksum of the .crate file (tarball)"))
293+
294+
features = serializers.JSONField(
295+
default=dict,
296+
required=False,
297+
help_text=_(
298+
"Feature flags mapping - maps feature names to lists of features/dependencies "
299+
"they enable"
300+
),
301+
)
302+
303+
features2 = serializers.JSONField(
304+
default=dict,
305+
required=False,
306+
allow_null=True,
307+
help_text=_("Extended feature syntax support (newer registry format)"),
308+
)
309+
310+
yanked = serializers.BooleanField(
311+
default=False,
312+
required=False,
313+
help_text=_("Whether this version has been yanked (removed from normal use)"),
314+
)
315+
316+
links = serializers.CharField(
317+
allow_null=True,
318+
required=False,
319+
help_text=_("Name of native library this package links to (from Cargo.toml 'links' field)"),
320+
)
321+
322+
v = serializers.IntegerField(
323+
default=1, required=False, help_text=_("Schema version of the index entry format")
324+
)
325+
rust_version = serializers.CharField(
326+
allow_null=True,
327+
required=False,
328+
help_text=_("Minimum Rust compiler version required (MSRV)"),
329+
)
330+
331+
def create(self, validated_data):
332+
"""Create RustContent and related dependencies."""
333+
dependencies_data = validated_data.pop("dependencies", [])
334+
content = super().create(validated_data)
335+
336+
# Create dependency records
337+
for dep_data in dependencies_data:
338+
models.RustDependency.objects.create(content=content, **dep_data)
339+
340+
return content
341+
342+
def update(self, instance, validated_data):
343+
"""Update RustContent and related dependencies."""
344+
dependencies_data = validated_data.pop("dependencies", None)
345+
346+
instance = super().update(instance, validated_data)
347+
348+
if dependencies_data is not None:
349+
# Replace all dependencies
350+
instance.dependencies.all().delete()
351+
for dep_data in dependencies_data:
352+
models.RustDependency.objects.create(content=instance, **dep_data)
353+
354+
return instance
355+
356+
class Meta:
357+
fields = core_serializers.SingleArtifactContentSerializer.Meta.fields + (
358+
"name",
359+
"vers",
360+
"dependencies",
361+
"cksum",
362+
"features",
363+
"features2",
364+
"yanked",
365+
"links",
366+
"v",
367+
"rust_version",
368+
)
369+
model = models.RustContent
370+
371+
372+
class RustRemoteSerializer(core_serializers.RemoteSerializer):
373+
"""
374+
A Serializer for RustRemote.
375+
"""
198376

199377
policy = serializers.ChoiceField(
200378
help_text="The policy to use when downloading content. The possible values include: "
201-
"'immediate', 'on_demand', and 'streamed'. 'immediate' is the default.",
379+
"'immediate', 'on_demand', and 'streamed'. 'streamed' is the default.",
202380
choices=models.Remote.POLICY_CHOICES,
203-
default=models.Remote.IMMEDIATE
381+
default=models.Remote.STREAMED,
204382
)
205-
"""
206383

207384
class Meta:
208385
fields = core_serializers.RemoteSerializer.Meta.fields
@@ -258,3 +435,31 @@ class Meta:
258435
class Meta:
259436
fields = core_serializers.DistributionSerializer.Meta.fields + ("allow_uploads", "remote")
260437
model = models.RustDistribution
438+
439+
440+
class RepositoryAddCachedContentSerializer(
441+
core_serializers.ValidateFieldsMixin, serializers.Serializer
442+
):
443+
remote = core_serializers.DetailRelatedField(
444+
required=False,
445+
view_name_pattern=r"remotes(-.*/.*)-detail",
446+
queryset=models.Remote.objects.all(),
447+
help_text=_(
448+
"A remote to use to identify content that was cached. This will override a "
449+
"remote set on repository."
450+
),
451+
)
452+
453+
def validate(self, data):
454+
data = super().validate(data)
455+
repository = None
456+
if "repository_pk" in self.context:
457+
repository = models.Repository.objects.get(pk=self.context["repository_pk"])
458+
remote = data.get("remote", None) or getattr(repository, "remote", None)
459+
460+
if not remote:
461+
raise serializers.ValidationError(
462+
{"remote": _("This field is required since a remote is not set on the repository.")}
463+
)
464+
self.check_cross_domains({"repository": repository, "remote": remote})
465+
return data

pulp_rust/app/tasks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .synchronizing import synchronize # noqa
2+
from .streaming import add_cached_content_to_repository # noqa

pulp_rust/app/tasks/streaming.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import datetime
2+
3+
from asgiref.sync import sync_to_async
4+
5+
from pulpcore.plugin.models import Content, ContentArtifact, RemoteArtifact
6+
from pulpcore.plugin.tasking import add_and_remove
7+
8+
from pulp_rust.app.models import RustRemote, RustRepository
9+
10+
11+
async def aadd_and_remove(*args, **kwargs):
12+
return await sync_to_async(add_and_remove)(*args, **kwargs)
13+
14+
15+
def add_cached_content_to_repository(repository_pk=None, remote_pk=None):
16+
"""
17+
Create a new repository version by adding content that was cached by pulpcore-content when
18+
streaming it from a remote.
19+
20+
Args:
21+
repository_pk (uuid): The primary key for a Repository for which a new Repository Version
22+
should be created.
23+
remote_pk (uuid): The primary key for a Remote which will be used to identify Content
24+
created by pulpcore-content when it streamed it to clients.
25+
"""
26+
repository = RustRepository.objects.get(pk=repository_pk)
27+
remote = RustRemote.objects.get(pk=remote_pk)
28+
29+
latest_version = repository.latest_version()
30+
31+
if latest_version.number == 0:
32+
date_min = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
33+
else:
34+
date_min = latest_version.pulp_created
35+
with repository.new_version(base_version=None) as new_version:
36+
ca_id_list = RemoteArtifact.objects.filter(
37+
remote=remote, pulp_created__gte=date_min
38+
).values_list("content_artifact")
39+
content_list = ContentArtifact.objects.filter(pk__in=ca_id_list).values_list("content")
40+
new_version.add_content(Content.objects.filter(pk__in=content_list))

pulp_rust/app/tasks/synchronizing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ async def run(self):
7373
downloader = self.remote.get_downloader(url=self.remote.url)
7474
result = await downloader.run()
7575
# Use ProgressReport to report progress
76+
raise NotImplemented("Not implemented")
77+
7678
for entry in self.read_my_metadata_file_somehow(result.path):
7779
unit = RustContent(entry) # make the content unit in memory-only
7880
artifact = Artifact(entry) # make Artifact in memory-only

pulp_rust/app/urls.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.conf import settings
2+
from django.urls import path, re_path
3+
4+
from pulp_rust.app.views import IndexRoot, CargoApiViewSet
5+
6+
if settings.DOMAIN_ENABLED:
7+
path_re = r"(?P<pulp_domain>[-a-zA-Z0-9_]+)/(?P<name>[\w-]+)/(?P<path>.*)"
8+
CRATES_IO_URL = "<slug:pulp_domain>/<path:path>/"
9+
else:
10+
path_re = r"(?P<name>[\w-]+)/(?P<path>.*)"
11+
CRATES_IO_URL = "<path:path>/"
12+
13+
14+
urlpatterns = [
15+
path(CRATES_IO_URL, IndexRoot.as_view({"get": "retrieve"}), name="crates-io-root"),
16+
re_path(rf"^pulp/cargo/{path_re}$", CargoApiViewSet.as_view({"get": "retrieve"})),
17+
]

0 commit comments

Comments
 (0)