pulpcore 3.85.1__py3-none-any.whl → 3.86.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.

@@ -6,6 +6,6 @@ class PulpCertGuardPluginAppConfig(PulpPluginAppConfig):
6
6
 
7
7
  name = "pulp_certguard.app"
8
8
  label = "certguard"
9
- version = "3.85.1"
9
+ version = "3.86.0"
10
10
  python_package_name = "pulpcore"
11
11
  domain_compatible = True
pulp_file/app/__init__.py CHANGED
@@ -8,6 +8,6 @@ class PulpFilePluginAppConfig(PulpPluginAppConfig):
8
8
 
9
9
  name = "pulp_file.app"
10
10
  label = "file"
11
- version = "3.85.1"
11
+ version = "3.86.0"
12
12
  python_package_name = "pulpcore"
13
13
  domain_compatible = True
pulpcore/app/apps.py CHANGED
@@ -239,7 +239,7 @@ class PulpAppConfig(PulpPluginAppConfig):
239
239
  label = "core"
240
240
 
241
241
  # The version of this app
242
- version = "3.85.1"
242
+ version = "3.86.0"
243
243
 
244
244
  # The python package name providing this app
245
245
  python_package_name = "pulpcore"
@@ -0,0 +1,84 @@
1
+ from collections import defaultdict
2
+ from gettext import gettext as _
3
+
4
+ from django.db import connection
5
+ from django.db.migrations.loader import MigrationLoader
6
+ from django.db.migrations.migration import SwappableTuple
7
+ from django.db.migrations.optimizer import MigrationOptimizer
8
+ from django.db.migrations.writer import MigrationWriter
9
+ from django.conf import settings
10
+ from django.core.management import BaseCommand
11
+
12
+
13
+ def print_stats(migration):
14
+ operations = migration.operations
15
+ migration_types = defaultdict(int)
16
+ for operation in operations:
17
+ migration_types[operation.__class__.__name__] += 1
18
+ for key, value in migration_types.items():
19
+ print(f"{value: 4} {key}")
20
+ print("---")
21
+ print(_("Total: {count}").format(count=len(operations)))
22
+
23
+
24
+ class Command(BaseCommand):
25
+ """
26
+ Django management command to optimize a migration.
27
+ """
28
+
29
+ help = _("Optimize a migration.")
30
+
31
+ def add_arguments(self, parser):
32
+ parser.add_argument("--dry-run", action="store_true", help=_("Don't change anything."))
33
+ parser.add_argument(
34
+ "--stat", action="store_true", help=_("Print statistics about operations.")
35
+ )
36
+ parser.add_argument("app-label", help=_("App label of the migrations to optimize."))
37
+ parser.add_argument("migration", help=_("Prefix of the migration to optimize."))
38
+
39
+ def handle(self, *args, **options):
40
+ dry_run = options.get("dry_run", False)
41
+ stat = options.get("stat", False)
42
+ app_label = options["app-label"]
43
+ migration_prefix = options["migration"]
44
+
45
+ loader = MigrationLoader(connection)
46
+
47
+ migration = loader.get_migration_by_prefix(app_label, migration_prefix)
48
+
49
+ print(_("Optimizing migration {}").format((migration.app_label, migration.name)))
50
+ if stat:
51
+ print(_("=== Old Migration Summary ==="))
52
+ print_stats(migration)
53
+
54
+ new_dependencies = []
55
+ for dependency in migration.dependencies:
56
+ if (
57
+ isinstance(dependency, SwappableTuple)
58
+ and settings.AUTH_USER_MODEL == dependency.setting
59
+ ):
60
+ new_dependencies.append(("__setting__", "AUTH_USER_MODEL"))
61
+ else:
62
+ new_dependencies.append(dependency)
63
+
64
+ optimizer = MigrationOptimizer()
65
+ new_operations = optimizer.optimize(migration.operations, app_label)
66
+
67
+ if new_operations != migration.operations:
68
+ print(
69
+ _("Changed from {old_count} to {new_count} operations.").format(
70
+ old_count=len(migration.operations), new_count=len(new_operations)
71
+ )
72
+ )
73
+ if stat:
74
+ print(_("=== New Migration Summary ==="))
75
+ print_stats(migration)
76
+
77
+ migration.operations = new_operations
78
+ migration.dependencies = new_dependencies
79
+ if not dry_run:
80
+ writer = MigrationWriter(migration)
81
+ with open(writer.path, "w") as output_file:
82
+ output_file.write(writer.as_string())
83
+ else:
84
+ print(_("No optimizations found."))
@@ -0,0 +1,19 @@
1
+ # Generated by Django 4.2.23 on 2025-08-07 11:43
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('core', '0138_vulnerabilityreport'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='task',
16
+ name='app_lock',
17
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='core.appstatus'),
18
+ ),
19
+ ]
@@ -15,6 +15,7 @@ from pulpcore.app.models import BaseModel
15
15
 
16
16
 
17
17
  class AppStatusManager(models.Manager):
18
+ # This should be replaced with 3.87.
18
19
  def online(self):
19
20
  """
20
21
  Returns a queryset of objects that are online.
@@ -51,8 +52,15 @@ class AppStatusManager(models.Manager):
51
52
 
52
53
  class _AppStatusManager(AppStatusManager):
53
54
  # This is an intermediate class in order to allow a ZDU.
54
- # It should be removed from the chain with 3.87.
55
+ # It should be made the real thing with 3.87.
56
+ def __init__(self):
57
+ super().__init__()
58
+ self._current_app_status = None
59
+
55
60
  def create(self, app_type, **kwargs):
61
+ if self._current_app_status is not None:
62
+ raise RuntimeError("There is already an app status in this process.")
63
+
56
64
  if app_type == "api":
57
65
  old_obj = ApiAppStatus.objects.create(**kwargs)
58
66
  elif app_type == "worker":
@@ -63,17 +71,27 @@ class _AppStatusManager(AppStatusManager):
63
71
  raise NotImplementedError(f"Invalid app_type: {app_type}")
64
72
  obj = super().create(app_type=app_type, **kwargs)
65
73
  obj._old_status = old_obj
74
+ self._current_app_status = obj
66
75
  return obj
67
76
 
68
77
  async def acreate(self, app_type, **kwargs):
78
+ if self._current_app_status is not None:
79
+ raise RuntimeError("There is already an app status in this process.")
80
+
69
81
  if app_type == "content":
70
82
  old_obj = await ContentAppStatus.objects.acreate(**kwargs)
71
83
  else:
72
84
  raise NotImplementedError(f"Invalid app_type: {app_type}")
73
85
  obj = await super().acreate(app_type=app_type, **kwargs)
74
86
  obj._old_status = old_obj
87
+ self._current_app_status = obj
75
88
  return obj
76
89
 
90
+ def current(self):
91
+ if self._current_app_status is None:
92
+ raise RuntimeError("There is no current app status.")
93
+ return self._current_app_status
94
+
77
95
  def online(self):
78
96
  """
