pulpcore 3.77.0__py3-none-any.whl → 3.78.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pulpcore might be problematic. Click here for more details.
- pulp_certguard/app/__init__.py +1 -1
- pulp_file/app/__init__.py +1 -1
- pulpcore/app/apps.py +1 -1
- pulpcore/app/management/commands/clean-up-progress-reports.py +52 -0
- pulpcore/app/models/task.py +7 -0
- pulpcore/app/serializers/user.py +65 -19
- pulpcore/app/settings.py +4 -3
- pulpcore/app/util.py +4 -3
- pulpcore/cache/cache.py +8 -3
- pulpcore/content/__init__.py +8 -0
- pulpcore/content/entrypoint.py +7 -1
- pulpcore/content/handler.py +57 -40
- pulpcore/tests/functional/api/using_plugin/test_content_cache.py +18 -2
- pulpcore/tests/functional/api/using_plugin/test_content_delivery.py +7 -7
- pulpcore/tests/performance/test_performance.py +8 -10
- pulpcore/tests/unit/models/test_task.py +44 -0
- pulpcore/tests/unit/serializers/test_user.py +86 -0
- pulpcore/tests/unit/test_viewsets.py +1 -1
- {pulpcore-3.77.0.dist-info → pulpcore-3.78.0.dist-info}/METADATA +8 -6
- {pulpcore-3.77.0.dist-info → pulpcore-3.78.0.dist-info}/RECORD +24 -21
- {pulpcore-3.77.0.dist-info → pulpcore-3.78.0.dist-info}/WHEEL +1 -1
- {pulpcore-3.77.0.dist-info → pulpcore-3.78.0.dist-info}/entry_points.txt +0 -0
- {pulpcore-3.77.0.dist-info → pulpcore-3.78.0.dist-info}/licenses/LICENSE +0 -0
- {pulpcore-3.77.0.dist-info → pulpcore-3.78.0.dist-info}/top_level.txt +0 -0
pulp_certguard/app/__init__.py
CHANGED
pulp_file/app/__init__.py
CHANGED
pulpcore/app/apps.py
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from gettext import gettext as _
|
|
2
|
+
|
|
3
|
+
from django.core.management import BaseCommand
|
|
4
|
+
from django.db.models import F
|
|
5
|
+
|
|
6
|
+
from pulpcore.app.models import ProgressReport
|
|
7
|
+
from pulpcore.constants import TASK_STATES
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Command(BaseCommand):
|
|
11
|
+
"""
|
|
12
|
+
Django management command for repairing progress-reports in inconsistent states.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
help = (
|
|
16
|
+
"Repairs issue #3609. Long-running tasks that utilize ProgressReports, which "
|
|
17
|
+
"fail or are cancelled, can leave their associated reports in state 'running'. "
|
|
18
|
+
"This script finds the ProgressReports marked as 'running', whose owning task "
|
|
19
|
+
"is in either 'cancelled or 'failed', and moves the state of the ProgressReport "
|
|
20
|
+
"to match that of the task."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def add_arguments(self, parser):
|
|
24
|
+
"""Set up arguments."""
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--dry-run",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help=_(
|
|
29
|
+
"Don't modify anything, just collect results on how many ProgressReports "
|
|
30
|
+
"are impacted."
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def handle(self, *args, **options):
|
|
35
|
+
dry_run = options["dry_run"]
|
|
36
|
+
for state in [TASK_STATES.CANCELED, TASK_STATES.FAILED]:
|
|
37
|
+
if dry_run:
|
|
38
|
+
to_be_updated = ProgressReport.objects.filter(
|
|
39
|
+
task__state__ne=F("state"), state=TASK_STATES.RUNNING, task__state=state
|
|
40
|
+
).count()
|
|
41
|
+
print(
|
|
42
|
+
_("Number of ProgressReports in inconsistent state for {} tasks: {}").format(
|
|
43
|
+
state, to_be_updated
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
updated = ProgressReport.objects.filter(
|
|
48
|
+
task__state__ne=F("state"), state=TASK_STATES.RUNNING, task__state=state
|
|
49
|
+
).update(state=state)
|
|
50
|
+
print(
|
|
51
|
+
_("Number of ProgressReports updated for {} tasks: {}").format(state, updated)
|
|
52
|
+
)
|
pulpcore/app/models/task.py
CHANGED
|
@@ -204,6 +204,10 @@ class Task(BaseModel, AutoAddObjPermsMixin):
|
|
|
204
204
|
)
|
|
205
205
|
)
|
|
206
206
|
|
|
207
|
+
def _cleanup_progress_reports(self, state):
|
|
208
|
+
"""Find any running progress-reports and set their states to the specified end-state."""
|
|
209
|
+
self.progress_reports.filter(state=TASK_STATES.RUNNING).update(state=state)
|
|
210
|
+
|
|
207
211
|
def set_completed(self):
|
|
208
212
|
"""
|
|
209
213
|
Set this Task to the completed state, save it, and log output in warning cases.
|
|
@@ -232,6 +236,7 @@ class Task(BaseModel, AutoAddObjPermsMixin):
|
|
|
232
236
|
self.pk, self.state
|
|
233
237
|
)
|
|
234
238
|
)
|
|
239
|
+
self._cleanup_progress_reports(TASK_STATES.COMPLETED)
|
|
235
240
|
|
|
236
241
|
def set_failed(self, exc, tb):
|
|
237
242
|
"""
|
|
@@ -264,6 +269,7 @@ class Task(BaseModel, AutoAddObjPermsMixin):
|
|
|
264
269
|
self.pk, self.state
|
|
265
270
|
)
|
|
266
271
|
)
|
|
272
|
+
self._cleanup_progress_reports(TASK_STATES.FAILED)
|
|
267
273
|
|
|
268
274
|
def set_canceling(self):
|
|
269
275
|
"""
|
|
@@ -317,6 +323,7 @@ class Task(BaseModel, AutoAddObjPermsMixin):
|
|
|
317
323
|
self.pk, self.state
|
|
318
324
|
)
|
|
319
325
|
)
|
|
326
|
+
self._cleanup_progress_reports(final_state)
|
|
320
327
|
|
|
321
328
|
def unblock(self):
|
|
322
329
|
# This should be safe to be called without holding the lock.
|
pulpcore/app/serializers/user.py
CHANGED
|
@@ -24,8 +24,10 @@ from pulpcore.app.serializers import (
|
|
|
24
24
|
PRNField,
|
|
25
25
|
)
|
|
26
26
|
from pulpcore.app.util import (
|
|
27
|
-
get_viewset_for_model,
|
|
28
27
|
get_request_without_query_params,
|
|
28
|
+
get_url,
|
|
29
|
+
get_prn,
|
|
30
|
+
resolve_prn,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
User = get_user_model()
|
|
@@ -60,27 +62,42 @@ class PermissionField(serializers.RelatedField):
|
|
|
60
62
|
class ContentObjectField(serializers.CharField):
|
|
61
63
|
"""Content object field"""
|
|
62
64
|
|
|
63
|
-
def to_representation(self,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
def to_representation(self, value):
|
|
66
|
+
if value is None:
|
|
67
|
+
return None
|
|
68
|
+
else:
|
|
68
69
|
request = get_request_without_query_params(self.context)
|
|
70
|
+
return get_url(value, request=request)
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
return serializer.data.get("pulp_href")
|
|
72
|
-
|
|
73
|
-
def to_internal_value(self, data):
|
|
72
|
+
def to_internal_value(self, value):
|
|
74
73
|
# ... circular import ...
|
|
75
74
|
from pulpcore.app.viewsets.base import NamedModelViewSet
|
|
76
75
|
|
|
77
|
-
if
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
if value is None:
|
|
77
|
+
return None
|
|
78
|
+
else:
|
|
79
|
+
try:
|
|
80
|
+
obj = NamedModelViewSet.get_resource(value)
|
|
81
|
+
except serializers.ValidationError:
|
|
82
|
+
raise serializers.ValidationError(_("Invalid value: {}.").format(value))
|
|
83
|
+
return obj
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ContentObjectPRNField(serializers.CharField):
|
|
87
|
+
"""Content object PRN field"""
|
|
88
|
+
|
|
89
|
+
def to_representation(self, value):
|
|
90
|
+
if value is None:
|
|
91
|
+
return None
|
|
92
|
+
else:
|
|
93
|
+
return get_prn(value)
|
|
94
|
+
|
|
95
|
+
def to_internal_value(self, value):
|
|
96
|
+
if value is None:
|
|
97
|
+
return None
|
|
98
|
+
else:
|
|
99
|
+
model, pk = resolve_prn(value)
|
|
100
|
+
return model.objects.get(pk=pk)
|
|
84
101
|
|
|
85
102
|
|
|
86
103
|
class UserGroupSerializer(serializers.ModelSerializer):
|
|
@@ -247,6 +264,13 @@ class ValidateRoleMixin:
|
|
|
247
264
|
and checks if the user/group already has the role. Does not set any value
|
|
248
265
|
in data or return anything.
|
|
249
266
|
"""
|
|
267
|
+
if "content_object" not in data:
|
|
268
|
+
raise serializers.ValidationError(
|
|
269
|
+
_(
|
|
270
|
+
"Either 'content_object' or 'content_object_prn' needs to be specified."
|
|
271
|
+
" Use 'null' for global or domain level access."
|
|
272
|
+
)
|
|
273
|
+
)
|
|
250
274
|
natural_key_args = {
|
|
251
275
|
f"{role_type}_id": data[role_type].pk,
|
|
252
276
|
"role_id": data["role"].pk,
|
|
@@ -308,8 +332,18 @@ class UserRoleSerializer(ValidateRoleMixin, ModelSerializer, NestedHyperlinkedMo
|
|
|
308
332
|
"pulp_href of the object for which role permissions should be asserted. "
|
|
309
333
|
"If set to 'null', permissions will act on either domain or model-level."
|
|
310
334
|
),
|
|
311
|
-
source="*",
|
|
312
335
|
allow_null=True,
|
|
336
|
+
required=False,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
content_object_prn = ContentObjectPRNField(
|
|
340
|
+
help_text=_(
|
|
341
|
+
"prn of the object for which role permissions should be asserted. "
|
|
342
|
+
"If set to 'null', permissions will act on either domain or model-level."
|
|
343
|
+
),
|
|
344
|
+
source="content_object",
|
|
345
|
+
allow_null=True,
|
|
346
|
+
required=False,
|
|
313
347
|
)
|
|
314
348
|
|
|
315
349
|
domain = RelatedField(
|
|
@@ -343,6 +377,7 @@ class UserRoleSerializer(ValidateRoleMixin, ModelSerializer, NestedHyperlinkedMo
|
|
|
343
377
|
fields = ModelSerializer.Meta.fields + (
|
|
344
378
|
"role",
|
|
345
379
|
"content_object",
|
|
380
|
+
"content_object_prn",
|
|
346
381
|
"description",
|
|
347
382
|
"permissions",
|
|
348
383
|
"domain",
|
|
@@ -366,8 +401,18 @@ class GroupRoleSerializer(ValidateRoleMixin, ModelSerializer, NestedHyperlinkedM
|
|
|
366
401
|
"pulp_href of the object for which role permissions should be asserted. "
|
|
367
402
|
"If set to 'null', permissions will act on the model-level."
|
|
368
403
|
),
|
|
369
|
-
source="*",
|
|
370
404
|
allow_null=True,
|
|
405
|
+
required=False,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
content_object_prn = ContentObjectPRNField(
|
|
409
|
+
help_text=_(
|
|
410
|
+
"prn of the object for which role permissions should be asserted. "
|
|
411
|
+
"If set to 'null', permissions will act on either domain or model-level."
|
|
412
|
+
),
|
|
413
|
+
source="content_object",
|
|
414
|
+
allow_null=True,
|
|
415
|
+
required=False,
|
|
371
416
|
)
|
|
372
417
|
|
|
373
418
|
domain = RelatedField(
|
|
@@ -403,6 +448,7 @@ class GroupRoleSerializer(ValidateRoleMixin, ModelSerializer, NestedHyperlinkedM
|
|
|
403
448
|
fields = ModelSerializer.Meta.fields + (
|
|
404
449
|
"role",
|
|
405
450
|
"content_object",
|
|
451
|
+
"content_object_prn",
|
|
406
452
|
"description",
|
|
407
453
|
"permissions",
|
|
408
454
|
"domain",
|
pulpcore/app/settings.py
CHANGED
|
@@ -342,9 +342,7 @@ CACHE_SETTINGS = {
|
|
|
342
342
|
"EXPIRES_TTL": 600, # 10 minutes
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
# The time a RemoteArtifact will be ignored after failure.
|
|
346
|
-
# In on-demand, if a fetching content from a remote failed due to corrupt data,
|
|
347
|
-
# the corresponding RemoteArtifact will be ignored for that time (seconds).
|
|
345
|
+
# The time in seconds a RemoteArtifact will be ignored after failure.
|
|
348
346
|
REMOTE_CONTENT_FETCH_FAILURE_COOLDOWN = 5 * 60 # 5 minutes
|
|
349
347
|
|
|
350
348
|
SPECTACULAR_SETTINGS = {
|
|
@@ -425,6 +423,9 @@ KAFKA_SASL_PASSWORD = None
|
|
|
425
423
|
# opentelemetry settings
|
|
426
424
|
OTEL_ENABLED = False
|
|
427
425
|
|
|
426
|
+
# Replaces asyncio event loop with uvloop
|
|
427
|
+
UVLOOP_ENABLED = False
|
|
428
|
+
|
|
428
429
|
# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
|
|
429
430
|
# Read more at https://www.dynaconf.com/django/
|
|
430
431
|
|
pulpcore/app/util.py
CHANGED
|
@@ -66,18 +66,19 @@ def get_url(model, domain=None, request=None):
|
|
|
66
66
|
str: The path component of the resource url
|
|
67
67
|
"""
|
|
68
68
|
kwargs = {}
|
|
69
|
-
view_action = "list"
|
|
70
69
|
if settings.DOMAIN_ENABLED:
|
|
71
70
|
kwargs["pulp_domain"] = "default"
|
|
72
|
-
if not domain and hasattr(model, "pulp_domain") and isinstance(model,
|
|
71
|
+
if not domain and hasattr(model, "pulp_domain") and isinstance(model, Model):
|
|
73
72
|
kwargs["pulp_domain"] = model.pulp_domain.name
|
|
74
73
|
elif isinstance(domain, models.Domain):
|
|
75
74
|
kwargs["pulp_domain"] = domain.name
|
|
76
75
|
elif isinstance(domain, str):
|
|
77
76
|
kwargs["pulp_domain"] = domain
|
|
78
|
-
if isinstance(model,
|
|
77
|
+
if isinstance(model, Model):
|
|
79
78
|
view_action = "detail"
|
|
80
79
|
kwargs["pk"] = model.pk
|
|
80
|
+
else:
|
|
81
|
+
view_action = "list"
|
|
81
82
|
|
|
82
83
|
return reverse(get_view_name_for_model(model, view_action), kwargs=kwargs, request=request)
|
|
83
84
|
|
pulpcore/cache/cache.py
CHANGED
|
@@ -8,7 +8,7 @@ from django.http import HttpResponseRedirect, HttpResponse, FileResponse as ApiF
|
|
|
8
8
|
|
|
9
9
|
from rest_framework.request import Request as ApiRequest
|
|
10
10
|
|
|
11
|
-
from aiohttp.web import FileResponse, Response, HTTPSuccessful, Request
|
|
11
|
+
from aiohttp.web import FileResponse, Response, HTTPSuccessful, Request, StreamResponse
|
|
12
12
|
from aiohttp.web_exceptions import HTTPFound
|
|
13
13
|
|
|
14
14
|
from redis import ConnectionError
|
|
@@ -401,6 +401,11 @@ class AsyncContentCache(AsyncCache):
|
|
|
401
401
|
except (HTTPSuccessful, HTTPFound) as e:
|
|
402
402
|
response = e
|
|
403
403
|
|
|
404
|
+
original_response = response
|
|
405
|
+
if isinstance(response, StreamResponse):
|
|
406
|
+
if hasattr(response, "future_response"):
|
|
407
|
+
response = response.future_response
|
|
408
|
+
|
|
404
409
|
entry = {"headers": dict(response.headers), "status": response.status}
|
|
405
410
|
if expires is not None:
|
|
406
411
|
# Redis TTL is not sufficient: https://github.com/pulp/pulpcore/issues/4845
|
|
@@ -427,12 +432,12 @@ class AsyncContentCache(AsyncCache):
|
|
|
427
432
|
entry["location"] = str(response.location)
|
|
428
433
|
entry["type"] = "Redirect"
|
|
429
434
|
else:
|
|
430
|
-
# We don't cache
|
|
435
|
+
# We don't cache errors
|
|
431
436
|
return response
|
|
432
437
|
|
|
433
438
|
# TODO look into smaller format, maybe some compression on the text
|
|
434
439
|
await self.set(key, json.dumps(entry), expires, base_key=base_key)
|
|
435
|
-
return
|
|
440
|
+
return original_response
|
|
436
441
|
|
|
437
442
|
def make_key(self, request):
|
|
438
443
|
"""Makes the key based off the request"""
|
pulpcore/content/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from contextlib import suppress
|
|
3
3
|
from importlib import import_module
|
|
4
|
+
from importlib.util import find_spec
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
|
|
@@ -35,6 +36,13 @@ if settings.OTEL_ENABLED:
|
|
|
35
36
|
else:
|
|
36
37
|
app = web.Application(middlewares=[guid, authenticate])
|
|
37
38
|
|
|
39
|
+
|
|
40
|
+
if settings.UVLOOP_ENABLED:
|
|
41
|
+
if not find_spec("uvloop"):
|
|
42
|
+
raise RuntimeError("The library 'uvloop' must be installed if UVLOOP_ENABLED is true.")
|
|
43
|
+
log.info("Using uvloop as the asyncio event loop.")
|
|
44
|
+
|
|
45
|
+
|
|
38
46
|
CONTENT_MODULE_NAME = "content"
|
|
39
47
|
|
|
40
48
|
|
pulpcore/content/entrypoint.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import click
|
|
2
2
|
from pulpcore.app.pulpcore_gunicorn_application import PulpcoreGunicornApplication
|
|
3
|
+
from django.conf import settings
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class PulpcoreContentApplication(PulpcoreGunicornApplication):
|
|
6
7
|
def load_app_specific_config(self):
|
|
8
|
+
worker_class = (
|
|
9
|
+
"aiohttp.GunicornUVLoopWebWorker"
|
|
10
|
+
if settings.UVLOOP_ENABLED
|
|
11
|
+
else "aiohttp.GunicornWebWorker"
|
|
12
|
+
)
|
|
7
13
|
self.set_option("default_proc_name", "pulpcore-content", enforced=True)
|
|
8
|
-
self.set_option("worker_class",
|
|
14
|
+
self.set_option("worker_class", worker_class, enforced=True)
|
|
9
15
|
|
|
10
16
|
def load(self):
|
|
11
17
|
import pulpcore.content
|
pulpcore/content/handler.py
CHANGED
|
@@ -1093,26 +1093,8 @@ class Handler:
|
|
|
1093
1093
|
ret.update({ca.relative_path: ca for ca in cas})
|
|
1094
1094
|
return ret
|
|
1095
1095
|
|
|
1096
|
-
|
|
1097
|
-
"""
|
|
1098
|
-
Handle response for a Content Artifact with the file present.
|
|
1099
|
-
|
|
1100
|
-
Depending on where the file storage (e.g. filesystem, S3, etc) this could be responding with
|
|
1101
|
-
the file (filesystem) or a redirect (S3).
|
|
1102
|
-
|
|
1103
|
-
Args:
|
|
1104
|
-
content_artifact (pulpcore.app.models.ContentArtifact) The Content Artifact to
|
|
1105
|
-
respond with.
|
|
1106
|
-
headers (dict): A dictionary of response headers.
|
|
1107
|
-
request(aiohttp.web.Request) The request to prepare a response for.
|
|
1108
|
-
|
|
1109
|
-
Raises:
|
|
1110
|
-
[aiohttp.web_exceptions.HTTPFound][]: When we need to redirect to the file
|
|
1111
|
-
NotImplementedError: If file is stored in a file storage we can't handle
|
|
1112
|
-
|
|
1113
|
-
Returns:
|
|
1114
|
-
The [aiohttp.web.FileResponse][] for the file.
|
|
1115
|
-
"""
|
|
1096
|
+
def _build_response_from_content_artifact(self, content_artifact, headers, request):
|
|
1097
|
+
"""Helper method to build the correct response to serve a ContentArtifact."""
|
|
1116
1098
|
|
|
1117
1099
|
def _set_params_from_headers(hdrs, storage_domain):
|
|
1118
1100
|
# Map standard-response-headers to storage-object-specific keys
|
|
@@ -1137,7 +1119,47 @@ class Handler:
|
|
|
1137
1119
|
artifact_name = artifact_file.name
|
|
1138
1120
|
domain = get_domain()
|
|
1139
1121
|
storage = domain.get_storage()
|
|
1122
|
+
headers["X-PULP-ARTIFACT-SIZE"] = str(artifact_file.size)
|
|
1123
|
+
|
|
1124
|
+
if domain.storage_class == "pulpcore.app.models.storage.FileSystem":
|
|
1125
|
+
path = storage.path(artifact_name)
|
|
1126
|
+
if not os.path.exists(path):
|
|
1127
|
+
raise Exception(_("Expected path '{}' is not found").format(path))
|
|
1128
|
+
return FileResponse(path, headers=headers)
|
|
1129
|
+
elif not domain.redirect_to_object_storage:
|
|
1130
|
+
return ArtifactResponse(content_artifact.artifact, headers=headers)
|
|
1131
|
+
elif domain.storage_class == "storages.backends.s3boto3.S3Boto3Storage":
|
|
1132
|
+
return HTTPFound(_build_url(http_method=request.method), headers=headers)
|
|
1133
|
+
elif domain.storage_class in (
|
|
1134
|
+
"storages.backends.azure_storage.AzureStorage",
|
|
1135
|
+
"storages.backends.gcloud.GoogleCloudStorage",
|
|
1136
|
+
):
|
|
1137
|
+
return HTTPFound(_build_url(), headers=headers)
|
|
1138
|
+
else:
|
|
1139
|
+
raise NotImplementedError()
|
|
1140
1140
|
|
|
1141
|
+
async def _serve_content_artifact(self, content_artifact, headers, request):
|
|
1142
|
+
"""
|
|
1143
|
+
Handle response for a Content Artifact with the file present.
|
|
1144
|
+
|
|
1145
|
+
Depending on where the file storage (e.g. filesystem, S3, etc) this could be responding with
|
|
1146
|
+
the file (filesystem) or a redirect (S3).
|
|
1147
|
+
|
|
1148
|
+
Args:
|
|
1149
|
+
content_artifact (pulpcore.app.models.ContentArtifact) The Content Artifact to
|
|
1150
|
+
respond with.
|
|
1151
|
+
headers (dict): A dictionary of response headers.
|
|
1152
|
+
request(aiohttp.web.Request) The request to prepare a response for.
|
|
1153
|
+
|
|
1154
|
+
Raises:
|
|
1155
|
+
[aiohttp.web_exceptions.HTTPFound][]: When we need to redirect to the file
|
|
1156
|
+
NotImplementedError: If file is stored in a file storage we can't handle
|
|
1157
|
+
HTTPRequestRangeNotSatisfiable: If the request is for a range that is not
|
|
1158
|
+
satisfiable.
|
|
1159
|
+
Returns:
|
|
1160
|
+
The [aiohttp.web.FileResponse][] for the file.
|
|
1161
|
+
"""
|
|
1162
|
+
artifact_file = content_artifact.artifact.file
|
|
1141
1163
|
content_length = artifact_file.size
|
|
1142
1164
|
|
|
1143
1165
|
try:
|
|
@@ -1152,25 +1174,13 @@ class Handler:
|
|
|
1152
1174
|
size = artifact_file.size or "*"
|
|
1153
1175
|
raise HTTPRequestRangeNotSatisfiable(headers={"Content-Range": f"bytes */{size}"})
|
|
1154
1176
|
|
|
1155
|
-
headers["X-PULP-ARTIFACT-SIZE"] = str(content_length)
|
|
1156
1177
|
artifacts_size_counter.add(content_length)
|
|
1157
1178
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
raise Exception(_("Expected path '{}' is not found").format(path))
|
|
1162
|
-
return FileResponse(path, headers=headers)
|
|
1163
|
-
elif not domain.redirect_to_object_storage:
|
|
1164
|
-
return ArtifactResponse(content_artifact.artifact, headers=headers)
|
|
1165
|
-
elif domain.storage_class == "storages.backends.s3boto3.S3Boto3Storage":
|
|
1166
|
-
raise HTTPFound(_build_url(http_method=request.method), headers=headers)
|
|
1167
|
-
elif domain.storage_class in (
|
|
1168
|
-
"storages.backends.azure_storage.AzureStorage",
|
|
1169
|
-
"storages.backends.gcloud.GoogleCloudStorage",
|
|
1170
|
-
):
|
|
1171
|
-
raise HTTPFound(_build_url(), headers=headers)
|
|
1179
|
+
response = self._build_response_from_content_artifact(content_artifact, headers, request)
|
|
1180
|
+
if isinstance(response, HTTPFound):
|
|
1181
|
+
raise response
|
|
1172
1182
|
else:
|
|
1173
|
-
|
|
1183
|
+
return response
|
|
1174
1184
|
|
|
1175
1185
|
async def _stream_remote_artifact(
|
|
1176
1186
|
self, request, response, remote_artifact, save_artifact=True, repository=None
|
|
@@ -1214,6 +1224,7 @@ class Handler:
|
|
|
1214
1224
|
size = remote_artifact.size or "*"
|
|
1215
1225
|
raise HTTPRequestRangeNotSatisfiable(headers={"Content-Range": f"bytes */{size}"})
|
|
1216
1226
|
|
|
1227
|
+
original_headers = response.headers.copy()
|
|
1217
1228
|
actual_content_length = None
|
|
1218
1229
|
|
|
1219
1230
|
if range_start or range_stop:
|
|
@@ -1303,10 +1314,9 @@ class Handler:
|
|
|
1303
1314
|
except DigestValidationError:
|
|
1304
1315
|
remote_artifact.failed_at = timezone.now()
|
|
1305
1316
|
await remote_artifact.asave()
|
|
1306
|
-
await downloader.session.close()
|
|
1307
1317
|
close_tcp_connection(request.transport._sock)
|
|
1308
1318
|
REMOTE_CONTENT_FETCH_FAILURE_COOLDOWN = settings.REMOTE_CONTENT_FETCH_FAILURE_COOLDOWN
|
|
1309
|
-
|
|
1319
|
+
log.error(
|
|
1310
1320
|
f"Pulp tried streaming {remote_artifact.url!r} to "
|
|
1311
1321
|
"the client, but it failed checksum validation.\n\n"
|
|
1312
1322
|
"We can't recover from wrong data already sent so we are:\n"
|
|
@@ -1316,16 +1326,23 @@ class Handler:
|
|
|
1316
1326
|
"If the Remote is known to be fixed, try resyncing the associated repository.\n"
|
|
1317
1327
|
"If the Remote is known to be permanently corrupted, try removing "
|
|
1318
1328
|
"affected Pulp Remote, adding a good one and resyncing.\n"
|
|
1319
|
-
"
|
|
1329
|
+
"Learn more on <https://pulpproject.org/pulpcore/docs/user/learn/"
|
|
1330
|
+
"on-demand-downloading/#on-demand-and-streamed-limitations>"
|
|
1320
1331
|
)
|
|
1332
|
+
return response
|
|
1321
1333
|
|
|
1322
1334
|
if save_artifact and remote.policy != Remote.STREAMED:
|
|
1323
1335
|
content_artifacts = await asyncio.shield(
|
|
1324
1336
|
sync_to_async(self._save_artifact)(download_result, remote_artifact, request)
|
|
1325
1337
|
)
|
|
1338
|
+
ca = content_artifacts[remote_artifact.content_artifact.relative_path]
|
|
1339
|
+
# If cache is enabled, add the future response to our stream response
|
|
1340
|
+
if settings.CACHE_ENABLED:
|
|
1341
|
+
response.future_response = self._build_response_from_content_artifact(
|
|
1342
|
+
ca, original_headers, request
|
|
1343
|
+
)
|
|
1326
1344
|
# Try to add content to repository if present & supported
|
|
1327
1345
|
if repository and repository.PULL_THROUGH_SUPPORTED:
|
|
1328
|
-
ca = content_artifacts[remote_artifact.content_artifact.relative_path]
|
|
1329
1346
|
await sync_to_async(repository.pull_through_add_content)(ca)
|
|
1330
1347
|
await response.write_eof()
|
|
1331
1348
|
|
|
@@ -16,7 +16,7 @@ from pulpcore.tests.functional.utils import get_from_url
|
|
|
16
16
|
@pytest.mark.parallel
|
|
17
17
|
def test_full_workflow(
|
|
18
18
|
file_repo_with_auto_publish,
|
|
19
|
-
|
|
19
|
+
duplicate_filename_paths,
|
|
20
20
|
file_remote_factory,
|
|
21
21
|
file_bindings,
|
|
22
22
|
distribution_base_url,
|
|
@@ -37,7 +37,8 @@ def test_full_workflow(
|
|
|
37
37
|
return r.status, r.headers.get("X-PULP-CACHE")
|
|
38
38
|
|
|
39
39
|
# Sync from the remote and assert that a new repository version is created
|
|
40
|
-
|
|
40
|
+
manifest_1, manifest_2 = duplicate_filename_paths
|
|
41
|
+
remote = file_remote_factory(manifest_path=manifest_1, policy="immediate")
|
|
41
42
|
body = RepositorySyncURL(remote=remote.pulp_href)
|
|
42
43
|
monitor_task(
|
|
43
44
|
file_bindings.RepositoriesFileApi.sync(file_repo_with_auto_publish.pulp_href, body).task
|
|
@@ -129,6 +130,21 @@ def test_full_workflow(
|
|
|
129
130
|
url = urljoin(distro_base_url, file)
|
|
130
131
|
assert (200, "HIT" if i % 2 == 1 else "MISS") == _check_cache(url), file
|
|
131
132
|
|
|
133
|
+
# Sync a new remote with same filenames but on-demand
|
|
134
|
+
remote = file_remote_factory(manifest_path=manifest_2, policy="on_demand")
|
|
135
|
+
body = RepositorySyncURL(remote=remote.pulp_href)
|
|
136
|
+
monitor_task(
|
|
137
|
+
file_bindings.RepositoriesFileApi.sync(file_repo_with_auto_publish.pulp_href, body).task
|
|
138
|
+
)
|
|
139
|
+
repo = file_bindings.RepositoriesFileApi.read(file_repo_with_auto_publish.pulp_href)
|
|
140
|
+
assert repo.latest_version_href.endswith("/versions/3/")
|
|
141
|
+
|
|
142
|
+
# Test that cache is invalidated from sync, but on-demand responses are immediately cached
|
|
143
|
+
files = ["1.iso", "1.iso", "2.iso", "2.iso", "3.iso", "3.iso"]
|
|
144
|
+
for i, file in enumerate(files):
|
|
145
|
+
url = urljoin(distro_base_url, file)
|
|
146
|
+
assert (200, "HIT" if i % 2 == 1 else None) == _check_cache(url), file
|
|
147
|
+
|
|
132
148
|
# Tests that deleting a repository invalidates the cache"""
|
|
133
149
|
monitor_task(file_bindings.RepositoriesFileApi.delete(repo.pulp_href).task)
|
|
134
150
|
files = ["", "PULP_MANIFEST", "2.iso"]
|
|
@@ -117,6 +117,7 @@ def test_remote_content_changed_with_on_demand(
|
|
|
117
117
|
file_bindings,
|
|
118
118
|
monitor_task,
|
|
119
119
|
file_distribution_factory,
|
|
120
|
+
tmp_path,
|
|
120
121
|
):
|
|
121
122
|
"""
|
|
122
123
|
GIVEN a remote synced on demand with fileA (e.g, digest=123),
|
|
@@ -124,6 +125,7 @@ def test_remote_content_changed_with_on_demand(
|
|
|
124
125
|
|
|
125
126
|
WHEN the client first requests that content
|
|
126
127
|
THEN the content app will start a response but close the connection before finishing
|
|
128
|
+
AND no file will be present in the filesystem
|
|
127
129
|
|
|
128
130
|
WHEN the client requests that content again (within the RA cooldown interval)
|
|
129
131
|
THEN the content app will return a 404
|
|
@@ -143,9 +145,12 @@ def test_remote_content_changed_with_on_demand(
|
|
|
143
145
|
get_url = urljoin(distribution_base_url(distribution.base_url), expected_file_list[0][0])
|
|
144
146
|
|
|
145
147
|
# WHEN (first request)
|
|
146
|
-
|
|
148
|
+
output_file = tmp_path / "out.rpm"
|
|
149
|
+
cmd = ["curl", "-v", get_url, "-o", str(output_file)]
|
|
150
|
+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
147
151
|
|
|
148
152
|
# THEN
|
|
153
|
+
assert not output_file.exists()
|
|
149
154
|
assert result.returncode == 18
|
|
150
155
|
assert b"* Closing connection 0" in result.stderr
|
|
151
156
|
assert b"curl: (18) transfer closed with outstanding read data remaining" in result.stderr
|
|
@@ -211,10 +216,6 @@ def test_handling_remote_artifact_on_demand_streaming_failure(
|
|
|
211
216
|
distribution = file_distribution_factory(repository=repo.pulp_href)
|
|
212
217
|
return distribution
|
|
213
218
|
|
|
214
|
-
def refresh_acs(acs):
|
|
215
|
-
monitor_task_group(file_bindings.AcsFileApi.refresh(acs.pulp_href).task_group)
|
|
216
|
-
return acs
|
|
217
|
-
|
|
218
219
|
def get_original_content_info(remote):
|
|
219
220
|
expected_files = get_files_in_manifest(remote.url)
|
|
220
221
|
content_unit = list(expected_files)[0]
|
|
@@ -231,8 +232,7 @@ def test_handling_remote_artifact_on_demand_streaming_failure(
|
|
|
231
232
|
acs_manifest_path = write_3_iso_file_fixture_data_factory("acs", seed=123)
|
|
232
233
|
remote = create_simple_remote(basic_manifest_path)
|
|
233
234
|
distribution = sync_publish_and_distribute(remote)
|
|
234
|
-
|
|
235
|
-
refresh_acs(acs)
|
|
235
|
+
create_acs_remote(acs_manifest_path)
|
|
236
236
|
write_3_iso_file_fixture_data_factory("acs", overwrite=True) # corrupt
|
|
237
237
|
|
|
238
238
|
# WHEN/THEN (first request)
|
|
@@ -9,8 +9,6 @@ from collections import namedtuple
|
|
|
9
9
|
from urllib.parse import urljoin
|
|
10
10
|
from uuid import uuid4
|
|
11
11
|
|
|
12
|
-
from .pulpperf import reporting
|
|
13
|
-
|
|
14
12
|
Args = namedtuple("Arguments", "limit processes repositories")
|
|
15
13
|
|
|
16
14
|
|
|
@@ -92,7 +90,7 @@ def test_performance(
|
|
|
92
90
|
responses.append(response)
|
|
93
91
|
|
|
94
92
|
results = [monitor_task(response.task) for response in responses]
|
|
95
|
-
|
|
93
|
+
report_tasks_stats("Sync tasks", results)
|
|
96
94
|
|
|
97
95
|
"""Measure time of resynchronization."""
|
|
98
96
|
responses = []
|
|
@@ -102,7 +100,7 @@ def test_performance(
|
|
|
102
100
|
responses.append(response)
|
|
103
101
|
|
|
104
102
|
results = [monitor_task(response.task) for response in responses]
|
|
105
|
-
|
|
103
|
+
report_tasks_stats("Resync tasks", results)
|
|
106
104
|
|
|
107
105
|
"""Measure time of repository publishing."""
|
|
108
106
|
responses = []
|
|
@@ -111,7 +109,7 @@ def test_performance(
|
|
|
111
109
|
responses.append(response)
|
|
112
110
|
|
|
113
111
|
results = [monitor_task(response.task) for response in responses]
|
|
114
|
-
|
|
112
|
+
report_tasks_stats("Publication tasks", results)
|
|
115
113
|
|
|
116
114
|
for i in range(len(results)):
|
|
117
115
|
data[i]["publication_href"] = results[i].created_resources[0]
|
|
@@ -132,7 +130,7 @@ def test_performance(
|
|
|
132
130
|
responses.append(response)
|
|
133
131
|
|
|
134
132
|
results = [monitor_task(response.task) for response in responses]
|
|
135
|
-
|
|
133
|
+
report_tasks_stats("Distribution tasks", results)
|
|
136
134
|
|
|
137
135
|
for i in range(len(results)):
|
|
138
136
|
data[i]["distribution_href"] = results[i].created_resources[0]
|
|
@@ -152,7 +150,7 @@ def test_performance(
|
|
|
152
150
|
pool.starmap(download, params)
|
|
153
151
|
|
|
154
152
|
after = datetime.datetime.utcnow()
|
|
155
|
-
|
|
153
|
+
print_fmt_experiment_time("Repository download", before, after)
|
|
156
154
|
|
|
157
155
|
"""Measure time of inspecting the repository content."""
|
|
158
156
|
before = datetime.datetime.utcnow()
|
|
@@ -172,7 +170,7 @@ def test_performance(
|
|
|
172
170
|
with multiprocessing.Pool(processes=args.processes) as pool:
|
|
173
171
|
pool.starmap(measureit, params)
|
|
174
172
|
after = datetime.datetime.utcnow()
|
|
175
|
-
|
|
173
|
+
print_fmt_experiment_time("Content inspection", before, after)
|
|
176
174
|
|
|
177
175
|
"""Measure time of repository cloning."""
|
|
178
176
|
for r in data:
|
|
@@ -192,7 +190,7 @@ def test_performance(
|
|
|
192
190
|
responses.append(response)
|
|
193
191
|
|
|
194
192
|
results = [monitor_task(response.task) for response in responses]
|
|
195
|
-
|
|
193
|
+
report_tasks_stats("Version clone with base_version tasks", results)
|
|
196
194
|
|
|
197
195
|
hrefs = [
|
|
198
196
|
i["pulp_href"]
|
|
@@ -206,7 +204,7 @@ def test_performance(
|
|
|
206
204
|
responses.append(response)
|
|
207
205
|
|
|
208
206
|
results = [monitor_task(response.task) for response in responses]
|
|
209
|
-
|
|
207
|
+
report_tasks_stats("Version clone with add_content_units tasks", results)
|
|
210
208
|
|
|
211
209
|
|
|
212
210
|
def download(base_url, file_name, file_size):
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import sys
|
|
3
|
+
from pulpcore.app.models import Task, ProgressReport
|
|
4
|
+
from pulpcore.constants import TASK_STATES
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"to_state,use_canceled",
|
|
9
|
+
[
|
|
10
|
+
(TASK_STATES.FAILED, False),
|
|
11
|
+
(TASK_STATES.CANCELED, False),
|
|
12
|
+
(TASK_STATES.CANCELED, True),
|
|
13
|
+
],
|
|
14
|
+
)
|
|
15
|
+
@pytest.mark.django_db
|
|
16
|
+
def test_report_state_changes(to_state, use_canceled):
|
|
17
|
+
task = Task.objects.create(name="test", state=TASK_STATES.RUNNING)
|
|
18
|
+
reports = {}
|
|
19
|
+
for state in vars(TASK_STATES):
|
|
20
|
+
report = ProgressReport(message="test", code="test", state=state, task=task)
|
|
21
|
+
report.save()
|
|
22
|
+
reports[state] = report
|
|
23
|
+
|
|
24
|
+
if TASK_STATES.FAILED == to_state:
|
|
25
|
+
# Two ways to fail a task - set_failed and set_canceled("failed")
|
|
26
|
+
if use_canceled:
|
|
27
|
+
task.set_cancelling()
|
|
28
|
+
task.set_canceled(TASK_STATES.FAILED)
|
|
29
|
+
else:
|
|
30
|
+
try:
|
|
31
|
+
raise ValueError("test")
|
|
32
|
+
except ValueError:
|
|
33
|
+
exc_type, exc, tb = sys.exc_info()
|
|
34
|
+
task.set_failed(exc, tb)
|
|
35
|
+
elif TASK_STATES.CANCELED == to_state:
|
|
36
|
+
task.set_canceling()
|
|
37
|
+
task.set_canceled()
|
|
38
|
+
|
|
39
|
+
for state in vars(TASK_STATES):
|
|
40
|
+
report = ProgressReport.objects.get(pk=reports[state].pulp_id)
|
|
41
|
+
if TASK_STATES.RUNNING == state: # report *was* running, should be changed
|
|
42
|
+
assert to_state == report.state
|
|
43
|
+
else:
|
|
44
|
+
assert state == report.state
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import Mock
|
|
3
|
+
|
|
4
|
+
from django.contrib.auth import get_user_model
|
|
5
|
+
from django.contrib.auth.models import Group
|
|
6
|
+
from rest_framework.exceptions import ValidationError
|
|
7
|
+
|
|
8
|
+
from pulpcore.app.util import get_url, get_prn
|
|
9
|
+
from pulpcore.app.serializers import UserRoleSerializer, GroupRoleSerializer
|
|
10
|
+
from pulp_file.app.models import FileRepository
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
pytestmark = [pytest.mark.django_db]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(params=[UserRoleSerializer, GroupRoleSerializer])
|
|
17
|
+
def serializer_class(request):
|
|
18
|
+
return request.param
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def context(db, serializer_class):
|
|
23
|
+
request = Mock()
|
|
24
|
+
if serializer_class == UserRoleSerializer:
|
|
25
|
+
User = get_user_model()
|
|
26
|
+
user = User.objects.create()
|
|
27
|
+
request.resolver_match.kwargs = {"user_pk": user.pk}
|
|
28
|
+
elif serializer_class == GroupRoleSerializer:
|
|
29
|
+
group = Group.objects.create()
|
|
30
|
+
request.resolver_match.kwargs = {"group_pk": group.pk}
|
|
31
|
+
else:
|
|
32
|
+
pytest.fail("This fixture received an unknown serializer class.")
|
|
33
|
+
return {"request": request}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def repository(db):
|
|
38
|
+
return FileRepository.objects.create(name="test1")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize(
|
|
42
|
+
"field",
|
|
43
|
+
[
|
|
44
|
+
"role",
|
|
45
|
+
"content_object",
|
|
46
|
+
"content_object_prn",
|
|
47
|
+
],
|
|
48
|
+
)
|
|
49
|
+
def test_nested_role_serializer_has_certain_fields(serializer_class, field):
|
|
50
|
+
serializer = serializer_class()
|
|
51
|
+
assert field in serializer.fields
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_nested_role_serializer_fails_without_content(serializer_class, context):
|
|
55
|
+
data = {"role": "file.filerepository_owner"}
|
|
56
|
+
serializer = serializer_class(data=data, context=context)
|
|
57
|
+
with pytest.raises(ValidationError):
|
|
58
|
+
serializer.is_valid(raise_exception=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_nested_role_serializer_with_null_content(serializer_class, context):
|
|
62
|
+
data = {"role": "file.filerepository_owner", "content_object": None}
|
|
63
|
+
serializer = serializer_class(data=data, context=context)
|
|
64
|
+
assert serializer.is_valid(raise_exception=True)
|
|
65
|
+
assert serializer.validated_data["content_object"] is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_nested_role_serializer_with_null_content_prn(serializer_class, context):
|
|
69
|
+
data = {"role": "file.filerepository_owner", "content_object_prn": None}
|
|
70
|
+
serializer = serializer_class(data=data, context=context)
|
|
71
|
+
assert serializer.is_valid(raise_exception=True)
|
|
72
|
+
assert serializer.validated_data["content_object"] is None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_nested_role_serializer_allows_href(serializer_class, context, repository):
|
|
76
|
+
data = {"role": "file.filerepository_owner", "content_object": get_url(repository)}
|
|
77
|
+
serializer = serializer_class(data=data, context=context)
|
|
78
|
+
assert serializer.is_valid(raise_exception=True)
|
|
79
|
+
assert serializer.validated_data["content_object"] == repository
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_nested_role_serializer_allows_prn(serializer_class, context, repository):
|
|
83
|
+
data = {"role": "file.filerepository_owner", "content_object_prn": get_prn(repository)}
|
|
84
|
+
serializer = serializer_class(data=data, context=context)
|
|
85
|
+
assert serializer.is_valid(raise_exception=True)
|
|
86
|
+
assert serializer.validated_data["content_object"] == repository
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pulpcore
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.78.0
|
|
4
4
|
Summary: Pulp Django Application and Related Modules
|
|
5
5
|
Author-email: Pulp Team <pulp-list@redhat.com>
|
|
6
6
|
Project-URL: Homepage, https://pulpproject.org
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: aiodns<=3.
|
|
24
|
+
Requires-Dist: aiodns<=3.4.0,>=3.0
|
|
25
25
|
Requires-Dist: aiofiles<24.2.0,>=22.1
|
|
26
26
|
Requires-Dist: aiohttp<3.12,>=3.8.1
|
|
27
27
|
Requires-Dist: asyncio-throttle<=1.0.2,>=1.0
|
|
@@ -46,13 +46,13 @@ Requires-Dist: jinja2<=3.1.6,>=3.1
|
|
|
46
46
|
Requires-Dist: json_stream<2.4,>=2.3.2
|
|
47
47
|
Requires-Dist: jq<1.9.0,>=1.6.0
|
|
48
48
|
Requires-Dist: PyOpenSSL<26.0
|
|
49
|
-
Requires-Dist: opentelemetry-api<1.
|
|
50
|
-
Requires-Dist: opentelemetry-sdk<1.
|
|
51
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-http<1.
|
|
49
|
+
Requires-Dist: opentelemetry-api<1.34,>=1.27.0
|
|
50
|
+
Requires-Dist: opentelemetry-sdk<1.34,>=1.27.0
|
|
51
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<1.34,>=1.27.0
|
|
52
52
|
Requires-Dist: protobuf<6.0,>=4.21.1
|
|
53
53
|
Requires-Dist: pulp-glue<0.33,>=0.18.0
|
|
54
54
|
Requires-Dist: pygtrie<=2.5.0,>=2.5
|
|
55
|
-
Requires-Dist: psycopg[binary]<=3.2.
|
|
55
|
+
Requires-Dist: psycopg[binary]<=3.2.8,>=3.1.8
|
|
56
56
|
Requires-Dist: pyparsing<=3.2.3,>=3.1.0
|
|
57
57
|
Requires-Dist: python-gnupg<=0.5.4,>=0.5
|
|
58
58
|
Requires-Dist: PyYAML<=6.0.2,>=5.1.1
|
|
@@ -78,6 +78,8 @@ Requires-Dist: confluent-kafka<2.10.0,>=2.4.0; extra == "kafka"
|
|
|
78
78
|
Provides-Extra: diagnostics
|
|
79
79
|
Requires-Dist: pyinstrument~=5.0; extra == "diagnostics"
|
|
80
80
|
Requires-Dist: memray~=1.17; extra == "diagnostics"
|
|
81
|
+
Provides-Extra: uvloop
|
|
82
|
+
Requires-Dist: uvloop<0.22,>=0.20; extra == "uvloop"
|
|
81
83
|
Dynamic: license-file
|
|
82
84
|
|
|
83
85
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
pulp_certguard/__init__.py,sha256=llnEd00PrsAretsgAOHiNKFbmvIdXe3iDVPmSaKz7gU,71
|
|
2
2
|
pulp_certguard/pytest_plugin.py,sha256=qhRbChzqN2PROtD-65KuoTfKr5k9T3GPsz9daFgpqpM,852
|
|
3
|
-
pulp_certguard/app/__init__.py,sha256=
|
|
3
|
+
pulp_certguard/app/__init__.py,sha256=hqH_1iUT2LJzUr3xz4LG8Dy4FOC5DXp3rqx6S3JrQkw,297
|
|
4
4
|
pulp_certguard/app/models.py,sha256=xy5IWxf0LQxayIDmQw25Y2YhB_NrlTGvuvdY-YW7QBU,8119
|
|
5
5
|
pulp_certguard/app/serializers.py,sha256=3jxWu82vU3xA578Qbyz-G4Q9Zlh3MFLGRHzX62M0RF8,1826
|
|
6
6
|
pulp_certguard/app/utils.py,sha256=O6T1Npdb8fu3XqIkDJd8PQdEFJWPUeQ-i_aHXBl7MEc,816
|
|
@@ -49,7 +49,7 @@ pulp_certguard/tests/unit/test_models.py,sha256=TBI0yKsrdbnJSPeBFfxSqhXK7zaNvR6q
|
|
|
49
49
|
pulp_file/__init__.py,sha256=0vOCXofR6Eyxkg4y66esnOGPeESCe23C1cNBHj56w44,61
|
|
50
50
|
pulp_file/manifest.py,sha256=1WwIOJrPSkFcmkRm7CkWifVOCoZvo_nnANgce6uuG7U,3796
|
|
51
51
|
pulp_file/pytest_plugin.py,sha256=Fi_p-Vle_I-VYUSe4Zlg7esb_Ul5fpB8Rx9UGLK5UNQ,13281
|
|
52
|
-
pulp_file/app/__init__.py,sha256=
|
|
52
|
+
pulp_file/app/__init__.py,sha256=MWeAHZ0tKUnYLsD7UEnk75YAkXMd5luRNhpvU_q_KPE,292
|
|
53
53
|
pulp_file/app/modelresource.py,sha256=v-m-_bBEsfr8wG0TI5ffx1TuKUy2-PsirhuQz4XXF-0,1063
|
|
54
54
|
pulp_file/app/models.py,sha256=QsrVg_2uKqnR89sLN2Y7Zy260_nLIcUfa94uZowlmFw,4571
|
|
55
55
|
pulp_file/app/replica.py,sha256=OtNWVmdFUgNTYhPttftVNQnSrnvx2_hnrJgtW_G0Vrg,1894
|
|
@@ -110,7 +110,7 @@ pulpcore/pytest_plugin.py,sha256=Tq_xlO8Z2iyjFtbnaKHbWQogq6jxcRpjji9XbKrs5_U,377
|
|
|
110
110
|
pulpcore/responses.py,sha256=mIGKmdCfTSoZxbFu4yIH1xbdLx1u5gqt3D99LTamcJg,6125
|
|
111
111
|
pulpcore/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
112
112
|
pulpcore/app/access_policy.py,sha256=5vCKy6WoHtIt1_-eS5vMaZ7CmR4G-CIpsrB8yT-d88Q,6079
|
|
113
|
-
pulpcore/app/apps.py,sha256=
|
|
113
|
+
pulpcore/app/apps.py,sha256=RR971l1RNPGhG0_eSTV_2Pk-_EqFLNeF5bBoH2AOowM,17860
|
|
114
114
|
pulpcore/app/authentication.py,sha256=1LIJW6HIQQlZrliHy__jdzkDEh6Oj7xKgd0V-vRcDus,2855
|
|
115
115
|
pulpcore/app/checks.py,sha256=jbfTF7nmftBbky4AQXHigpyCaGydKasvRUXsd72JZVg,1946
|
|
116
116
|
pulpcore/app/entrypoint.py,sha256=HRfaHDkveSIfcTOtWEWYqg1poTmTo0J9hzzmj0yDcEM,4885
|
|
@@ -128,14 +128,15 @@ pulpcore/app/redis_connection.py,sha256=VTdG0ulXuyESjYV6SJdG_jLzkLZH-MlLcD6pielw
|
|
|
128
128
|
pulpcore/app/replica.py,sha256=b6r-QF4H4G94N5HoaV3PGHeOD4-BqVb7YVsRNHx0h9Y,11675
|
|
129
129
|
pulpcore/app/response.py,sha256=hYH_jSBrxmRsBr2bknmXE1qfs2g8JjDTXYcQ5ZWlF_c,1950
|
|
130
130
|
pulpcore/app/role_util.py,sha256=84HSt8_9fxB--dtfSyg_TumVgOdyBbyP6rBaiAfTpOU,22393
|
|
131
|
-
pulpcore/app/settings.py,sha256=
|
|
131
|
+
pulpcore/app/settings.py,sha256=f2LXOVLd58iG2z1Whm_XlLop129uKT5-GYxWiBq-ja8,22430
|
|
132
132
|
pulpcore/app/urls.py,sha256=0gdI74CAdycJStXSw1gknviDGe3J3k0UhS4J8RYa5dg,8120
|
|
133
|
-
pulpcore/app/util.py,sha256=
|
|
133
|
+
pulpcore/app/util.py,sha256=nYF6nZXgqVk4U1QeZEpWYX-wqitGSGAJip6W78IfXUk,24432
|
|
134
134
|
pulpcore/app/wsgi.py,sha256=7rpZ_1NHEN_UfeNZCj8206bas1WeqRkHnGdxpd7rdDI,492
|
|
135
135
|
pulpcore/app/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
136
136
|
pulpcore/app/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
137
137
|
pulpcore/app/management/commands/add-signing-service.py,sha256=jzUHopJoQ0fKkC23ybmd9WjX4R-UtvEHvvBxI_QkJF0,3232
|
|
138
138
|
pulpcore/app/management/commands/analyze-publication.py,sha256=imcHSBYHI20vaT6ZgGJkJ4lVzMgQDVlhAe9mGu07MIk,3124
|
|
139
|
+
pulpcore/app/management/commands/clean-up-progress-reports.py,sha256=3LLB1MJyyq_eHfuVn-XwzojufqphEaNTRIbXNbjgiCM,1971
|
|
139
140
|
pulpcore/app/management/commands/datarepair-2327.py,sha256=HCw3XQcEEFbgYPd7H1bBjboApDapCNplsAxd9ua8f6M,4677
|
|
140
141
|
pulpcore/app/management/commands/dump-permissions.py,sha256=hrwDbEMBpEGM6UmgZtCHR9vpEiVQP8KLPybVmqSMxmA,8662
|
|
141
142
|
pulpcore/app/management/commands/dump-publications-to-fs.py,sha256=0rIt7fJX_q-h_5sbys5T4SClp6Q326ABOWu9ZZfUPdA,7037
|
|
@@ -302,7 +303,7 @@ pulpcore/app/models/repository.py,sha256=xBMKsryirkpZyrQHnFbwolNbvyX1jHljcqC1ofv
|
|
|
302
303
|
pulpcore/app/models/role.py,sha256=dZklNd2VeAw4cT6dyJ7SyTBt9sZvdqakY86wXGAY3vU,3287
|
|
303
304
|
pulpcore/app/models/status.py,sha256=72oUOJ7BnCAw3uDbc-XuI72oAyP2llCoBic4zb2JP78,3683
|
|
304
305
|
pulpcore/app/models/storage.py,sha256=2b-DQWaO31NqjV6FiISALegND-sQZAU7BVAsduUvm3o,6780
|
|
305
|
-
pulpcore/app/models/task.py,sha256=
|
|
306
|
+
pulpcore/app/models/task.py,sha256=OhR7nxqExYhpdAoNkDDem0C6FvY8O_l0yDJ-0AaeZwU,15049
|
|
306
307
|
pulpcore/app/models/upload.py,sha256=3njXT2rrVJwBjEDegvqcLD9_7cPnnl974lhbAhikEp8,3004
|
|
307
308
|
pulpcore/app/protobuf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
308
309
|
pulpcore/app/protobuf/analytics_pb2.py,sha256=-4CkbSW8JUAEIjZJBTPAJ5QezFJOdCPiDhx8_KA1bMU,2168
|
|
@@ -327,7 +328,7 @@ pulpcore/app/serializers/repository.py,sha256=fSM92qJTjQIGXgnmA4xr62jRG9atYj6tkI
|
|
|
327
328
|
pulpcore/app/serializers/status.py,sha256=nIrQl-MlOzUIvV2DrkgC19gqGmRVNKvWVN4pIBELgcQ,3815
|
|
328
329
|
pulpcore/app/serializers/task.py,sha256=IGJGoSEC_wKS8t77JGnZWRqK-Mk5-4rXSj8j0Ha6nRA,10026
|
|
329
330
|
pulpcore/app/serializers/upload.py,sha256=4r6iBegbYHmgFYjBYPcqB8J7eSxXgY4ukayMxJZNh_M,2402
|
|
330
|
-
pulpcore/app/serializers/user.py,sha256=
|
|
331
|
+
pulpcore/app/serializers/user.py,sha256=QBEnUCfq2my3Lq_pohj7hphDE8wqU6g6fnYuEXl8VtI,18413
|
|
331
332
|
pulpcore/app/tasks/__init__.py,sha256=6fhLD0Z9LMluzqyBwQkatId71qI_2U7-o2-ZI1JH1Ls,576
|
|
332
333
|
pulpcore/app/tasks/analytics.py,sha256=eB3p-sdocH5yyNoe0OG5rUzwiVOfayOfHNzkohAfx-U,4722
|
|
333
334
|
pulpcore/app/tasks/base.py,sha256=4I88Bn5SttqEvvVlNJmIwkPv2IWe7OhpM-kbQiQ9T_U,5929
|
|
@@ -369,11 +370,11 @@ pulpcore/app/viewsets/task.py,sha256=pMoOQnhjA91dUgNNAnL3OaCHcVOrQcB-CD3D5Px96YE
|
|
|
369
370
|
pulpcore/app/viewsets/upload.py,sha256=Mfy9Vcm5KcqARooH4iExzoXVkL6boDddEqAnGWDWzFg,5452
|
|
370
371
|
pulpcore/app/viewsets/user.py,sha256=86eMawpaVrvp6ilQmb1C4j7SKpesPB5HgMovYL9rY3Q,13813
|
|
371
372
|
pulpcore/cache/__init__.py,sha256=GkYD4PgIMaVL83ywfAsLBC9JNNDUpmTtbitW9zZSslk,131
|
|
372
|
-
pulpcore/cache/cache.py,sha256=
|
|
373
|
-
pulpcore/content/__init__.py,sha256=
|
|
373
|
+
pulpcore/cache/cache.py,sha256=d8GMlvjeGG9MOMdi5_9029WpGCKH8Y5q9b2lt3wSREo,17371
|
|
374
|
+
pulpcore/content/__init__.py,sha256=CVrhM5Ep2NFZBWOPxuyXXz7xL0bdZsmpBaOLPeA14SI,4010
|
|
374
375
|
pulpcore/content/authentication.py,sha256=lEZBkXBBBkIdtFMCSpHDD7583M0bO-zsZNYXTmpr4k8,3235
|
|
375
|
-
pulpcore/content/entrypoint.py,sha256=
|
|
376
|
-
pulpcore/content/handler.py,sha256=
|
|
376
|
+
pulpcore/content/entrypoint.py,sha256=svs6pEYa5bEGhWAAHpZt-uqlTuVXQ2UdW4U_LRlRHhY,2048
|
|
377
|
+
pulpcore/content/handler.py,sha256=J5LDpT0uJn86HESsX37eIEIiakl84OKXdV934gpP-JI,56712
|
|
377
378
|
pulpcore/content/instrumentation.py,sha256=H0N0GWzvOPGGjFi6eIbGW3mcvagfnAfazccTh-BZVmE,1426
|
|
378
379
|
pulpcore/download/__init__.py,sha256=s3Wh2GKdsmbUooVIR6wSvhYVIhpaTbtfR3Ar1OJhC7s,154
|
|
379
380
|
pulpcore/download/base.py,sha256=G8jgyowvVEFCGA_KTr0CtHn0qHWXKsnv4Xpi0KSMglM,12821
|
|
@@ -460,8 +461,8 @@ pulpcore/tests/functional/api/test_workers.py,sha256=u3oQnErjf6qPCg08XMRZzecGetL
|
|
|
460
461
|
pulpcore/tests/functional/api/using_plugin/__init__.py,sha256=QyyfzgjLOi4n32G3o9aGH5eQDNjjD_qUpHLOZpPPZa4,80
|
|
461
462
|
pulpcore/tests/functional/api/using_plugin/test_checkpoint.py,sha256=gx1oiHOVUH5QZfF33k_DXSw-AVbYQp39uKii1D96BoI,7965
|
|
462
463
|
pulpcore/tests/functional/api/using_plugin/test_content_access.py,sha256=Ym800bU-M48RCDfQMkVa1UQt_sfgy5ciU0FxorCk9Ds,2551
|
|
463
|
-
pulpcore/tests/functional/api/using_plugin/test_content_cache.py,sha256=
|
|
464
|
-
pulpcore/tests/functional/api/using_plugin/test_content_delivery.py,sha256=
|
|
464
|
+
pulpcore/tests/functional/api/using_plugin/test_content_cache.py,sha256=OB3gDbPDptQBjyYnr_jHyU9bcI_-ANAoUp9EDiskwug,7312
|
|
465
|
+
pulpcore/tests/functional/api/using_plugin/test_content_delivery.py,sha256=BNinSe7eUGWzJ_PVlzlDykAV02xV_EPHxunQnVjndgo,10566
|
|
465
466
|
pulpcore/tests/functional/api/using_plugin/test_content_directory.py,sha256=w4uY258etnP8-LbrbZ_EZTolciYTt7cY1HJK9Ll7mS0,1931
|
|
466
467
|
pulpcore/tests/functional/api/using_plugin/test_content_path.py,sha256=fvqeptqo-mrUAiKIjlypuvHG1XsFeKKP81ocTmo4hv0,3334
|
|
467
468
|
pulpcore/tests/functional/api/using_plugin/test_content_promotion.py,sha256=Co4ytrfpzklwgDdEthv45dsmrceRpqIQfLJlZWM6EBY,2388
|
|
@@ -483,7 +484,7 @@ pulpcore/tests/functional/api/using_plugin/test_tasks.py,sha256=wIQr2J8DD2isx_0t
|
|
|
483
484
|
pulpcore/tests/functional/api/using_plugin/test_unlinking_repo.py,sha256=rGJP2qcDarJALSpjzEsoO-ewQ0J2kWhFvN3Y1Z9fvzA,1085
|
|
484
485
|
pulpcore/tests/functional/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
485
486
|
pulpcore/tests/performance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
486
|
-
pulpcore/tests/performance/test_performance.py,sha256=
|
|
487
|
+
pulpcore/tests/performance/test_performance.py,sha256=LIUSbDG6voX8IX4y7UXKDNcg4Ih2M0tVcvgB7vfClBc,8502
|
|
487
488
|
pulpcore/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
488
489
|
pulpcore/tests/unit/conftest.py,sha256=GfVjybp2b8c3E75rgq-kXVQ1_Tc3owuG3NK7Z0eGrsY,277
|
|
489
490
|
pulpcore/tests/unit/test_cache.py,sha256=sDNc7W31JLHS9r7ZiIUHz6iB7UkqPCKjLlfVmyTd8t4,3259
|
|
@@ -494,7 +495,7 @@ pulpcore/tests/unit/test_import_checks.py,sha256=Pu78yWFm68e1IfBbJIbzgwgKfDsjkpH
|
|
|
494
495
|
pulpcore/tests/unit/test_pulp_urls.py,sha256=sMRZowo7ej4HIrGEOY_OsDfXI1J4I8k_VXBvRDSCvYA,3416
|
|
495
496
|
pulpcore/tests/unit/test_settings.py,sha256=-yRXxmK-OdgC6mHqS83XgkD-PkZYvY1wZ37gRrGEoYc,2067
|
|
496
497
|
pulpcore/tests/unit/test_util.py,sha256=hgioXXC5-tufFpk6zrmMEHtWPG7UbmMHqeF5CiXOLqQ,884
|
|
497
|
-
pulpcore/tests/unit/test_viewsets.py,sha256=
|
|
498
|
+
pulpcore/tests/unit/test_viewsets.py,sha256=6rek28Rr0kEuYjQZ0_kTSnKsTvmMmD3l-WV_GVb48YQ,3208
|
|
498
499
|
pulpcore/tests/unit/content/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
499
500
|
pulpcore/tests/unit/content/test_handler.py,sha256=f4F9RAqCP60PUanYdeLW_A955UjRt8eCTrRuh0mChDU,19774
|
|
500
501
|
pulpcore/tests/unit/content/test_heartbeat.py,sha256=pHmSNy7KjlU9LpRcejBr8lttaJXrE4k9XQ5e2rDZmT0,1036
|
|
@@ -512,6 +513,7 @@ pulpcore/tests/unit/models/test_base.py,sha256=77hnxOFBJYMNbI1YGEaR5yj8VCapNGmEg
|
|
|
512
513
|
pulpcore/tests/unit/models/test_content.py,sha256=heU0vJKucPIp6py2Ww-eXLvhFopvmK8QjFgzt1jGnYQ,5599
|
|
513
514
|
pulpcore/tests/unit/models/test_remote.py,sha256=KxXwHdA-wj7D-ZpuVi33cLX43wkEeIzeqF9uMsJGt-k,2354
|
|
514
515
|
pulpcore/tests/unit/models/test_repository.py,sha256=rnBw1VOsi2Lv3zez2pV2RDXGk_z70KiaACOtyyXugJM,10379
|
|
516
|
+
pulpcore/tests/unit/models/test_task.py,sha256=rjxeYe383Zsjk8Ck4inMBBTzR4osCrgTeZNWwmHfbjk,1457
|
|
515
517
|
pulpcore/tests/unit/roles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
516
518
|
pulpcore/tests/unit/roles/test_roles.py,sha256=TkPPCLEHMaxfafsRf_3pc4Z3w8BPTyteY7rFkVo65GM,4973
|
|
517
519
|
pulpcore/tests/unit/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -521,14 +523,15 @@ pulpcore/tests/unit/serializers/test_fields.py,sha256=ba25xHl6aezQ3K5o2mPdw-pK9k
|
|
|
521
523
|
pulpcore/tests/unit/serializers/test_orphans_cleanup.py,sha256=z3yWWioSl3A1ZlQR0RGzkzPExp7AggyxHZBsE5cMjl4,617
|
|
522
524
|
pulpcore/tests/unit/serializers/test_pulpexport.py,sha256=gXn7E13X-SP0rFM0bUv8PwpdLI614txrPW-JKHZ4pQE,3010
|
|
523
525
|
pulpcore/tests/unit/serializers/test_repository.py,sha256=eknsHlbHz1K0nqntDntltFLU2EunrSlXCgg3HrV9PTI,9288
|
|
526
|
+
pulpcore/tests/unit/serializers/test_user.py,sha256=lemDxBIDWKrfFmazl9DMW7-k3lQyWtD8uQCxNktHI3Q,3094
|
|
524
527
|
pulpcore/tests/unit/stages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
525
528
|
pulpcore/tests/unit/stages/test_artifactdownloader.py,sha256=qB1ANdFmNtUnljg8fCdLHTiAakrO3KtX-w9RA5fPSOQ,12480
|
|
526
529
|
pulpcore/tests/unit/stages/test_stages.py,sha256=H1a2BQLjdZlZvcb_qULp62huZ1xy6ItTcthktVyGU0w,4735
|
|
527
530
|
pulpcore/tests/unit/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
528
531
|
pulpcore/tests/unit/viewsets/test_viewset_base.py,sha256=W9o3V6758bZctR6krMPPQytb0xJuF-jb4uBWTNDoD_U,4837
|
|
529
|
-
pulpcore-3.
|
|
530
|
-
pulpcore-3.
|
|
531
|
-
pulpcore-3.
|
|
532
|
-
pulpcore-3.
|
|
533
|
-
pulpcore-3.
|
|
534
|
-
pulpcore-3.
|
|
532
|
+
pulpcore-3.78.0.dist-info/licenses/LICENSE,sha256=dhnHU8rJXUdAIgIjveSKAyYG_KzN5eVG-bxETIGrNW0,17988
|
|
533
|
+
pulpcore-3.78.0.dist-info/METADATA,sha256=uo86c3lPxNoL4oLA7SpqwFpx-VAnB39P6ko50f46zas,4336
|
|
534
|
+
pulpcore-3.78.0.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
|
535
|
+
pulpcore-3.78.0.dist-info/entry_points.txt,sha256=OZven4wzXzQA5b5q9MpP4HUpIPPQCSvIOvkKtNInrK0,452
|
|
536
|
+
pulpcore-3.78.0.dist-info/top_level.txt,sha256=6h-Lm3FKQSaT_nL1KSxu_hBnzKE15bcvf_BoU-ea4CI,34
|
|
537
|
+
pulpcore-3.78.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|