79
97
  Returns a queryset of objects that are online.
@@ -116,7 +116,8 @@ class Task(BaseModel, AutoAddObjPermsMixin):
116
116
  Defaults to `True`.
117
117
 
118
118
  Relations:
119
-
119
+ app_lock (AppStatus): The app holding the lock on this task.
120
+ Warning: This is not yet implemented/enforced.
120
121
  parent (models.ForeignKey): Task that spawned this task (if any)
121
122
  worker (models.ForeignKey): The worker that this task is in
122
123
  pulp_domain (models.ForeignKey): The domain the Task is a part of
@@ -138,6 +139,10 @@ class Task(BaseModel, AutoAddObjPermsMixin):
138
139
  enc_kwargs = EncryptedJSONField(null=True, encoder=DjangoJSONEncoder)
139
140
 
140
141
  worker = models.ForeignKey("Worker", null=True, related_name="tasks", on_delete=models.SET_NULL)
142
+ # This field is supposed to replace the session advisory locks to protect tasks.
143
+ app_lock = models.ForeignKey(
144
+ "AppStatus", null=True, related_name="tasks", on_delete=models.SET_NULL
145
+ )
141
146
 
142
147
  parent_task = models.ForeignKey(
143
148
  "Task", null=True, related_name="child_tasks", on_delete=models.SET_NULL
pulpcore/app/settings.py CHANGED
@@ -405,6 +405,7 @@ KAFKA_SASL_PASSWORD = None
405
405
 
406
406
  # opentelemetry settings
407
407
  OTEL_ENABLED = False
408
+ OTEL_PULP_API_HISTOGRAM_BUCKETS = []
408
409
 
409
410
  # VulnerabilityReport settings
410
411
  VULN_REPORT_TASK_LIMITER = 10
@@ -504,6 +505,16 @@ authentication_json_header_openapi_security_scheme_validator = Validator(
504
505
  messages={"is_type_of": "{name} must be a dictionary."},
505
506
  )
506
507
 
508
+ otel_pulp_api_histogram_buckets_validator = Validator(
509
+ "OTEL_PULP_API_HISTOGRAM_BUCKETS",
510
+ is_type_of=list,
511
+ condition=lambda v: all([isinstance(value, float) for value in v]),
512
+ messages={
513
+ "is_type_of": "{name} must be a list.",
514
+ "condition": "All buckets must be declared as a float value",
515
+ },
516
+ )
517
+
507
518
 
508
519
  def otel_middleware_hook(settings):
509
520
  data = {"dynaconf_merge": True}
@@ -530,6 +541,7 @@ settings = DjangoDynaconf(
530
541
  unknown_algs_validator,
531
542
  json_header_auth_validator,
532
543
  authentication_json_header_openapi_security_scheme_validator,
544
+ otel_pulp_api_histogram_buckets_validator,
533
545
  ],
534
546
  post_hooks=(otel_middleware_hook,),
535
547
  )
pulpcore/middleware.py CHANGED
@@ -1,5 +1,8 @@
1
1
  import time
2
+ import re
3
+
2
4
  from contextvars import ContextVar
5
+ from os import environ
3
6
 
4
7
  from django.http.response import Http404
5
8
  from django.conf import settings
@@ -101,15 +104,32 @@ class APIRootRewriteMiddleware:
101
104
  class DjangoMetricsMiddleware:
102
105
  def __init__(self, get_response):
103
106
  self.meter = init_otel_meter("pulp-api")
104
- self.request_duration_histogram = self.meter.create_histogram(
105
- name="api.request_duration",
106
- description="Tracks the duration of HTTP requests",
107
- unit="ms",
108
- )
107
+ self._set_histogram(self.meter)
109
108
 
110
109
  self.get_response = get_response
111
110
 
111
+ def _excluded_urls(self, url):
112
+
113
+ excluded_urls = environ.get(
114
+ "OTEL_PYTHON_EXCLUDED_URLS", environ.get("OTEL_PYTHON_DJANGO_EXCLUDED_URLS", "")
115
+ )
116
+
117
+ if excluded_urls:
118
+ excluded_urls_list = [excluded_url.strip() for excluded_url in excluded_urls.split(",")]
119
+ else:
120
+ return False
121
+
122
+ exclusion_pattern = "|".join(excluded_urls_list)
123
+
124
+ if re.search(exclusion_pattern, url):
125
+ return True
126
+
127
+ return False
128
+
112
129
  def __call__(self, request):
130
+ if self._excluded_urls(request.build_absolute_uri("?")):
131
+ return self.get_response(request)
132
+
113
133
  start_time = time.time()
114
134
  response = self.get_response(request)
115
135
  end_time = time.time()
@@ -122,11 +142,18 @@ class DjangoMetricsMiddleware:
122
142
  return response
123
143
 
124
144
  def _set_histogram(self, meter):
125
- self.request_duration_histogram = meter.create_histogram(
126
- name="api.request_duration",
127
- description="Tracks the duration of HTTP requests",
128
- unit="ms",
129
- )
145
+ create_histogram_kwargs = {
146
+ "name": "api.request_duration",
147
+ "description": "Tracks the duration of HTTP requests",
148
+ "unit": "ms",
149
+ }
150
+
151
+ if settings.OTEL_PULP_API_HISTOGRAM_BUCKETS:
152
+ create_histogram_kwargs["explicit_bucket_boundaries_advisory"] = (
153
+ settings.OTEL_PULP_API_HISTOGRAM_BUCKETS
154
+ )
155
+
156
+ self.request_duration_histogram = meter.create_histogram(**create_histogram_kwargs)
130
157
 
131
158
  def _process_attributes(self, request, response):
132
159
  return {
@@ -116,9 +116,15 @@ def validate_version_paths(version):
116
116
  Raises:
117
117
  ValueError: If two artifact relative paths overlap
118
118
  """
119
- paths = ContentArtifact.objects.filter(content__pk__in=version.content).values_list(
120
- "relative_path", flat=True
119
+ # Get unique (path, artifact) pairs to allow artifacts shared across content
120
+ content_artifacts = (
121
+ ContentArtifact.objects.filter(content__pk__in=version.content)
122
+ .values_list("relative_path", "artifact")
123
+ .distinct()
121
124
  )
125
+
126
+ paths = [path for path, artifact_id in content_artifacts]
127
+
122
128
  try:
123
129
  validate_file_paths(paths)
124
130
  except ValueError as e:
pulpcore/pytest_plugin.py CHANGED
@@ -1013,23 +1013,25 @@ def dispatch_task(pulpcore_bindings):
1013
1013
  commands = (
1014
1014
  "from django_guid import set_guid; "
1015
1015
  "from pulpcore.tasking.tasks import dispatch; "
1016
- "from pulpcore.app.models import TaskGroup; "
1016
+ "from pulpcore.app.models import TaskGroup, AppStatus; "
1017
1017
  "from pulpcore.app.util import get_url, set_current_user; "
1018
1018
  "from django.contrib.auth import get_user_model; "
1019
+ f"app_status=AppStatus.objects.create(name='test-' + {cid!r},app_type='worker'); "
1019
1020
  "User = get_user_model(); "
1020
1021
  f"user = User.objects.filter(username='{username}').first(); "
1021
1022
  "set_current_user(user); "
1022
1023
  f"set_guid({cid!r}); "
1023
1024
  f"tg = {task_group_id!r} and TaskGroup.objects.filter(pk={task_group_id!r}).first(); "
1024
1025
  f"task = dispatch(*{args!r}, task_group=tg, **{kwargs!r}); "
1026
+ "app_status.delete(); "
1025
1027
  "print(get_url(task))"
1026
1028
  )
1027
1029
 
1028
1030
  process = subprocess.run(["pulpcore-manager", "shell", "-c", commands], capture_output=True)
1029
-
1030
- assert process.returncode == 0
1031
+ err_log = process.stderr.decode()
1032
+ assert process.returncode == 0, err_log
1031
1033
  task_href = process.stdout.decode().strip()
1032
- print(process.stderr.decode(), file=sys.stderr)
1034
+ print(err_log, file=sys.stderr)
1033
1035
  return task_href
1034
1036
 
1035
1037
  return _dispatch_task
pulpcore/tasking/tasks.py CHANGED
@@ -8,7 +8,6 @@ import sys
8
8
  import traceback
9
9
  import tempfile
10
10
  import threading
11
- from asgiref.sync import sync_to_async
12
11
  from gettext import gettext as _
13
12
 
14
13
  from django.conf import settings
@@ -16,8 +15,12 @@ from django.db import connection, transaction
16
15
  from django.db.models import Model
17
16
  from django_guid import get_guid
18
17
  from pulpcore.app.apps import MODULE_PLUGIN_VERSIONS
19
- from pulpcore.app.models import Task, TaskGroup
20
- from pulpcore.app.util import current_task, get_domain, get_prn, deprecation_logger
18
+ from pulpcore.app.models import Task, TaskGroup, AppStatus
19
+ from pulpcore.app.util import (
20
+ current_task,
21
+ get_domain,
22
+ get_prn,
23
+ )
21
24
  from pulpcore.constants import (
22
25
  TASK_FINAL_STATES,
23
26
  TASK_INCOMPLETE_STATES,
@@ -81,19 +84,11 @@ def _execute_task(task):
81
84
  immediate = task.immediate
82
85
  is_coroutine_fn = asyncio.iscoroutinefunction(func)
83
86
 
84
- if not is_coroutine_fn:
85
- if immediate:
86
- deprecation_logger.warning(
87
- "Immediate tasks must be coroutine functions. "
88
- "Support for non-coroutine immediate tasks will be dropped "
89
- "in pulpcore 3.85."
90
- )
91
- func = sync_to_async(func)
92
- is_coroutine_fn = True
93
- else:
94
- func(*args, **kwargs)
87
+ if immediate and not is_coroutine_fn:
88
+ raise ValueError("Immediate tasks must be async functions.")
95
89
 
96
90
  if is_coroutine_fn:
91
+ # both regular and immediate tasks can be coroutines, but only immediate must timeout
97
92
  _logger.debug("Task is coroutine %s", task.pk)
98
93
  coro = func(*args, **kwargs)
99
94
  if immediate:
@@ -110,6 +105,8 @@ def _execute_task(task):
110
105
  timeout=IMMEDIATE_TIMEOUT,
111
106
  )
112
107
  )
108
+ else:
109
+ func(*args, **kwargs)
113
110
 
114
111
  except Exception:
115
112
  exc_type, exc, tb = sys.exc_info()
@@ -240,6 +237,7 @@ def dispatch(
240
237
  immediate=immediate,
241
238
  deferred=deferred,
242
239
  profile_options=x_task_diagnostics_var.get(None),
240
+ app_lock=(immediate and AppStatus.objects.current()) or None,
243
241
  )
244
242
  task.refresh_from_db() # The database may have assigned a timestamp for us.
245
243
  if immediate:
@@ -272,13 +270,16 @@ def dispatch(
272
270
  try:
273
271
  execute_task(task)
274
272
  finally:
275
- # whether the task fails or not, we should always restore the workdir
273
+ # Whether the task fails or not, we should always restore the workdir.
276
274
  os.chdir(cur_dir)
277
275
 
278
276
  if resources:
279
277
  notify_workers = True
280
278
  elif deferred:
279
+ # Resources are blocked. Let the others handle it.
281
280
  notify_workers = True
281
+ task.app_lock = None
282
+ task.save()
282
283
  else:
283
284
  task.set_canceling()
284
285
  task.set_canceled(TASK_STATES.CANCELED, "Resources temporarily unavailable.")
@@ -363,41 +363,60 @@ class PulpcoreWorker:
363
363
  def iter_tasks(self):
364
364
  """Iterate over ready tasks and yield each task while holding the lock."""
365
365
  while not self.shutdown_requested:
366
- # When batching this query, be sure to use "pulp_created" as a cursor
366
+ # When batching this query, be sure to use "pulp_created" as a cursor.
367
367
  for task in Task.objects.filter(
368
368
  state__in=TASK_INCOMPLETE_STATES,
369
369
  unblocked_at__isnull=False,
370
370
  ).order_by("-immediate", F("pulp_created") + Value(timedelta(seconds=8)) * Random()):
371
- # This code will only be called if we acquired the lock successfully
372
- # The lock will be automatically be released at the end of the block
371
+ # This code will only be called if we acquired the lock successfully.
372
+ # The lock will be automatically be released at the end of the block.
373
373
  with contextlib.suppress(AdvisoryLockError), task:
374
- # Check if someone else changed the task before we got the lock
375
- task.refresh_from_db()
376
-
377
- if task.state == TASK_STATES.CANCELING and task.worker is None:
378
- # No worker picked this task up before being canceled
379
- if self.cancel_abandoned_task(task, TASK_STATES.CANCELED):
380
- # Continue looking for the next task without considering this
381
- # tasks resources, as we just released them
382
- continue
383
- if task.state in [TASK_STATES.RUNNING, TASK_STATES.CANCELING]:
384
- # A running task without a lock must be abandoned
385
- if self.cancel_abandoned_task(
386
- task, TASK_STATES.FAILED, "Worker has gone missing."
374
+ # We got the advisory lock (OLD) now try to get the app_lock (NEW).
375
+ rows = Task.objects.filter(pk=task.pk, app_lock=None).update(
376
+ app_lock=AppStatus.objects.current()
377
+ )
378
+ if rows == 0:
379
+ _logger.error(
380
+ "Acquired advisory lock but missed the app_lock for the task. "
381
+ "This should only happen during the upgrade phase to the new app_lock."
382
+ )
383
+ continue
384
+ try:
385
+ # Check if someone else changed the task before we got the lock.
386
+ task.refresh_from_db()
387
+
388
+ if task.state == TASK_STATES.CANCELING and task.worker is None:
389
+ # No worker picked this task up before being canceled.
390
+ if self.cancel_abandoned_task(task, TASK_STATES.CANCELED):
391
+ # Continue looking for the next task without considering this
392
+ # tasks resources, as we just released them.
393
+ continue
394
+ if task.state in [TASK_STATES.RUNNING, TASK_STATES.CANCELING]:
395
+ # A running task without a lock must be abandoned.
396
+ if self.cancel_abandoned_task(
397
+ task, TASK_STATES.FAILED, "Worker has gone missing."
398
+ ):
399
+ # Continue looking for the next task without considering this
400
+ # tasks resources, as we just released them.
401
+ continue
402
+
403
+ # This statement is using lazy evaluation.
404
+ if (
405
+ task.state == TASK_STATES.WAITING
406
+ and task.unblocked_at is not None
407
+ and self.is_compatible(task)
387
408
  ):
388
- # Continue looking for the next task without considering this
389
- # tasks resources, as we just released them
390
- continue
391
-
392
- # This statement is using lazy evaluation
393
- if (
394
- task.state == TASK_STATES.WAITING
395
- and task.unblocked_at is not None
396
- and self.is_compatible(task)
397
- ):
398
- yield task
399
- # Start from the top of the Task list
400
- break
409
+ yield task
410
+ # Start from the top of the Task list.
411
+ break
412
+ finally:
413
+ rows = Task.objects.filter(
414
+ pk=task.pk, app_lock=AppStatus.objects.current()
415
+ ).update(app_lock=None)
416
+ if rows != 1:
417
+ raise RuntimeError(
418
+ "Something other than us is messing around with locks."
419
+ )
401
420
  else:
402
421
  # No task found in the for-loop
403
422
  break
@@ -490,22 +490,21 @@ class TestImmediateTaskWithNoResource:
490
490
  assert task.worker is None
491
491
 
492
492
  @pytest.mark.parallel
493
- def test_executes_on_api_worker_when_no_async(self, pulpcore_bindings, dispatch_task, capsys):
493
+ def test_executes_on_api_worker_when_no_async(
494
+ self, pulpcore_bindings, dispatch_task, monitor_task
495
+ ):
494
496
  """
495
497
  GIVEN a task with no resource requirements
496
498
  AND the task IS NOT an async function
497
499
  WHEN dispatching a task as immediate
498
- THEN the task completes with no associated worker
500
+ THEN the dispatch should throw an error
499
501
  """
500
- # TODO: on 3.85 this should throw an error
501
- task_href = dispatch_task(
502
- "pulpcore.app.tasks.test.sleep", args=(LT_TIMEOUT,), immediate=True
503
- )
504
- stderr_content = capsys.readouterr().err
505
- task = pulpcore_bindings.TasksApi.read(task_href)
506
- assert task.state == "completed"
507
- assert task.worker is None
508
- assert "Support for non-coroutine immediate tasks will be dropped" in stderr_content
502
+ with pytest.raises(PulpTaskError) as ctx:
503
+ task_href = dispatch_task(
504
+ "pulpcore.app.tasks.test.sleep", args=(LT_TIMEOUT,), immediate=True
505
+ )
506
+ monitor_task(task_href)
507
+ assert "Immediate tasks must be async functions" in ctx.value.task.error["description"]
509
508
 
510
509
  @pytest.mark.parallel
511
510
  def test_timeouts_on_api_worker(self, pulpcore_bindings, dispatch_task):
@@ -4,7 +4,6 @@ import pytest
4
4
  import subprocess
5
5
  import uuid
6
6
  from datetime import datetime, timedelta
7
- from random import choice
8
7
  from time import sleep
9
8
 
10
9
 
@@ -23,7 +22,7 @@ def test_worker_actions(pulpcore_bindings):
23
22
  assert val is not None
24
23
 
25
24
  # Pick a random worker to be used for the next assertions.
26
- chosen_worker = choice(workers)
25
+ chosen_worker = next(worker for worker in workers if not worker.name.startswith("test-"))
27
26
 
28
27
  # Read a worker by its pulp_href.
29
28
  read_worker = pulpcore_bindings.WorkersApi.read(chosen_worker.pulp_href)
@@ -5,7 +5,7 @@ from django.db.utils import InterfaceError, OperationalError
5
5
 
6
6
  from pulpcore.content import _heartbeat
7
7
  from pulpcore.content.handler import Handler
8
- from pulpcore.app.models.status import AppStatusManager
8
+ from pulpcore.app.models.status import AppStatus, AppStatusManager
9
9
 
10
10
 
11
11
  class MockException(Exception):
@@ -25,6 +25,7 @@ async def test_db_connection_interface_error(monkeypatch, settings, error_class)
25
25
  mock_acreate = AsyncMock()
26
26
  mock_acreate.return_value = mock_app_status
27
27
  monkeypatch.setattr(AppStatusManager, "acreate", mock_acreate)
28
+ monkeypatch.setattr(AppStatus, "objects", AppStatusManager())
28
29
  mock_reset_db = Mock()
29
30
  monkeypatch.setattr(Handler, "_reset_db_connection", mock_reset_db)
30
31
  settings.CONTENT_APP_TTL = 1
@@ -3,7 +3,8 @@ from uuid import uuid4
3
3
 
4
4
  from itertools import compress
5
5
 
6
- from pulpcore.plugin.models import Content, Repository
6
+ from pulpcore.plugin.models import Artifact, Content, ContentArtifact, Repository
7
+ from pulpcore.plugin.repo_version_utils import validate_version_paths
7
8
 
8
9
 
9
10
  def pks_of_next_qs(qs_generator):
@@ -258,3 +259,85 @@ def test_next_version_with_multiple_versions():
258
259
 
259
260
  assert repository.next_version == 4
260
261
  assert repository.latest_version().number == 1
262
+
263
+
264
+ @pytest.mark.django_db
265
+ def test_shared_artifact_same_path_validation(tmp_path):
266
+ """
267
+ Test that multiple content units can reference the same artifact with the same
268
+ relative path without causing validation errors.
269
+
270
+ This reproduces scenarios where different content units legitimately share
271
+ the same artifact (e.g. upstream source files).
272
+ """
273
+ # Create a repository
274
+ repository = Repository.objects.create(name=uuid4())
275
+ repository.CONTENT_TYPES = [Content]
276
+
277
+ # Create a shared artifact using proper test pattern
278
+ artifact_path = tmp_path / "shared_file.txt"
279
+ artifact_path.write_text("Shared content data")
280
+ shared_artifact = Artifact.init_and_validate(str(artifact_path))
281
+ shared_artifact.save()
282
+
283
+ # Create two content units (simulates any content that shares artifacts)
284
+ content1 = Content.objects.create(pulp_type="core.content")
285
+ content2 = Content.objects.create(pulp_type="core.content")
286
+
287
+ # Both content units reference the same artifact with same path
288
+ ContentArtifact.objects.create(
289
+ content=content1, artifact=shared_artifact, relative_path="shared/common_file.txt"
290
+ )
291
+ ContentArtifact.objects.create(
292
+ content=content2, artifact=shared_artifact, relative_path="shared/common_file.txt"
293
+ )
294
+
295
+ # Create a repository version with both content units
296
+ with repository.new_version() as new_version:
297
+ new_version.add_content(Content.objects.filter(pk__in=[content1.pk, content2.pk]))
298
+
299
+ # This should not raise validation errors with our fix
300
+ validate_version_paths(new_version)
301
+
302
+
303
+ @pytest.mark.django_db
304
+ def test_different_artifacts_same_path_validation_fails(tmp_path):
305
+ """
306
+ Test that different artifacts trying to use the same relative path
307
+ still fail validation (this is a real conflict that should be caught).
308
+ """
309
+ # Create a repository
310
+ repository = Repository.objects.create(name=uuid4())
311
+ repository.CONTENT_TYPES = [Content]
312
+
313
+ # Create two different artifacts using proper test pattern
314
+ artifact1_path = tmp_path / "artifact1.txt"
315
+ artifact1_path.write_text("Content of first artifact")
316
+ artifact1 = Artifact.init_and_validate(str(artifact1_path))
317
+ artifact1.save()
318
+
319
+ artifact2_path = tmp_path / "artifact2.txt"
320
+ artifact2_path.write_text("Content of second artifact") # Different content
321
+ artifact2 = Artifact.init_and_validate(str(artifact2_path))
322
+ artifact2.save()
323
+
324
+ # Create two content units with different artifacts but same path
325
+ content1 = Content.objects.create(pulp_type="core.content")
326
+ content2 = Content.objects.create(pulp_type="core.content")
327
+
328
+ ContentArtifact.objects.create(
329
+ content=content1, artifact=artifact1, relative_path="conflicting/file.txt"
330
+ )
331
+ ContentArtifact.objects.create(
332
+ content=content2,
333
+ artifact=artifact2,
334
+ relative_path="conflicting/file.txt", # Same path, different artifact
335
+ )
336
+
337
+ # Create a repository version with both content units
338
+ with repository.new_version() as new_version:
339
+ new_version.add_content(Content.objects.filter(pk__in=[content1.pk, content2.pk]))
340
+
341
+ # This should raise a validation error due to path conflict
342
+ with pytest.raises(ValueError, match="Repository version errors"):
343
+ validate_version_paths(new_version)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulpcore
3
- Version: 3.85.1
3
+ Version: 3.86.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
@@ -45,7 +45,7 @@ Requires-Dist: PyOpenSSL<26.0
45
45
  Requires-Dist: opentelemetry-api<1.37,>=1.27.0
46
46
  Requires-Dist: opentelemetry-sdk<1.37,>=1.27.0
47
47
  Requires-Dist: opentelemetry-exporter-otlp-proto-http<1.37,>=1.27.0
48
- Requires-Dist: protobuf<6.0,>=4.21.1
48
+ Requires-Dist: protobuf<7.0,>=4.21.1
49
49
  Requires-Dist: pulp-glue<0.36,>=0.28.0
50
50
  Requires-Dist: pygtrie<=2.5.0,>=2.5
51
51
  Requires-Dist: psycopg[binary]<3.3,>=3.1.8
@@ -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=vRWSjas1xU7Jcp9S5EQu_eUMv5QScQwkZCvjHf3P_B8,297
3
+ pulp_certguard/app/__init__.py,sha256=uZZw9MWtJ_10aWLtmfluDTNp95Ys1lJil_PXTdA1_W4,297
4
4
  pulp_certguard/app/models.py,sha256=YLEhBtZM4hetekVZ_GTnbLlWD6CkIQw2B3ILwXRcq-s,7483
5
5
  pulp_certguard/app/serializers.py,sha256=9IxlQiy783RdKF9oI1mrYS4haG5Boy2DOjfP_eJtMLY,1726
6
6
  pulp_certguard/app/viewsets.py,sha256=1_gNmsWyOT8kcOiGVkn4-wrtAjZO4wC8q0-aoEsCpjI,697
@@ -51,7 +51,7 @@ pulp_certguard/tests/unit/test_rhsm_check_path.py,sha256=Q1CsXnUgD7ELvtolPeumyNr
51
51
  pulp_file/__init__.py,sha256=0vOCXofR6Eyxkg4y66esnOGPeESCe23C1cNBHj56w44,61
52
52
  pulp_file/manifest.py,sha256=1WwIOJrPSkFcmkRm7CkWifVOCoZvo_nnANgce6uuG7U,3796
53
53
  pulp_file/pytest_plugin.py,sha256=l1PvTxUi5D3uJy4SnHWNhr-otWEYNcm-kc5nSqVJg0Y,10646
54
- pulp_file/app/__init__.py,sha256=Vpe9lHLHQMiESLRNLTy5MhUgXUN11U5PkkHeDAg3xPQ,292
54
+ pulp_file/app/__init__.py,sha256=jD3E48tyhIAmkz0RW7ThsAoxuXVR5KpUsS68u6x5W4E,292
55
55
  pulp_file/app/modelresource.py,sha256=v-m-_bBEsfr8wG0TI5ffx1TuKUy2-PsirhuQz4XXF-0,1063
56
56
  pulp_file/app/models.py,sha256=QsrVg_2uKqnR89sLN2Y7Zy260_nLIcUfa94uZowlmFw,4571
57
57
  pulp_file/app/replica.py,sha256=OtNWVmdFUgNTYhPttftVNQnSrnvx2_hnrJgtW_G0Vrg,1894
@@ -90,13 +90,13 @@ pulpcore/backends.py,sha256=Ax_MJpbvtNDg_rhkHaiQRm39DBSS2dH8UpMRJN2T0oE,4482
90
90
  pulpcore/constants.py,sha256=ym3LV5TqFtUE_klh6yEogUb4fg4NZACF213VztnZ6jw,4882
91
91
  pulpcore/filters.py,sha256=dD5oRRkWg65s3LoObr-ipRvRsxZK_3Zr0lKMNr9Sg5o,16682
92
92
  pulpcore/metrics.py,sha256=Mfq-nnRjRf3vBHFO-ux-4d1I3yE7TgeptwgiSgGz4rA,2230
93
- pulpcore/middleware.py,sha256=10Jxc4Iq03gZD3n39t63OmBCpdftcke8bxEd-LioJlA,5973
93
+ pulpcore/middleware.py,sha256=YTmIW41bpIu3rw_zVRFz-1A4nko4zMte7EiYXBJkwXg,6721
94
94
  pulpcore/migrations.py,sha256=YhvuCIqyTr9AwBGwOpawCEuuZpHHo50xt6VHqUGraeE,3383
95
- pulpcore/pytest_plugin.py,sha256=cWqLZ_JIQGFRvfKJQSP0o-rX3e_J6Kes8KKFD8rIpOw,38039
95
+ pulpcore/pytest_plugin.py,sha256=fy9vz5-bw30T7f4jxDtNIgF7L_0MJ_q7KIAzpvizvnY,38215
96
96
  pulpcore/responses.py,sha256=mIGKmdCfTSoZxbFu4yIH1xbdLx1u5gqt3D99LTamcJg,6125
97
97
  pulpcore/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  pulpcore/app/access_policy.py,sha256=5vCKy6WoHtIt1_-eS5vMaZ7CmR4G-CIpsrB8yT-d88Q,6079
99
- pulpcore/app/apps.py,sha256=n9HJolGvkvQLINcZpkJ3BqPwulF4tl2cqMO_GsmrDP8,17412
99
+ pulpcore/app/apps.py,sha256=1X52djxH5kP1k2Tktti7NJttzmNqI2rSKuJ_JKti7Zg,17412
100
100
  pulpcore/app/authentication.py,sha256=1LIJW6HIQQlZrliHy__jdzkDEh6Oj7xKgd0V-vRcDus,2855
101
101
  pulpcore/app/checks.py,sha256=jbfTF7nmftBbky4AQXHigpyCaGydKasvRUXsd72JZVg,1946
102
102
  pulpcore/app/entrypoint.py,sha256=GYEq4GjglQZhFlU3865AT_H0nPypDKJAsf8qdyR4tPY,4985
@@ -115,7 +115,7 @@ pulpcore/app/redis_connection.py,sha256=VTdG0ulXuyESjYV6SJdG_jLzkLZH-MlLcD6pielw
115
115
  pulpcore/app/replica.py,sha256=rGE14OBaR_FKxmHL7NMxf_OizMyS-90IPsMRo_j9YRI,11474
116
116
  pulpcore/app/response.py,sha256=hYH_jSBrxmRsBr2bknmXE1qfs2g8JjDTXYcQ5ZWlF_c,1950
117
117
  pulpcore/app/role_util.py,sha256=84HSt8_9fxB--dtfSyg_TumVgOdyBbyP6rBaiAfTpOU,22393
118
- pulpcore/app/settings.py,sha256=LXwuQguRSqiLS8kwVLaRW3Qs96Jc8R8W13wy9QQlwto,21475
118
+ pulpcore/app/settings.py,sha256=k1MEoM25PXe1K8p9swB_lsNvg5Rd8X2mctuLTL7-nBw,21893
119
119
  pulpcore/app/urls.py,sha256=0gdI74CAdycJStXSw1gknviDGe3J3k0UhS4J8RYa5dg,8120
120
120
  pulpcore/app/util.py,sha256=nYF6nZXgqVk4U1QeZEpWYX-wqitGSGAJip6W78IfXUk,24432
121
121
  pulpcore/app/wsgi.py,sha256=7rpZ_1NHEN_UfeNZCj8206bas1WeqRkHnGdxpd7rdDI,492
@@ -130,6 +130,7 @@ pulpcore/app/management/commands/dump-publications-to-fs.py,sha256=lkLwxPi4GXzcL
130
130
  pulpcore/app/management/commands/handle-artifact-checksums.py,sha256=gblm6CkuyXrf9TsiTtts6iIgk4nyZnsJShozGxyALV8,8728
131
131
  pulpcore/app/management/commands/migrationstat.py,sha256=Gy19UMSyUeXYT13ERQ-P1PdgnmNX9veJteEOgMMG6QY,1517
132
132
  pulpcore/app/management/commands/openapi.py,sha256=p-aPuVfbnFQYIU7BMnipxe9nId-f2agNiviSIy43y9Q,3634
133
+ pulpcore/app/management/commands/optimizemigration.py,sha256=jt_V2PxvA7TSLz730hm6Yq8QwlcRIE5cllmve9KJo9M,3155
133
134
  pulpcore/app/management/commands/rebasemigrations.py,sha256=xpn8Gg-L3SfBGG0dD782sBmhLMgVHrveBg7xrGII3f8,3356
134
135
  pulpcore/app/management/commands/remove-plugin.py,sha256=bgcCm4y_Cl-C_8DL8wqTveOV4Jvw3cMwashauOhDD2E,7949
135
136
  pulpcore/app/management/commands/remove-signing-service.py,sha256=OF0IeGoldEhFAK3QvDaxMpsmoCu-q6Kcs8SbZJutLgU,2199
@@ -186,6 +187,7 @@ pulpcore/app/migrations/0135_task_pulp_task_resources_index.py,sha256=X2cJRbuuKx
186
187
  pulpcore/app/migrations/0136_delete_basedistribution.py,sha256=4oKidAU27jLLWRkboPstNF75kq3mTZ-QUxrKbMIczKo,315
187
188
  pulpcore/app/migrations/0137_appstatus.py,sha256=WMjQ8Tb-jsaCtyYBVZAmFemGyIzNDABVPfTKsEYvxjI,1332
188
189
  pulpcore/app/migrations/0138_vulnerabilityreport.py,sha256=rYmdIXfTCSZeH5sHLCCFMc2wrKhSuVE334A4kmFkJ6w,1372
190
+ pulpcore/app/migrations/0139_task_app_lock.py,sha256=Dtu_om_zFplrPr8DageoiXOWUiOS5aHgOy99S0bMXH0,502
189
191
  pulpcore/app/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
190
192
  pulpcore/app/models/__init__.py,sha256=P_2UnLmtQYbASWrm8elO2Zm_od-LXVqQKnjCwYFlZW0,3552
191
193
  pulpcore/app/models/access_policy.py,sha256=o4L41RGoZ5UMmh5UeeenmadD5MJgShguphgd4eAVxQA,6071
@@ -204,9 +206,9 @@ pulpcore/app/models/publication.py,sha256=75uUnm_sU5V_QraD6kPvEXVKxYyA1ikIsoFD51
204
206
  pulpcore/app/models/replica.py,sha256=i_wPxyPaVWpEVTJNVjJsBarxFauqeagtuwLadsmVz-g,2067
205
207
  pulpcore/app/models/repository.py,sha256=SIc21Gex6okxI7OCfHEGIpXpGlypG3z9IgMt5-mkNy0,58056
206
208
  pulpcore/app/models/role.py,sha256=dZklNd2VeAw4cT6dyJ7SyTBt9sZvdqakY86wXGAY3vU,3287
207
- pulpcore/app/models/status.py,sha256=4HbuuTCbug7CY8e_8fCaYiDp2T1UidozCKucm5rFVHk,8621
209
+ pulpcore/app/models/status.py,sha256=2Fac5z8quv0cG-8CTxIzdT_uzPea6c0i5sEaHp4V82A,9268
208
210
  pulpcore/app/models/storage.py,sha256=2b-DQWaO31NqjV6FiISALegND-sQZAU7BVAsduUvm3o,6780
209
- pulpcore/app/models/task.py,sha256=5T7PwJNNZOJI5opIGdLoUghCAlcAHfSarTxVed_oH0w,15321
211
+ pulpcore/app/models/task.py,sha256=fHubp1YO6CjSxfXVtgdZsQyn7NJ7apBHKz6-YEqnuDM,15653
210
212
  pulpcore/app/models/upload.py,sha256=3njXT2rrVJwBjEDegvqcLD9_7cPnnl974lhbAhikEp8,3004
211
213
  pulpcore/app/models/vulnerability_report.py,sha256=DDAUjDaW3Kn9KPBkBl94u4EuQy8UIu5wKbmE5kMkhWE,1238
212
214
  pulpcore/app/protobuf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -308,7 +310,7 @@ pulpcore/plugin/modelresources.py,sha256=E7w0ajI4Vp3hndkoLYqP0ICVjuj-D44aDF_OyGS
308
310
  pulpcore/plugin/publication_utils.py,sha256=QOpOdVC6iotaBnDBUaj2VLlgcRKPx11CTNMdd-GjEyY,802
309
311
  pulpcore/plugin/pulp_hashlib.py,sha256=imCPNxDcUEb7t-WMGlyZQwjuGQK-kwUaESNkiU5nYJE,157
310
312
  pulpcore/plugin/replica.py,sha256=7TzKxW98ju7rXslugyBL4RUW-zHsUu_Nbd9cE-43sNQ,58
311
- pulpcore/plugin/repo_version_utils.py,sha256=nuo55r5rQgDoFfdaobuyvqINW1B8k5xM618Jg-LHBws,5081
313
+ pulpcore/plugin/repo_version_utils.py,sha256=aFLnE04-Azd0Qq3qBnHKcw2MOLBMGpF_ncwjTginswI,5270
312
314
  pulpcore/plugin/responses.py,sha256=VcgJtVaTzCPjdIGz_AS4nQwO4ooh4O5T9KBEa1FGA1w,62
313
315
  pulpcore/plugin/storage.py,sha256=CMoWFPfZG82-SOmuuaf8aPDYerK7mvxlER_cgWdJzQA,82
314
316
  pulpcore/plugin/sync.py,sha256=IwAUZ_7vVYuba2Uhm0ndMyEnNZTkOhX0kOVWtb8SgxE,1002
@@ -334,8 +336,8 @@ pulpcore/tasking/_util.py,sha256=fPW4k1nUa_NZ0ywy_A15Fuiejo5stY58abPbZTXw5t8,990
334
336
  pulpcore/tasking/entrypoint.py,sha256=eAypZD4ORoNOrmBeMdbwO9p6GSQ59bMvZ3TrbnE0czw,1305
335
337
  pulpcore/tasking/kafka.py,sha256=76z4DzeXM1WL5uu1HlKnduWeLO3-b-czvGBXdWR6054,3845
336
338
  pulpcore/tasking/storage.py,sha256=zQkwlpC_FDQtmZGZ8vKwHqxvD6CLO_gAS4Q7wijZE-k,3106
337
- pulpcore/tasking/tasks.py,sha256=bYYAFwJGGyl3mgS7Iw3JZ-vDl1nXbHiAbt3LBSq-nqQ,12946
338
- pulpcore/tasking/worker.py,sha256=N7aiZve1nZs7LRfOEq4HiXH4jjV0m1sFufXfVD81lc8,26073
339
+ pulpcore/tasking/tasks.py,sha256=CTlWLCmxP5-HZjo5_KLYIJQu-VKJnzQ5cyL7IFNRMWw,12944
340
+ pulpcore/tasking/worker.py,sha256=pp_9e8fTUgmghiUZI6NDQlVVxv8hpNes4SnFnqVvawQ,27195
339
341
  pulpcore/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
340
342
  pulpcore/tests/functional/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
341
343
  pulpcore/tests/functional/content_with_coverage.py,sha256=gQK8himy32s9O9vpXdgoM6-_z2KySaXm5rTga9z0jGI,260
@@ -361,10 +363,10 @@ pulpcore/tests/functional/api/test_scoping.py,sha256=uiLOsx5_7puRMcvrpPKEYQziqlu
361
363
  pulpcore/tests/functional/api/test_signing_service.py,sha256=yr1HXBrNoliBHJNAGAN4PAN0eBKPIvAQP-uMoMSrO_I,222
362
364
  pulpcore/tests/functional/api/test_status.py,sha256=Vmmj-ueGxJw1JFSQtr5feTM8vjgyyROvxsRm-OgzLUQ,5370
363
365
  pulpcore/tests/functional/api/test_task_purge.py,sha256=Av4DrUdCqf-JegfoP1pkY4B-teoUzYd1LBZKAhDa-08,7273
364
- pulpcore/tests/functional/api/test_tasking.py,sha256=jozm0Bpv1AavPDSu0QOZMCzre9Zgpw7_M9HCiaoTRmA,22511
366
+ pulpcore/tests/functional/api/test_tasking.py,sha256=UELvclncYdiqRBEC_oZZ_HoNLV4SW9isMaPQHjxw2Q4,22388
365
367
  pulpcore/tests/functional/api/test_upload.py,sha256=oLP1ZmQgPzgK5jAQwGeXS8uLFHgAzVeLW0GfANMWexI,6794
366
368
  pulpcore/tests/functional/api/test_users_groups.py,sha256=YFG0xtyJuIRraczR7ERl_UNS7dlJfKd2eUmXgD1lLBU,2926
367
- pulpcore/tests/functional/api/test_workers.py,sha256=u3oQnErjf6qPCg08XMRZzecGetLLDWmvHvoZIk-AUAA,4659
369
+ pulpcore/tests/functional/api/test_workers.py,sha256=XJrQdxt0BpMeMVOdTyzcTEMk5bB8XC4rA8U580HnzBc,4691
368
370
  pulpcore/tests/functional/api/using_plugin/__init__.py,sha256=QyyfzgjLOi4n32G3o9aGH5eQDNjjD_qUpHLOZpPPZa4,80
369
371
  pulpcore/tests/functional/api/using_plugin/test_checkpoint.py,sha256=l1dE-CieRU_9wheQu-8Y7aAnGSKUztOJ6gaa4a0yoA8,8562
370
372
  pulpcore/tests/functional/api/using_plugin/test_content_access.py,sha256=Ym800bU-M48RCDfQMkVa1UQt_sfgy5ciU0FxorCk9Ds,2551
@@ -406,7 +408,7 @@ pulpcore/tests/unit/test_viewsets.py,sha256=6rek28Rr0kEuYjQZ0_kTSnKsTvmMmD3l-WV_
406
408
  pulpcore/tests/unit/test_vulnerability_report.py,sha256=KFehXFns2gIkGQ-zWsXyK--d8CqVfHgijlWbMI8QmF0,2986
407
409
  pulpcore/tests/unit/content/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
408
410
  pulpcore/tests/unit/content/test_handler.py,sha256=f4F9RAqCP60PUanYdeLW_A955UjRt8eCTrRuh0mChDU,19774
409
- pulpcore/tests/unit/content/test_heartbeat.py,sha256=2hkl4CZQgQ3h6czusb4M_xCLMxFZ4knuss3KhEzCc1U,1173
411
+ pulpcore/tests/unit/content/test_heartbeat.py,sha256=Xtj4cvyI0jBsFZskcypwxruKgMh5M9cgNXQGDWhXMP4,1250
410
412
  pulpcore/tests/unit/download/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
411
413
  pulpcore/tests/unit/download/test_downloader_base.py,sha256=TYG_OPuyj-_N5L-zLTW1qJD_29nKFMA_PC-fPfLKLOo,1281
412
414
  pulpcore/tests/unit/download/test_downloader_factory.py,sha256=mumtIAtRg_dS2uQvOH3J5NXb9XuvQ53iWlBP5koZ_nM,1177
@@ -419,7 +421,7 @@ pulpcore/tests/unit/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
419
421
  pulpcore/tests/unit/models/test_base.py,sha256=77hnxOFBJYMNbI1YGEaR5yj8VCapNGmEgoD0m8Kr3GY,2618
420
422
  pulpcore/tests/unit/models/test_content.py,sha256=heU0vJKucPIp6py2Ww-eXLvhFopvmK8QjFgzt1jGnYQ,5599
421
423
  pulpcore/tests/unit/models/test_remote.py,sha256=KxXwHdA-wj7D-ZpuVi33cLX43wkEeIzeqF9uMsJGt-k,2354
422
- pulpcore/tests/unit/models/test_repository.py,sha256=rnBw1VOsi2Lv3zez2pV2RDXGk_z70KiaACOtyyXugJM,10379
424
+ pulpcore/tests/unit/models/test_repository.py,sha256=ciwyo7dMl-dxlzHb55eQ-ohEEBPF3-GjbO23mMSccqQ,13791
423
425
  pulpcore/tests/unit/models/test_task.py,sha256=rjxeYe383Zsjk8Ck4inMBBTzR4osCrgTeZNWwmHfbjk,1457
424
426
  pulpcore/tests/unit/roles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
425
427
  pulpcore/tests/unit/roles/test_roles.py,sha256=TkPPCLEHMaxfafsRf_3pc4Z3w8BPTyteY7rFkVo65GM,4973
@@ -436,9 +438,9 @@ pulpcore/tests/unit/stages/test_artifactdownloader.py,sha256=qB1ANdFmNtUnljg8fCd
436
438
  pulpcore/tests/unit/stages/test_stages.py,sha256=H1a2BQLjdZlZvcb_qULp62huZ1xy6ItTcthktVyGU0w,4735
437
439
  pulpcore/tests/unit/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
438
440
  pulpcore/tests/unit/viewsets/test_viewset_base.py,sha256=W9o3V6758bZctR6krMPPQytb0xJuF-jb4uBWTNDoD_U,4837
439
- pulpcore-3.85.1.dist-info/licenses/LICENSE,sha256=dhnHU8rJXUdAIgIjveSKAyYG_KzN5eVG-bxETIGrNW0,17988
440
- pulpcore-3.85.1.dist-info/METADATA,sha256=vCiJgls67GdXGs5EoZpWu9DnSn8jJjM-zNM0wJkboIY,4105
441
- pulpcore-3.85.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
442
- pulpcore-3.85.1.dist-info/entry_points.txt,sha256=OZven4wzXzQA5b5q9MpP4HUpIPPQCSvIOvkKtNInrK0,452
443
- pulpcore-3.85.1.dist-info/top_level.txt,sha256=6h-Lm3FKQSaT_nL1KSxu_hBnzKE15bcvf_BoU-ea4CI,34
444
- pulpcore-3.85.1.dist-info/RECORD,,
441
+ pulpcore-3.86.0.dist-info/licenses/LICENSE,sha256=dhnHU8rJXUdAIgIjveSKAyYG_KzN5eVG-bxETIGrNW0,17988
442
+ pulpcore-3.86.0.dist-info/METADATA,sha256=1JVEkZojL684SzjlPGm99WNzSTnuSeXODPsyYJg9UY0,4105
443
+ pulpcore-3.86.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
444
+ pulpcore-3.86.0.dist-info/entry_points.txt,sha256=OZven4wzXzQA5b5q9MpP4HUpIPPQCSvIOvkKtNInrK0,452
445
+ pulpcore-3.86.0.dist-info/top_level.txt,sha256=6h-Lm3FKQSaT_nL1KSxu_hBnzKE15bcvf_BoU-ea4CI,34
446
+ pulpcore-3.86.0.dist-info/RECORD,,