pulp-python 3.31.2__tar.gz → 3.32.1__tar.gz

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.
Files changed (100) hide show
  1. {pulp_python-3.31.2 → pulp_python-3.32.1}/CHANGES.md +55 -0
  2. {pulp_python-3.31.2 → pulp_python-3.32.1}/PKG-INFO +2 -2
  3. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/__init__.py +1 -1
  4. pulp_python-3.32.1/pulp_python/app/cache.py +33 -0
  5. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/pypi/views.py +24 -3
  6. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/pytest_plugin.py +14 -12
  7. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_attestations.py +14 -16
  8. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_pypi_apis.py +18 -9
  9. pulp_python-3.32.1/pulp_python/tests/functional/api/test_simple_cache.py +141 -0
  10. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_upload.py +3 -3
  11. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python.egg-info/PKG-INFO +2 -2
  12. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python.egg-info/SOURCES.txt +2 -2
  13. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python.egg-info/requires.txt +1 -1
  14. {pulp_python-3.31.2 → pulp_python-3.32.1}/pyproject.toml +5 -4
  15. pulp_python-3.31.2/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +0 -1
  16. pulp_python-3.31.2/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +0 -1
  17. {pulp_python-3.31.2 → pulp_python-3.32.1}/COMMITMENT +0 -0
  18. {pulp_python-3.31.2 → pulp_python-3.32.1}/COPYRIGHT +0 -0
  19. {pulp_python-3.31.2 → pulp_python-3.32.1}/LICENSE +0 -0
  20. {pulp_python-3.31.2 → pulp_python-3.32.1}/MANIFEST.in +0 -0
  21. {pulp_python-3.31.2 → pulp_python-3.32.1}/README.md +0 -0
  22. {pulp_python-3.31.2 → pulp_python-3.32.1}/functest_requirements.txt +0 -0
  23. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/__init__.py +0 -0
  24. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/exceptions.py +0 -0
  25. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/global_access_conditions.py +0 -0
  26. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/management/__init__.py +0 -0
  27. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/management/commands/__init__.py +0 -0
  28. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
  29. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0001_initial.py +0 -0
  30. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
  31. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
  32. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
  33. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
  34. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
  35. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
  36. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
  37. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
  38. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
  39. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
  40. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
  41. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0012_add_domain.py +0 -0
  42. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
  43. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
  44. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
  45. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
  46. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
  47. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
  48. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
  49. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py +0 -0
  50. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py +0 -0
  51. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/0022_pythonblocklistentry.py +0 -0
  52. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/migrations/__init__.py +0 -0
  53. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/modelresource.py +0 -0
  54. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/models.py +0 -0
  55. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/provenance.py +0 -0
  56. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/pypi/__init__.py +0 -0
  57. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/pypi/serializers.py +0 -0
  58. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/replica.py +0 -0
  59. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/serializers.py +0 -0
  60. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/settings.py +0 -0
  61. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/tasks/__init__.py +0 -0
  62. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/tasks/publish.py +0 -0
  63. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/tasks/repair.py +0 -0
  64. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/tasks/sync.py +0 -0
  65. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/tasks/upload.py +0 -0
  66. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/tasks/vulnerability_report.py +0 -0
  67. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/urls.py +0 -0
  68. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/utils.py +0 -0
  69. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/viewsets.py +0 -0
  70. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/webserver_snippets/__init__.py +0 -0
  71. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/webserver_snippets/apache.conf +0 -0
  72. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
  73. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/__init__.py +0 -0
  74. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/__init__.py +0 -0
  75. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/__init__.py +0 -0
  76. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
  77. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_blocklist.py +0 -0
  78. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
  79. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
  80. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
  81. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
  82. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_domains.py +0 -0
  83. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_download_content.py +0 -0
  84. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_export_import.py +0 -0
  85. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
  86. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
  87. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_rbac.py +0 -0
  88. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_repair.py +0 -0
  89. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_sync.py +0 -0
  90. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
  91. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/constants.py +0 -0
  92. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/functional/utils.py +0 -0
  93. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/unit/__init__.py +0 -0
  94. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python/tests/unit/test_models.py +0 -0
  95. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python.egg-info/dependency_links.txt +0 -0
  96. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python.egg-info/entry_points.txt +0 -0
  97. {pulp_python-3.31.2 → pulp_python-3.32.1}/pulp_python.egg-info/top_level.txt +0 -0
  98. {pulp_python-3.31.2 → pulp_python-3.32.1}/setup.cfg +0 -0
  99. {pulp_python-3.31.2 → pulp_python-3.32.1}/test_requirements.txt +0 -0
  100. {pulp_python-3.31.2 → pulp_python-3.32.1}/unittest_requirements.txt +0 -0
@@ -8,6 +8,29 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
+ ## 3.32.1 (2026-07-29) {: #3.32.1 }
12
+
13
+ #### Bugfixes {: #3.32.1-bugfix }
14
+
15
+ - Fixed Simple API cache incorrectly serving JSON responses for HTML requests when clients used `?format=json`.
16
+ [#1302](https://github.com/pulp/pulp_python/issues/1302)
17
+
18
+ ---
19
+
20
+ ## 3.32.0 (2026-07-21) {: #3.32.0 }
21
+
22
+ #### Features {: #3.32.0-feature }
23
+
24
+ - Added server-side caching and HTTP cache headers (`Cache-Control`, `ETag`) to Simple API responses.
25
+ [#1254](https://github.com/pulp/pulp_python/issues/1254)
26
+
27
+ #### Bugfixes {: #3.32.0-bugfix }
28
+
29
+ - Fixed a bug where the `python_version` field was not parsed correctly when Pulp workers had hyphens in their name.
30
+ - Fixed provenance sync failing when syncing Pulp-created attestations that use a `Pulp User` publisher.
31
+
32
+ ---
33
+
11
34
  ## 3.31.2 (2026-07-15) {: #3.31.2 }
12
35
 
13
36
  #### Bugfixes {: #3.31.2-bugfix }
@@ -135,6 +158,22 @@
135
158
 
136
159
  ---
137
160
 
161
+ ## 3.27.6 (2026-07-16) {: #3.27.6 }
162
+
163
+ #### Bugfixes {: #3.27.6-bugfix }
164
+
165
+ - Fixed provenance sync failing when syncing Pulp-created attestations that use a `Pulp User` publisher.
166
+
167
+ ---
168
+
169
+ ## 3.27.5 (2026-06-30) {: #3.27.5 }
170
+
171
+ #### Bugfixes {: #3.27.5-bugfix }
172
+
173
+ - Fixed a bug where the `python_version` field was not parsed correctly when Pulp workers had hyphens in their name.
174
+
175
+ ---
176
+
138
177
  ## 3.27.4 (2026-06-05) {: #3.27.4 }
139
178
 
140
179
  #### Bugfixes {: #3.27.4-bugfix }
@@ -373,6 +412,14 @@
373
412
 
374
413
  ---
375
414
 
415
+ ## 3.19.4 (2026-06-30) {: #3.19.4 }
416
+
417
+ #### Bugfixes {: #3.19.4-bugfix }
418
+
419
+ - Fixed a bug where the `python_version` field was not parsed correctly when Pulp workers had hyphens in their name.
420
+
421
+ ---
422
+
376
423
  ## 3.19.3 (2026-05-04) {: #3.19.3 }
377
424
 
378
425
  #### Bugfixes {: #3.19.3-bugfix }
@@ -496,6 +543,14 @@ No significant changes.
496
543
 
497
544
  ---
498
545
 
546
+ ## 3.13.7 (2026-06-30) {: #3.13.7 }
547
+
548
+ #### Bugfixes {: #3.13.7-bugfix }
549
+
550
+ - Fixed a bug where the `python_version` field was not parsed correctly when Pulp workers had hyphens in their name.
551
+
552
+ ---
553
+
499
554
  ## 3.13.6 (2026-04-01) {: #3.13.6 }
500
555
 
501
556
  #### Bugfixes {: #3.13.6-bugfix }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.31.2
3
+ Version: 3.32.1
4
4
  Summary: pulp-python plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
20
20
  Requires-Python: >=3.11
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: pulpcore<3.115,>=3.105.0
23
+ Requires-Dist: pulpcore<3.130,>=3.105.0
24
24
  Requires-Dist: pkginfo<1.13.0,>=1.12.0
25
25
  Requires-Dist: bandersnatch<6.7,>=6.6.0
26
26
  Requires-Dist: pypi-simple<2.0,>=1.8.0
@@ -12,7 +12,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
12
12
 
13
13
  name = "pulp_python.app"
14
14
  label = "python"
15
- version = "3.31.2"
15
+ version = "3.32.1"
16
16
  python_package_name = "pulp-python"
17
17
  domain_compatible = True
18
18
 
@@ -0,0 +1,33 @@
1
+ from pulpcore.plugin.cache import CacheKeys, SyncContentCache
2
+ from pulpcore.plugin.util import cache_key
3
+
4
+ MEDIA_TYPE_KEY = "media_type"
5
+
6
+
7
+ class PythonApiCache(SyncContentCache):
8
+ """
9
+ Cache for the Simple API.
10
+
11
+ Keys on the negotiated media type so HTML and JSON are cached separately,
12
+ including when the same Accept header is used with `?format=json`.
13
+ """
14
+
15
+ def __init__(self, base_key=None):
16
+ keys = (CacheKeys.path, CacheKeys.method, MEDIA_TYPE_KEY)
17
+ super().__init__(base_key=base_key, keys=keys)
18
+
19
+ def make_key(self, request):
20
+ all_keys = {
21
+ CacheKeys.path: request.path,
22
+ CacheKeys.method: request.method,
23
+ MEDIA_TYPE_KEY: getattr(request, "accepted_media_type", "") or "",
24
+ }
25
+ return ":".join(all_keys[k] for k in self.keys)
26
+
27
+
28
+ def find_base_path_cached(request, cached):
29
+ """
30
+ Resolve the distribution base_path for use as the Redis cache base_key.
31
+ """
32
+ path = request.resolver_match.kwargs["path"]
33
+ return cache_key(path)
@@ -1,3 +1,4 @@
1
+ import hashlib
1
2
  import logging
2
3
  from datetime import datetime, timedelta, timezone
3
4
  from itertools import chain
@@ -15,9 +16,11 @@ from django.http.response import (
15
16
  HttpResponseBadRequest,
16
17
  HttpResponseForbidden,
17
18
  HttpResponseNotFound,
18
- StreamingHttpResponse,
19
19
  )
20
20
  from django.shortcuts import redirect
21
+ from django.utils.decorators import method_decorator
22
+ from django.views.decorators.cache import cache_control
23
+ from django.views.decorators.http import condition
21
24
  from drf_spectacular.utils import extend_schema
22
25
  from dynaconf import settings
23
26
  from packaging.utils import canonicalize_name
@@ -31,6 +34,7 @@ from pulpcore.plugin.util import get_domain, get_url
31
34
  from pulpcore.plugin.viewsets import OperationPostponedResponse
32
35
 
33
36
  from pulp_python.app import tasks
37
+ from pulp_python.app.cache import PythonApiCache, find_base_path_cached
34
38
  from pulp_python.app.models import (
35
39
  PackageProvenance,
36
40
  PythonDistribution,
@@ -65,6 +69,17 @@ PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
65
69
  PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
66
70
 
67
71
 
72
+ def _etag_func(request, path, **kwargs):
73
+ """Compute unquoted ETag for the condition decorator. Returns None if no repo."""
74
+ try:
75
+ distro = PyPIMixin.get_distribution(path)
76
+ repo_ver = PyPIMixin.get_repository_version(distro)
77
+ except Http404:
78
+ return None
79
+ raw = f"{repo_ver.number}:{repo_ver.pulp_created.isoformat()}"
80
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]
81
+
82
+
68
83
  class PyPISimpleHTMLRenderer(TemplateHTMLRenderer):
69
84
  media_type = PYPI_SIMPLE_V1_HTML
70
85
 
@@ -298,6 +313,9 @@ class SimpleView(PackageUploadMixin, ViewSet):
298
313
  )
299
314
 
300
315
  @extend_schema(summary="Get index simple page")
316
+ @method_decorator(cache_control(max_age=600, public=True))
317
+ @method_decorator(condition(etag_func=_etag_func))
318
+ @PythonApiCache(base_key=find_base_path_cached)
301
319
  def list(self, request, path):
302
320
  """Gets the simple api html page for the index."""
303
321
  repo_version, content = self.get_rvc()
@@ -316,9 +334,9 @@ class SimpleView(PackageUploadMixin, ViewSet):
316
334
  index_data = write_simple_index_json(names)
317
335
  return Response(index_data, headers=headers)
318
336
  else:
319
- index_data = write_simple_index(names, streamed=True)
337
+ index_data = write_simple_index(names)
320
338
  kwargs = {"content_type": media_type, "headers": headers}
321
- return StreamingHttpResponse(index_data, **kwargs)
339
+ return HttpResponse(index_data, **kwargs)
322
340
 
323
341
  def pull_through_package_simple(self, package, path, remote):
324
342
  """Gets the package's simple page from remote."""
@@ -355,6 +373,9 @@ class SimpleView(PackageUploadMixin, ViewSet):
355
373
  }
356
374
 
357
375
  @extend_schema(operation_id="pypi_simple_package_read", summary="Get package simple page")
376
+ @method_decorator(cache_control(max_age=600, public=True))
377
+ @method_decorator(condition(etag_func=_etag_func))
378
+ @PythonApiCache(base_key=find_base_path_cached)
358
379
  def retrieve(self, request, path, package):
359
380
  """Retrieves the simple api html/json page for a package."""
360
381
  repo_ver, content = self.get_rvc()
@@ -7,11 +7,8 @@ from pulpcore.tests.functional.utils import BindingsNamespace
7
7
 
8
8
  from pulp_python.tests.functional.constants import (
9
9
  PYTHON_EGG_FILENAME,
10
- PYTHON_EGG_URL,
11
10
  PYTHON_FIXTURE_URL,
12
11
  PYTHON_URL,
13
- PYTHON_WHEEL_FILENAME,
14
- PYTHON_WHEEL_URL,
15
12
  PYTHON_XS_PROJECT_SPECIFIER,
16
13
  )
17
14
 
@@ -239,12 +236,17 @@ def get_href(item):
239
236
 
240
237
  @pytest.fixture(scope="session")
241
238
  def python_package_dist_directory(tmp_path_factory, http_get):
242
- """Creates a temp dir to hold package distros for uploading."""
243
- dist_dir = tmp_path_factory.mktemp("dist")
244
- egg_file = dist_dir / PYTHON_EGG_FILENAME
245
- wheel_file = dist_dir / PYTHON_WHEEL_FILENAME
246
- with open(egg_file, "wb") as f:
247
- f.write(http_get(PYTHON_EGG_URL))
248
- with open(wheel_file, "wb") as f:
249
- f.write(http_get(PYTHON_WHEEL_URL))
250
- yield dist_dir, egg_file, wheel_file
239
+ """Returns a factory that downloads packages into a temp directory."""
240
+
241
+ def _download(*urls):
242
+ dist_dir = tmp_path_factory.mktemp("dist")
243
+ paths = []
244
+ for url in urls:
245
+ filename = url.rsplit("/", 1)[-1]
246
+ path = dist_dir / filename
247
+ with open(path, "wb") as f:
248
+ f.write(http_get(url))
249
+ paths.append(path)
250
+ return (dist_dir, *paths)
251
+
252
+ return _download
@@ -1,7 +1,5 @@
1
1
  import json
2
- import shutil
3
2
  import subprocess
4
- from pathlib import Path
5
3
  from urllib.parse import urljoin
6
4
 
7
5
  import pytest
@@ -10,6 +8,8 @@ from pypi_simple import PyPISimple
10
8
 
11
9
  from pulpcore.tests.functional.utils import PulpTaskError
12
10
 
11
+ from pulp_python.tests.functional.constants import PYTHON_FIXTURES_URL
12
+
13
13
 
14
14
  @pytest.fixture(scope="session")
15
15
  def twine_package():
@@ -207,24 +207,22 @@ def test_attestation_twine_upload(
207
207
  python_content_summary,
208
208
  python_empty_repo_distro,
209
209
  python_package_dist_directory,
210
+ http_get,
210
211
  monitor_task,
211
212
  ):
212
213
  """Tests that packages with attestations can be properly uploaded through Twine."""
214
+ packages_url = urljoin(PYTHON_FIXTURES_URL, "packages/")
215
+ filenames = ("twine-6.2.0.tar.gz", "twine-6.2.0-py3-none-any.whl")
216
+ dist_dir = python_package_dist_directory(*(urljoin(packages_url, f) for f in filenames))[0]
217
+
218
+ for filename in filenames:
219
+ provenance = json.loads(http_get(urljoin(packages_url, f"{filename}.provenance.json")))
220
+ attestation = provenance["attestation_bundles"][0]["attestations"][0]
221
+ with open(dist_dir / f"{filename}.publish.attestation", "w") as f:
222
+ json.dump(attestation, f)
223
+
213
224
  repo, distro = python_empty_repo_distro()
214
225
  url = urljoin(distro.base_url, "legacy/")
215
- dist_dir, _, _ = python_package_dist_directory
216
-
217
- # Copy attestation files from test assets to dist_dir
218
- assets_dir = Path(__file__).parent.parent / "assets"
219
- attestation_files = [
220
- "shelf-reader-0.1.tar.gz.publish.attestation",
221
- "shelf_reader-0.1-py2-none-any.whl.publish.attestation",
222
- ]
223
- for attestation_file in attestation_files:
224
- src = assets_dir / attestation_file
225
- dst = dist_dir / attestation_file
226
- shutil.copy2(src, dst)
227
-
228
226
  username, password = "admin", "password"
229
227
  subprocess.run(
230
228
  (
@@ -245,7 +243,7 @@ def test_attestation_twine_upload(
245
243
  tasks = pulpcore_bindings.TasksApi.list(reserved_resources=repo.pulp_href).results
246
244
  for task in reversed(tasks):
247
245
  t = monitor_task(task.pulp_href)
248
- repo_ver_href = t.created_resources[0]
246
+ repo_ver_href = [r for r in t.created_resources if "versions" in r][0]
249
247
 
250
248
  assert repo_ver_href.endswith("versions/2/")
251
249
  summary = python_content_summary(repository_version=repo_ver_href)
@@ -11,10 +11,13 @@ from pulp_python.tests.functional.constants import (
11
11
  PYPI_SIMPLE_V1_JSON,
12
12
  PYTHON_EGG_FILENAME,
13
13
  PYTHON_EGG_SHA256,
14
+ PYTHON_EGG_URL,
15
+ PYTHON_FIXTURES_URL,
14
16
  PYTHON_MD_PROJECT_SPECIFIER,
15
17
  PYTHON_MD_PYPI_SUMMARY,
16
18
  PYTHON_WHEEL_FILENAME,
17
19
  PYTHON_WHEEL_SHA256,
20
+ PYTHON_WHEEL_URL,
18
21
  SHELF_PYTHON_JSON,
19
22
  TWINE_WHEEL_FILENAME,
20
23
  TWINE_WHEEL_URL,
@@ -70,7 +73,7 @@ def test_package_upload(
70
73
  ):
71
74
  """Tests that packages can be uploaded."""
72
75
  repo, distro = python_empty_repo_distro()
73
- dist_dir, egg_file, wheel_file = python_package_dist_directory
76
+ dist_dir, egg_file, wheel_file = python_package_dist_directory(PYTHON_EGG_URL, PYTHON_WHEEL_URL)
74
77
  url = urljoin(distro.base_url, "legacy/")
75
78
  response = requests.post(
76
79
  url,
@@ -100,7 +103,7 @@ def test_package_upload_session(
100
103
  """Tests that multiple uploads will be broken up into multiple tasks."""
101
104
  repo, distro = python_empty_repo_distro()
102
105
  url = urljoin(distro.base_url, "legacy/")
103
- dist_dir, egg_file, wheel_file = python_package_dist_directory
106
+ dist_dir, egg_file, wheel_file = python_package_dist_directory(PYTHON_EGG_URL, PYTHON_WHEEL_URL)
104
107
  session = requests.Session()
105
108
  response = session.post(
106
109
  url,
@@ -130,7 +133,7 @@ def test_package_upload_simple(
130
133
  """Tests that the package upload endpoint exposed at `/simple/` works."""
131
134
  repo, distro = python_empty_repo_distro()
132
135
  url = urljoin(distro.base_url, "simple/")
133
- dist_dir, egg_file, wheel_file = python_package_dist_directory
136
+ dist_dir, egg_file, wheel_file = python_package_dist_directory(PYTHON_EGG_URL, PYTHON_WHEEL_URL)
134
137
  response = requests.post(
135
138
  url,
136
139
  data={"sha256_digest": PYTHON_EGG_SHA256},
@@ -156,7 +159,7 @@ def test_package_upload_with_metadata(
156
159
  """
157
160
  repo, distro = python_empty_repo_distro()
158
161
  url = urljoin(distro.base_url, "simple/")
159
- dist_dir, egg_file, wheel_file = python_package_dist_directory
162
+ dist_dir, egg_file, wheel_file = python_package_dist_directory(PYTHON_EGG_URL, PYTHON_WHEEL_URL)
160
163
  response = requests.post(
161
164
  url,
162
165
  data={"sha256_digest": PYTHON_WHEEL_SHA256},
@@ -183,9 +186,14 @@ def test_twine_upload(
183
186
  monitor_task,
184
187
  ):
185
188
  """Tests that packages can be properly uploaded through Twine."""
189
+ packages_url = urljoin(PYTHON_FIXTURES_URL, "packages/")
190
+ dist_dir, _, _ = python_package_dist_directory(
191
+ urljoin(packages_url, "pytz-2023.3.tar.gz"),
192
+ urljoin(packages_url, "pytz-2023.3-py2.py3-none-any.whl"),
193
+ )
194
+
186
195
  repo, distro = python_empty_repo_distro()
187
196
  url = urljoin(distro.base_url, "legacy/")
188
- dist_dir, _, _ = python_package_dist_directory
189
197
  username, password = "admin", "password"
190
198
  subprocess.run(
191
199
  (
@@ -205,7 +213,7 @@ def test_twine_upload(
205
213
  tasks = pulpcore_bindings.TasksApi.list(reserved_resources=repo.pulp_href).results
206
214
  for task in reversed(tasks):
207
215
  t = monitor_task(task.pulp_href)
208
- repo_ver_href = t.created_resources[-1]
216
+ repo_ver_href = [r for r in t.created_resources if "versions" in r][0]
209
217
  summary = python_content_summary(repository_version=repo_ver_href)
210
218
  assert summary.present["python.python"]["count"] == 2
211
219
 
@@ -245,8 +253,9 @@ def test_simple_redirect_with_publications(
245
253
  assert response.url == str(urljoin(pulp_content_url, f"{distro.base_path}/simple/"))
246
254
 
247
255
 
248
- @pytest.mark.parallel
249
- def test_pypi_json(python_remote_factory, python_repo_with_sync, python_distribution_factory):
256
+ def test_pypi_json(
257
+ delete_orphans_pre, python_remote_factory, python_repo_with_sync, python_distribution_factory
258
+ ):
250
259
  """Checks the data of `pypi/{package_name}/json` endpoint."""
251
260
  remote = python_remote_factory(policy="immediate")
252
261
  repo = python_repo_with_sync(remote)
@@ -260,8 +269,8 @@ def test_pypi_json(python_remote_factory, python_repo_with_sync, python_distribu
260
269
  assert_pypi_json(response.json())
261
270
 
262
271
 
263
- @pytest.mark.parallel
264
272
  def test_pypi_json_content_app(
273
+ delete_orphans_pre,
265
274
  python_remote_factory,
266
275
  python_repo_with_sync,
267
276
  python_publication_factory,
@@ -0,0 +1,141 @@
1
+ from urllib.parse import urljoin
2
+
3
+ import pytest
4
+ import requests
5
+
6
+ from pulp_python.tests.functional.constants import (
7
+ PYPI_SIMPLE_V1_HTML,
8
+ PYPI_SIMPLE_V1_JSON,
9
+ PYPI_TEXT_HTML,
10
+ PYTHON_SM_PROJECT_SPECIFIER,
11
+ )
12
+
13
+
14
+ @pytest.fixture
15
+ def skip_without_cache(pulp_settings):
16
+ """
17
+ Skip test if server-side caching is not enabled.
18
+ """
19
+ if not pulp_settings.CACHE_ENABLED:
20
+ pytest.skip("CACHE_ENABLED is not set")
21
+
22
+
23
+ @pytest.fixture
24
+ def synced_distro(
25
+ skip_without_cache,
26
+ python_remote_factory,
27
+ python_repo_with_sync,
28
+ python_distribution_factory,
29
+ ):
30
+ """
31
+ Sync a repo and create a distribution for cache tests.
32
+ """
33
+ remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
34
+ repo = python_repo_with_sync(remote)
35
+ return python_distribution_factory(repository=repo)
36
+
37
+
38
+ @pytest.mark.parallel
39
+ def test_simple_cache_hit_miss_and_headers(synced_distro):
40
+ """
41
+ First request is a MISS, second is a HIT. Cache headers are present and stable.
42
+ """
43
+ index_url = urljoin(synced_distro.base_url, "simple/")
44
+ detail_url = f"{index_url}aiohttp"
45
+
46
+ for url in [index_url, detail_url]:
47
+ r1 = requests.get(url)
48
+ assert r1.status_code == 200
49
+ assert r1.headers["X-PULP-CACHE"] == "MISS"
50
+ assert r1.headers["Cache-Control"] == "max-age=600, public"
51
+ assert r1.headers["ETag"].startswith('"') and r1.headers["ETag"].endswith('"')
52
+
53
+ r2 = requests.get(url)
54
+ assert r2.status_code == 200
55
+ assert r2.headers["X-PULP-CACHE"] == "HIT"
56
+ assert r2.headers["Cache-Control"] == r1.headers["Cache-Control"]
57
+ assert r2.headers["ETag"] == r1.headers["ETag"]
58
+
59
+
60
+ @pytest.mark.parallel
61
+ def test_simple_cache_separate_accept_headers(synced_distro):
62
+ """
63
+ HTML and JSON responses are cached separately by negotiated media type.
64
+ """
65
+ url = urljoin(synced_distro.base_url, "simple/")
66
+
67
+ for header in [PYPI_SIMPLE_V1_HTML, PYPI_SIMPLE_V1_JSON]:
68
+ r = requests.get(url, headers={"Accept": header})
69
+ assert r.status_code == 200
70
+ assert r.headers["X-PULP-CACHE"] == "MISS"
71
+
72
+ for header in [PYPI_SIMPLE_V1_HTML, PYPI_SIMPLE_V1_JSON]:
73
+ r = requests.get(url, headers={"Accept": header})
74
+ assert r.status_code == 200
75
+ assert r.headers["X-PULP-CACHE"] == "HIT"
76
+
77
+
78
+ @pytest.mark.parallel
79
+ def test_simple_cache_format_json_does_not_poison_html(synced_distro):
80
+ """
81
+ A ?format=json response must not poison a later request with the same Accept.
82
+
83
+ Clients like uv/pip send an Accept that allows both JSON and HTML. DRF's
84
+ ?format=json overrides negotiation to JSON, while the same Accept without
85
+ that query param selects HTML. Caching must key on the negotiated type so
86
+ the JSON entry is not served (and re-rendered) for the HTML request.
87
+ """
88
+ url = f"{urljoin(synced_distro.base_url, 'simple/')}aiohttp"
89
+ # pip/uv-style Accept: JSON preferred, HTML still acceptable
90
+ headers = {
91
+ "Accept": (f"{PYPI_SIMPLE_V1_JSON}, {PYPI_SIMPLE_V1_HTML};q=0.1, {PYPI_TEXT_HTML};q=0.01")
92
+ }
93
+
94
+ r_json = requests.get(url, headers=headers, params={"format": "json"})
95
+ assert r_json.status_code == 200
96
+ assert PYPI_SIMPLE_V1_JSON in r_json.headers["Content-Type"]
97
+ assert r_json.headers["X-PULP-CACHE"] == "MISS"
98
+ assert r_json.json()["name"] == "aiohttp"
99
+
100
+ r_html = requests.get(url, headers=headers)
101
+ assert r_html.status_code == 200
102
+ assert PYPI_TEXT_HTML in r_html.headers["Content-Type"]
103
+ assert r_html.headers["X-PULP-CACHE"] == "MISS"
104
+ assert b"<a href=" in r_html.content
105
+
106
+ r_html_hit = requests.get(url, headers=headers)
107
+ assert r_html_hit.status_code == 200
108
+ assert r_html_hit.headers["X-PULP-CACHE"] == "HIT"
109
+ assert PYPI_TEXT_HTML in r_html_hit.headers["Content-Type"]
110
+
111
+ r_json_hit = requests.get(url, headers=headers, params={"format": "json"})
112
+ assert r_json_hit.status_code == 200
113
+ assert r_json_hit.headers["X-PULP-CACHE"] == "HIT"
114
+ assert PYPI_SIMPLE_V1_JSON in r_json_hit.headers["Content-Type"]
115
+
116
+
117
+ @pytest.mark.parallel
118
+ def test_simple_cache_etag_conditional_request(synced_distro):
119
+ """
120
+ Matching If-None-Match returns 304, non-matching returns 200.
121
+ """
122
+ url = urljoin(synced_distro.base_url, "simple/")
123
+
124
+ r1 = requests.get(url)
125
+ assert r1.status_code == 200
126
+ etag = r1.headers["ETag"]
127
+ cache_control = r1.headers["Cache-Control"]
128
+
129
+ r2 = requests.get(url, headers={"If-None-Match": etag})
130
+ assert r2.status_code == 304
131
+ assert r2.headers["ETag"] == etag
132
+ assert r2.headers["Cache-Control"] == cache_control
133
+ assert "X-PULP-CACHE" not in r2.headers
134
+ assert len(r2.content) == 0
135
+
136
+ r3 = requests.get(url, headers={"If-None-Match": '"old"'})
137
+ assert r3.status_code == 200
138
+ assert r3.headers["ETag"] == etag
139
+ assert r3.headers["Cache-Control"] == cache_control
140
+ assert r3.headers["X-PULP-CACHE"] == "HIT"
141
+ assert len(r3.content) > 0
@@ -82,7 +82,7 @@ def test_synchronous_package_upload_with_metadata(
82
82
  def test_legacy_upload_invalid_protocol_version(
83
83
  python_empty_repo_distro, python_package_dist_directory
84
84
  ):
85
- _, egg_file, _ = python_package_dist_directory
85
+ _, egg_file, _ = python_package_dist_directory(PYTHON_EGG_URL, PYTHON_WHEEL_URL)
86
86
  _, distro = python_empty_repo_distro()
87
87
  url = urljoin(distro.base_url, "legacy/")
88
88
  with open(egg_file, "rb") as f:
@@ -108,7 +108,7 @@ def test_legacy_upload_invalid_protocol_version(
108
108
 
109
109
  @pytest.mark.parallel
110
110
  def test_legacy_upload_invalid_filetype(python_empty_repo_distro, python_package_dist_directory):
111
- _, egg_file, wheel_file = python_package_dist_directory
111
+ _, egg_file, wheel_file = python_package_dist_directory(PYTHON_EGG_URL, PYTHON_WHEEL_URL)
112
112
  _, distro = python_empty_repo_distro()
113
113
  url = urljoin(distro.base_url, "legacy/")
114
114
  with open(egg_file, "rb") as f:
@@ -140,7 +140,7 @@ def test_legacy_upload_invalid_filetype(python_empty_repo_distro, python_package
140
140
  def test_legacy_upload_invalid_metadata_version(
141
141
  python_empty_repo_distro, python_package_dist_directory
142
142
  ):
143
- _, egg_file, _ = python_package_dist_directory
143
+ _, egg_file, _ = python_package_dist_directory(PYTHON_EGG_URL, PYTHON_WHEEL_URL)
144
144
  _, distro = python_empty_repo_distro()
145
145
  url = urljoin(distro.base_url, "legacy/")
146
146
  with open(egg_file, "rb") as f:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.31.2
3
+ Version: 3.32.1
4
4
  Summary: pulp-python plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
20
20
  Requires-Python: >=3.11
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: pulpcore<3.115,>=3.105.0
23
+ Requires-Dist: pulpcore<3.130,>=3.105.0
24
24
  Requires-Dist: pkginfo<1.13.0,>=1.12.0
25
25
  Requires-Dist: bandersnatch<6.7,>=6.6.0
26
26
  Requires-Dist: pypi-simple<2.0,>=1.8.0
@@ -17,6 +17,7 @@ pulp_python.egg-info/entry_points.txt
17
17
  pulp_python.egg-info/requires.txt
18
18
  pulp_python.egg-info/top_level.txt
19
19
  pulp_python/app/__init__.py
20
+ pulp_python/app/cache.py
20
21
  pulp_python/app/exceptions.py
21
22
  pulp_python/app/global_access_conditions.py
22
23
  pulp_python/app/modelresource.py
@@ -87,10 +88,9 @@ pulp_python/tests/functional/api/test_pypi_apis.py
87
88
  pulp_python/tests/functional/api/test_pypi_simple_api.py
88
89
  pulp_python/tests/functional/api/test_rbac.py
89
90
  pulp_python/tests/functional/api/test_repair.py
91
+ pulp_python/tests/functional/api/test_simple_cache.py
90
92
  pulp_python/tests/functional/api/test_sync.py
91
93
  pulp_python/tests/functional/api/test_upload.py
92
94
  pulp_python/tests/functional/api/test_vulnerability_report.py
93
- pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation
94
- pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation
95
95
  pulp_python/tests/unit/__init__.py
96
96
  pulp_python/tests/unit/test_models.py
@@ -1,4 +1,4 @@
1
- pulpcore<3.115,>=3.105.0
1
+ pulpcore<3.130,>=3.105.0
2
2
  pkginfo<1.13.0,>=1.12.0
3
3
  bandersnatch<6.7,>=6.6.0
4
4
  pypi-simple<2.0,>=1.8.0
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
7
7
 
8
8
  [project]
9
9
  name = "pulp-python"
10
- version = "3.31.2"
10
+ version = "3.32.1"
11
11
  description = "pulp-python plugin for the Pulp Project"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -26,7 +26,7 @@ classifiers=[
26
26
  ]
27
27
  requires-python = ">=3.11"
28
28
  dependencies = [
29
- "pulpcore>=3.105.0,<3.115",
29
+ "pulpcore>=3.105.0,<3.130",
30
30
  "pkginfo>=1.12.0,<1.13.0",
31
31
  "bandersnatch>=6.6.0,<6.7",
32
32
  "pypi-simple>=1.8.0,<2.0",
@@ -79,7 +79,7 @@ ignore = [
79
79
  [tool.bumpversion]
80
80
  # This section is managed by the plugin template. Do not edit manually.
81
81
 
82
- current_version = "3.31.2"
82
+ current_version = "3.32.1"
83
83
  commit = false
84
84
  tag = false
85
85
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<alpha>0a)?(?P<patch>\\d+)(\\.(?P<release>[a-z]+))?"
@@ -125,7 +125,6 @@ filename = "./pyproject.toml"
125
125
  search = "version = \"{current_version}\""
126
126
  replace = "version = \"{new_version}\""
127
127
 
128
-
129
128
  [tool.black]
130
129
  line-length = 100
131
130
 
@@ -139,6 +138,7 @@ extend-exclude = [
139
138
 
140
139
  [tool.ruff.lint]
141
140
  # This section is managed by the plugin template. Do not edit manually.
141
+ select = ["E4", "E7", "E9", "F"]
142
142
  extend-select = [
143
143
  "I",
144
144
  "INT",
@@ -149,6 +149,7 @@ extend-select = [
149
149
  [tool.ruff.lint.flake8-tidy-imports.banned-api]
150
150
  # This section is managed by the plugin template. Do not edit manually.
151
151
  "distutils".msg = "The 'distutils' module has been deprecated since Python 3.9."
152
+ "pulpcore.app.settings".msg = "Always import 'settings' from 'django.conf' instead."
152
153
  "pulpcore.app".msg = "The 'pulpcore' apis must only be consumed via 'pulpcore.plugin'."
153
154
 
154
155
  [tool.ruff.lint.isort]
@@ -1 +0,0 @@
1
- {"version":1,"verification_material":{"certificate":"MIIIMzCCB7igAwIBAgIUKrdo9OfFJy6MuUewIeE/ILWw/tswCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUxMjA0MjAzMjM5WhcNMjUxMjA0MjA0MjM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEleubASHneG8xQmJ/HacfPp63RpBRb71lTUW3uqc/dxFttAkIu11LtR5Y1aRa7h/uFP6e9B/3WAV11xiu29vxRqOCBtcwggbTMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUcDHutpRx316lThqpRMpz2kw+/f4wHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwgaUGA1UdEQEB/wSBmjCBl4aBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBCg3OTU4NmMxZGVhZDhiODhmYmZhOWE3ODgyZGExMTZkZWNmNjc0YTQyMC0GCisGAQQBg78wAQQEH0V4dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24wSQYKKwYBBAGDvzABBQQ7c2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24wHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTCBpgYKKwYBBAGDvzABCQSBlwyBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABCgQqDCg3OTU4NmMxZGVhZDhiODhmYmZhOWE3ODgyZGExMTZkZWNmNjc0YTQyMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDBeBgorBgEEAYO/MAEMBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbjA4BgorBgEEAYO/MAENBCoMKDc5NTg2YzFkZWFkOGI4OGZiZmE5YTc4ODJkYTExNmRlY2Y2NzRhNDIwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21haW4wGQYKKwYBBAGDvzABDwQLDAk2MzI1OTY4OTcwNwYKKwYBBAGDvzABEAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UwGQYKKwYBBAGDvzABEQQLDAkxMzE4MDQ1NjMwgaYGCisGAQQBg78wARIEgZcMgZRodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24vLmdpdGh1Yi93b3JrZmxvd3MvZXh0cmVtZWx5LWRhbmdlcm91cy1vaWRjLWJlYWNvbi55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoNzk1ODZjMWRlYWQ4Yjg4ZmJmYTlhNzg4MmRhMTE2ZGVjZjY3NGE0MjAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMIGCBgorBgEEAYO/MAEVBHQMcmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi9hY3Rpb25zL3J1bnMvMTk5NDMwMzAxOTkvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABmusRH1wAAAQDAEcwRQIgMjrB10EwMTKAWmjTMab2AX1YMg1lWJp7xV3yhBx7GyYCIQC5zr44ZzUSPnJ3Gtm6k8M1CBoIzDYWzwBIoRAdh5NE3TAKBggqhkjOPQQDAwNpADBmAjEAgyfhSQno+h3UugP0A6V0A8b++q1IV4hYm4dVfi6skZKkEYMFbs0ocMWqWlJg8PaFAjEAt7+HDiWOfa3MxLPnIdzeEPz4kbgXZqKQDMHY5m1ZG976bnJBwttMAbAEXhELfyHl","transparency_entries":[{"logIndex":"51125016","logId":{"keyId":"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1764880360","inclusionPromise":{"signedEntryTimestamp":"MEUCIQC48Y/s6Rv+IoXoftvGOhM3sccchkODD/YqwIKJgwJAiAIgWkva5KEaCl3P78T21oElOTbq9cOxurz//9N9giFPDWw="},"inclusionProof":{"logIndex":"19442604","rootHash":"EdmMTYTkzJL0XRtufY9DMHS2axO0Vg6jO4eVM/Cetd4=","treeSize":"19442605","hashes":["k5UsnMaUY0kOQlFg0Za5deoskYSOUTFQ2Jzl3upcbX0=","F8+CGCoMNj9Jb1Csuk4bxbAWW35iktydoLKPPLk6Hb8=","P6IFMwnZlp99wiunjusnNW+K5V06iOEtzfoD9MtHCSU=","V4//f/ISXnSeJjUckmcLLzwS38XjXwMjrMAw6B3hKfg=","baLnSFGPpXZDhE38YKkGPW9P35RXQTaZlgDJ2i/u5xA=","YPKEUUZB3AVufkBQm5r7wLJeJMECETlxWoHo3+sKnHE=","VEcYk6UKGgyx1fCjyU6cfCPViC8OZFBjvHz8fco4eGM=","tf738kZ3YKyYiskKppTF5etATmFZgyY6H1ktyC0leCA=","gXQKVsGeO2E9oDbviE7+SuHFG5g7UH35dYGHc9JSz8s=","6kC2DcsZfB5F/TmenKHUzDr2UH6XK//Ch6hC8PZWJ5c=","JA9P8Q8v916uA/xomSrsiXeVBiyxxQAD5Mer9MBIYzA=","BrjYsYvo65dTDWKO8aY0s+9WaWHlVdZuOEzsBHLHe8U="],"checkpoint":{"envelope":"rekor.sigstage.dev - 8202293616175992157\n19442605\nEdmMTYTkzJL0XRtufY9DMHS2axO0Vg6jO4eVM/Cetd4=\n\n— rekor.sigstage.dev 0y8wozBFAiAPcLZnrw7rn8sFhxqGjp5q58EtMUibZptS8W1Mk6CcAAIhAJ6c+pCBCCe4+K1+zxuUsofR9Y56J5pTBvqvfGceJI/l\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiYzJkZjczOTEzMzgyZmVkM2MzNTU5ODRjOTdiN2Y2NTc5YTYxZmVmZDAwOWMxYmM3M2M2ODE0ODk3ZjMxNDY3MyJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjRkMjdjMTZjMTI3ZDM4ZGE2NDU3MGVmMmY3Mjc2MjIwOWVjMzlkMmU0NjhiYjQ0YTU5NjE1MjhlOTMyNDNjNWQifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVZQ0lRQ2hSUGN1N2w5QXBIbTAxNlE3bnpob3pxaHkvYXVBV1NiWWc5MUpxQzluYlFJaEFOdGtFV01hV2RHS0dUZHVNZW4yQ081eDhPY1N0UStyazlnWVFWeDk1YU9VIiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VsTmVrTkRRamRwWjBGM1NVSkJaMGxWUzNKa2J6bFBaa1pLZVRaTmRWVmxkMGxsUlM5SlRGZDNMM1J6ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmVFMXFRVEJOYWtGNlRXcE5OVmRvWTA1TmFsVjRUV3BCTUUxcVFUQk5hazAxVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVnNaWFZpUVZOSWJtVkhPSGhSYlVvdlNHRmpabEJ3TmpOU2NFSlNZamN4YkZSVlZ6TUtkWEZqTDJSNFJuUjBRV3RKZFRFeFRIUlNOVmt4WVZKaE4yZ3ZkVVpRTm1VNVFpOHpWMEZXTVRGNGFYVXlPWFo0VW5GUFEwSjBZM2RuWjJKVVRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVmpSRWgxQ25Sd1VuZ3pNVFpzVkdoeGNGSk5jSG95YTNjckwyWTBkMGgzV1VSV1VqQnFRa0puZDBadlFWVmpXVmwzY0doU09GbHRMelU1T1dJd1FsSndMMWd2TDNJS1lqWjNkMmRoVlVkQk1WVmtSVkZGUWk5M1UwSnRha05DYkRSaFFteEhhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9YcGhWMlI2WkVjNWVRcGFVekZxWWpJMWJXSXpTblJaVnpWcVdsTTViR1ZJVW5sYVZ6RnNZa2hyZEZwSFJuVmFNbFo1WWpOV2VreFlRakZaYlhod1dYa3hkbUZYVW1wTVYwcHNDbGxYVG5aaWFUaDFXakpzTUdGSVZtbE1NMlIyWTIxMGJXSkhPVE5qZVRsc1pVaFNlVnBYTVd4aVNHdDBXa2RHZFZveVZubGlNMVo2VEZjNWNGcEhUWFFLV1cxV2FGa3lPWFZNYm14MFlrVkNlVnBYV25wTU1taHNXVmRTZWt3eU1XaGhWelIzVDFGWlMwdDNXVUpDUVVkRWRucEJRa0ZSVVhKaFNGSXdZMGhOTmdwTWVUa3dZakowYkdKcE5XaFpNMUp3WWpJMWVreHRaSEJrUjJneFdXNVdlbHBZU21waU1qVXdXbGMxTUV4dFRuWmlWRUZtUW1kdmNrSm5SVVZCV1U4dkNrMUJSVU5DUWtZellqTktjbHB0ZUhaa01UbHJZVmhPZDFsWVVtcGhSRUV5UW1kdmNrSm5SVVZCV1U4dlRVRkZSRUpEWnpOUFZGVTBUbTFOZUZwSFZtZ0tXa1JvYVU5RWFHMVpiVnBvVDFkRk0wOUVaM2xhUjBWNFRWUmFhMXBYVG0xT2FtTXdXVlJSZVUxRE1FZERhWE5IUVZGUlFtYzNPSGRCVVZGRlNEQldOQXBrU0Vwc1lsZFdjMlZUUW10WlZ6VnVXbGhLZG1SWVRXZFVNR3hGVVhsQ2FWcFhSbXBpTWpSM1UxRlpTMHQzV1VKQ1FVZEVkbnBCUWtKUlVUZGpNbXh1Q21NelVuWmpiVlYwV1RJNWRWcHRPWGxpVjBaMVdUSlZkbHBZYURCamJWWjBXbGQ0TlV4WFVtaGliV1JzWTIwNU1XTjVNWGRrVjBwellWZE5kR0l5YkdzS1dYa3hhVnBYUm1waU1qUjNTRkZaUzB0M1dVSkNRVWRFZG5wQlFrSm5VVkJqYlZadFkzazViMXBYUm10amVUbDBXVmRzZFUxRWMwZERhWE5IUVZGUlFncG5OemgzUVZGblJVeFJkM0poU0ZJd1kwaE5Oa3g1T1RCaU1uUnNZbWsxYUZrelVuQmlNalY2VEcxa2NHUkhhREZaYmxaNldsaEthbUl5TlRCYVZ6VXdDa3h0VG5aaVZFTkNjR2RaUzB0M1dVSkNRVWRFZG5wQlFrTlJVMEpzZDNsQ2JFZG9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWVtRlhaSG9LWkVjNWVWcFRNV3BpTWpWdFlqTktkRmxYTldwYVV6bHNaVWhTZVZwWE1XeGlTR3QwV2tkR2RWb3lWbmxpTTFaNlRGaENNVmx0ZUhCWmVURjJZVmRTYWdwTVYwcHNXVmRPZG1KcE9IVmFNbXd3WVVoV2FVd3paSFpqYlhSdFlrYzVNMk41T1d4bFNGSjVXbGN4YkdKSWEzUmFSMFoxV2pKV2VXSXpWbnBNVnpsd0NscEhUWFJaYlZab1dUSTVkVXh1YkhSaVJVSjVXbGRhZWt3eWFHeFpWMUo2VERJeGFHRlhOSGRQUVZsTFMzZFpRa0pCUjBSMmVrRkNRMmRSY1VSRFp6TUtUMVJWTkU1dFRYaGFSMVpvV2tSb2FVOUVhRzFaYlZwb1QxZEZNMDlFWjNsYVIwVjRUVlJhYTFwWFRtMU9hbU13V1ZSUmVVMUNNRWREYVhOSFFWRlJRZ3BuTnpoM1FWRnpSVVIzZDA1YU1td3dZVWhXYVV4WGFIWmpNMUpzV2tSQ1pVSm5iM0pDWjBWRlFWbFBMMDFCUlUxQ1JrRk5WRzFvTUdSSVFucFBhVGgyQ2xveWJEQmhTRlpwVEcxT2RtSlRPWHBoVjJSNlpFYzVlVnBUTVdwaU1qVnRZak5LZEZsWE5XcGFVemxzWlVoU2VWcFhNV3hpU0d0MFdrZEdkVm95Vm5rS1lqTldla3hZUWpGWmJYaHdXWGt4ZG1GWFVtcE1WMHBzV1ZkT2RtSnFRVFJDWjI5eVFtZEZSVUZaVHk5TlFVVk9Ra052VFV0RVl6Vk9WR2N5V1hwR2F3cGFWMFpyVDBkSk5FOUhXbWxhYlVVMVdWUmpORTlFU210WlZFVjRUbTFTYkZreVdUSk9lbEpvVGtSSmQwaDNXVXRMZDFsQ1FrRkhSSFo2UVVKRVoxRlNDa1JCT1hsYVYxcDZUREpvYkZsWFVucE1NakZvWVZjMGQwZFJXVXRMZDFsQ1FrRkhSSFo2UVVKRWQxRk1SRUZyTWsxNlNURlBWRmswVDFSamQwNTNXVXNLUzNkWlFrSkJSMFIyZWtGQ1JVRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkbU50VlhSWk1qbDFXbTA1ZVFwaVYwWjFXVEpWZDBkUldVdExkMWxDUWtGSFJIWjZRVUpGVVZGTVJFRnJlRTE2UlRSTlJGRXhUbXBOZDJkaFdVZERhWE5IUVZGUlFtYzNPSGRCVWtsRkNtZGFZMDFuV2xKdlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVblpqYlZWMFdUSTVkVnB0T1hsaVYwWjFXVEpWZGxwWWFEQUtZMjFXZEZwWGVEVk1WMUpvWW0xa2JHTnRPVEZqZVRGM1pGZEtjMkZYVFhSaU1teHJXWGt4YVZwWFJtcGlNalIyVEcxa2NHUkhhREZaYVRrellqTktjZ3BhYlhoMlpETk5kbHBZYURCamJWWjBXbGQ0TlV4WFVtaGliV1JzWTIwNU1XTjVNWFpoVjFKcVRGZEtiRmxYVG5aaWFUVTFZbGQ0UVdOdFZtMWplVGx2Q2xwWFJtdGplVGwwV1Zkc2RVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVrMUZTMmQzYjA1NmF6RlBSRnBxVFZkU2JGbFhVVFJaYW1jMFdtMUtiVmxVYkdnS1RucG5ORTF0VW1oTlZFVXlXa2RXYWxwcVdUTk9SMFV3VFdwQmFFSm5iM0pDWjBWRlFWbFBMMDFCUlZWQ1FrMU5SVmhrZG1OdGRHMWlSemt6V0RKU2NBcGpNMEpvWkVkT2IwMUpSME5DWjI5eVFtZEZSVUZaVHk5TlFVVldRa2hSVFdOdGFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1hwaFYyUjZDbVJIT1hsYVV6RnFZakkxYldJelNuUlpWelZxV2xNNWJHVklVbmxhVnpGc1lraHJkRnBIUm5WYU1sWjVZak5XZWt4WVFqRlpiWGh3V1hreGRtRlhVbW9LVEZkS2JGbFhUblppYVRsb1dUTlNjR0l5TlhwTU0wb3hZbTVOZGsxVWF6Vk9SRTEzVFhwQmVFOVVhM1paV0ZJd1dsY3hkMlJJVFhaTlZFRlhRbWR2Y2dwQ1owVkZRVmxQTDAxQlJWZENRV2ROUW01Q01WbHRlSEJaZWtOQ2FXZFpTMHQzV1VKQ1FVaFhaVkZKUlVGblVqaENTRzlCWlVGQ01rRkRjM2QyVG5odkNtbE5ibWswWkdkdFMxWTFNRWd3WnpWTldsbERPSEIzZW5reE5VUlJVRFo1Y2tsYU5rRkJRVUp0ZFhOU1NERjNRVUZCVVVSQlJXTjNVbEZKWjAxcWNrSUtNVEJGZDAxVVMwRlhiV3BVVFdGaU1rRllNVmxOWnpGc1YwcHdOM2hXTTNsb1FuZzNSM2xaUTBsUlF6VjZjalEwV25wVlUxQnVTak5IZEcwMmF6aE5NUXBEUW05SmVrUlpWM3AzUWtsdlVrRmthRFZPUlROVVFVdENaMmR4YUd0cVQxQlJVVVJCZDA1d1FVUkNiVUZxUlVGbmVXWm9VMUZ1Ynl0b00xVjFaMUF3Q2tFMlZqQkJPR0lySzNFeFNWWTBhRmx0TkdSV1ptazJjMnRhUzJ0RldVMUdZbk13YjJOTlYzRlhiRXBuT0ZCaFJrRnFSVUYwTnl0SVJHbFhUMlpoTTAwS2VFeFFia2xrZW1WRlVIbzBhMkpuV0ZweFMxRkVUVWhaTlcweFdrYzVOelppYmtwQ2QzUjBUVUZpUVVWWWFFVk1abmxJYkFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9XX19"}]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoic2hlbGYtcmVhZGVyLTAuMS50YXIuZ3oiLCJkaWdlc3QiOnsic2hhMjU2IjoiMDRjZmQ4YmI0Zjg0M2UzNWQ1MWJmZGVmMjAzNTEwOWJkZWE4MzFiNTVhNTdjM2U2YTE1NGQxNGJlMTE2Mzk4YyJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2RvY3MucHlwaS5vcmcvYXR0ZXN0YXRpb25zL3B1Ymxpc2gvdjEiLCJwcmVkaWNhdGUiOm51bGx9","signature":"MEYCIQChRPcu7l9ApHm016Q7nzhozqhy/auAWSbYg91JqC9nbQIhANtkEWMaWdGKGTduMen2CO5x8OcStQ+rk9gYQVx95aOU"}}
@@ -1 +0,0 @@
1
- {"version":1,"verification_material":{"certificate":"MIIIMzCCB7igAwIBAgIUKrdo9OfFJy6MuUewIeE/ILWw/tswCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUxMjA0MjAzMjM5WhcNMjUxMjA0MjA0MjM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEleubASHneG8xQmJ/HacfPp63RpBRb71lTUW3uqc/dxFttAkIu11LtR5Y1aRa7h/uFP6e9B/3WAV11xiu29vxRqOCBtcwggbTMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUcDHutpRx316lThqpRMpz2kw+/f4wHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwgaUGA1UdEQEB/wSBmjCBl4aBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBCg3OTU4NmMxZGVhZDhiODhmYmZhOWE3ODgyZGExMTZkZWNmNjc0YTQyMC0GCisGAQQBg78wAQQEH0V4dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24wSQYKKwYBBAGDvzABBQQ7c2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24wHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTCBpgYKKwYBBAGDvzABCQSBlwyBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABCgQqDCg3OTU4NmMxZGVhZDhiODhmYmZhOWE3ODgyZGExMTZkZWNmNjc0YTQyMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDBeBgorBgEEAYO/MAEMBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbjA4BgorBgEEAYO/MAENBCoMKDc5NTg2YzFkZWFkOGI4OGZiZmE5YTc4ODJkYTExNmRlY2Y2NzRhNDIwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21haW4wGQYKKwYBBAGDvzABDwQLDAk2MzI1OTY4OTcwNwYKKwYBBAGDvzABEAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UwGQYKKwYBBAGDvzABEQQLDAkxMzE4MDQ1NjMwgaYGCisGAQQBg78wARIEgZcMgZRodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24vLmdpdGh1Yi93b3JrZmxvd3MvZXh0cmVtZWx5LWRhbmdlcm91cy1vaWRjLWJlYWNvbi55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoNzk1ODZjMWRlYWQ4Yjg4ZmJmYTlhNzg4MmRhMTE2ZGVjZjY3NGE0MjAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMIGCBgorBgEEAYO/MAEVBHQMcmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi9hY3Rpb25zL3J1bnMvMTk5NDMwMzAxOTkvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABmusRH1wAAAQDAEcwRQIgMjrB10EwMTKAWmjTMab2AX1YMg1lWJp7xV3yhBx7GyYCIQC5zr44ZzUSPnJ3Gtm6k8M1CBoIzDYWzwBIoRAdh5NE3TAKBggqhkjOPQQDAwNpADBmAjEAgyfhSQno+h3UugP0A6V0A8b++q1IV4hYm4dVfi6skZKkEYMFbs0ocMWqWlJg8PaFAjEAt7+HDiWOfa3MxLPnIdzeEPz4kbgXZqKQDMHY5m1ZG976bnJBwttMAbAEXhELfyHl","transparency_entries":[{"logIndex":"51125015","logId":{"keyId":"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1764880359","inclusionPromise":{"signedEntryTimestamp":"MEUCIQCiibbwwQHoLxvH9C1/P8Lve6IUoBU/6Rcs1ShGqGoyqQIgBngTOdjZiPEaBMzkNTGRDkGKDAO8FVlClua3dB+VczU="},"inclusionProof":{"logIndex":"19442603","rootHash":"WveYvRUsyPpMgj5BTpatQytFwmxXNIAtg3r3T0Erwzc=","treeSize":"19442604","hashes":["u9GSDHFTk49/HwIcxCnZ46wrLt8BhbE4ghHA15/zujs=","4+9oEq/UTGK6dNePTKcKp7D/uIuzi/1MCP3/QyYOtWg=","F8+CGCoMNj9Jb1Csuk4bxbAWW35iktydoLKPPLk6Hb8=","P6IFMwnZlp99wiunjusnNW+K5V06iOEtzfoD9MtHCSU=","V4//f/ISXnSeJjUckmcLLzwS38XjXwMjrMAw6B3hKfg=","baLnSFGPpXZDhE38YKkGPW9P35RXQTaZlgDJ2i/u5xA=","YPKEUUZB3AVufkBQm5r7wLJeJMECETlxWoHo3+sKnHE=","VEcYk6UKGgyx1fCjyU6cfCPViC8OZFBjvHz8fco4eGM=","tf738kZ3YKyYiskKppTF5etATmFZgyY6H1ktyC0leCA=","gXQKVsGeO2E9oDbviE7+SuHFG5g7UH35dYGHc9JSz8s=","6kC2DcsZfB5F/TmenKHUzDr2UH6XK//Ch6hC8PZWJ5c=","JA9P8Q8v916uA/xomSrsiXeVBiyxxQAD5Mer9MBIYzA=","BrjYsYvo65dTDWKO8aY0s+9WaWHlVdZuOEzsBHLHe8U="],"checkpoint":{"envelope":"rekor.sigstage.dev - 8202293616175992157\n19442604\nWveYvRUsyPpMgj5BTpatQytFwmxXNIAtg3r3T0Erwzc=\n\n— rekor.sigstage.dev 0y8wozBEAiBLrKviMibkIK4/qPxYL29tvR5IlL8BTQKpjRpzoORIEAIgaXw4XKDTCbaHka5VBOwwH6PuT70TIC2vxO6ukgQrVqI=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiNjNhNzJhYjFkM2FhNWY5ZjlhMzUzMTQzNjE4NTBiZTRkMTg3NGI0OWZhODUwYmExYTY4OWM4MDIzY2UwYWFhZiJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImJjNWM2OTczMDRhYTU0MmE0YTZlMjkyYmExYzg4OWRhMWVhZWE0NmJkYTlhN2JlMmI4MmY1NjIzNTUzMTNlN2UifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVRQ0lEekoyZWZRUy9xSG5Bd1B4KzVnV2NOSlhHNmtIbndJMmpvMXlycmxzUlhNQWlBNDg2QzBoK3pGOEY3aUNJbjAyTGJzSFpwSGdHZnhDWFkraGU2QU1aUU5IQT09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VsTmVrTkRRamRwWjBGM1NVSkJaMGxWUzNKa2J6bFBaa1pLZVRaTmRWVmxkMGxsUlM5SlRGZDNMM1J6ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmVFMXFRVEJOYWtGNlRXcE5OVmRvWTA1TmFsVjRUV3BCTUUxcVFUQk5hazAxVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVnNaWFZpUVZOSWJtVkhPSGhSYlVvdlNHRmpabEJ3TmpOU2NFSlNZamN4YkZSVlZ6TUtkWEZqTDJSNFJuUjBRV3RKZFRFeFRIUlNOVmt4WVZKaE4yZ3ZkVVpRTm1VNVFpOHpWMEZXTVRGNGFYVXlPWFo0VW5GUFEwSjBZM2RuWjJKVVRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVmpSRWgxQ25Sd1VuZ3pNVFpzVkdoeGNGSk5jSG95YTNjckwyWTBkMGgzV1VSV1VqQnFRa0puZDBadlFWVmpXVmwzY0doU09GbHRMelU1T1dJd1FsSndMMWd2TDNJS1lqWjNkMmRoVlVkQk1WVmtSVkZGUWk5M1UwSnRha05DYkRSaFFteEhhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9YcGhWMlI2WkVjNWVRcGFVekZxWWpJMWJXSXpTblJaVnpWcVdsTTViR1ZJVW5sYVZ6RnNZa2hyZEZwSFJuVmFNbFo1WWpOV2VreFlRakZaYlhod1dYa3hkbUZYVW1wTVYwcHNDbGxYVG5aaWFUaDFXakpzTUdGSVZtbE1NMlIyWTIxMGJXSkhPVE5qZVRsc1pVaFNlVnBYTVd4aVNHdDBXa2RHZFZveVZubGlNMVo2VEZjNWNGcEhUWFFLV1cxV2FGa3lPWFZNYm14MFlrVkNlVnBYV25wTU1taHNXVmRTZWt3eU1XaGhWelIzVDFGWlMwdDNXVUpDUVVkRWRucEJRa0ZSVVhKaFNGSXdZMGhOTmdwTWVUa3dZakowYkdKcE5XaFpNMUp3WWpJMWVreHRaSEJrUjJneFdXNVdlbHBZU21waU1qVXdXbGMxTUV4dFRuWmlWRUZtUW1kdmNrSm5SVVZCV1U4dkNrMUJSVU5DUWtZellqTktjbHB0ZUhaa01UbHJZVmhPZDFsWVVtcGhSRUV5UW1kdmNrSm5SVVZCV1U4dlRVRkZSRUpEWnpOUFZGVTBUbTFOZUZwSFZtZ0tXa1JvYVU5RWFHMVpiVnBvVDFkRk0wOUVaM2xhUjBWNFRWUmFhMXBYVG0xT2FtTXdXVlJSZVUxRE1FZERhWE5IUVZGUlFtYzNPSGRCVVZGRlNEQldOQXBrU0Vwc1lsZFdjMlZUUW10WlZ6VnVXbGhLZG1SWVRXZFVNR3hGVVhsQ2FWcFhSbXBpTWpSM1UxRlpTMHQzV1VKQ1FVZEVkbnBCUWtKUlVUZGpNbXh1Q21NelVuWmpiVlYwV1RJNWRWcHRPWGxpVjBaMVdUSlZkbHBZYURCamJWWjBXbGQ0TlV4WFVtaGliV1JzWTIwNU1XTjVNWGRrVjBwellWZE5kR0l5YkdzS1dYa3hhVnBYUm1waU1qUjNTRkZaUzB0M1dVSkNRVWRFZG5wQlFrSm5VVkJqYlZadFkzazViMXBYUm10amVUbDBXVmRzZFUxRWMwZERhWE5IUVZGUlFncG5OemgzUVZGblJVeFJkM0poU0ZJd1kwaE5Oa3g1T1RCaU1uUnNZbWsxYUZrelVuQmlNalY2VEcxa2NHUkhhREZaYmxaNldsaEthbUl5TlRCYVZ6VXdDa3h0VG5aaVZFTkNjR2RaUzB0M1dVSkNRVWRFZG5wQlFrTlJVMEpzZDNsQ2JFZG9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWVtRlhaSG9LWkVjNWVWcFRNV3BpTWpWdFlqTktkRmxYTldwYVV6bHNaVWhTZVZwWE1XeGlTR3QwV2tkR2RWb3lWbmxpTTFaNlRGaENNVmx0ZUhCWmVURjJZVmRTYWdwTVYwcHNXVmRPZG1KcE9IVmFNbXd3WVVoV2FVd3paSFpqYlhSdFlrYzVNMk41T1d4bFNGSjVXbGN4YkdKSWEzUmFSMFoxV2pKV2VXSXpWbnBNVnpsd0NscEhUWFJaYlZab1dUSTVkVXh1YkhSaVJVSjVXbGRhZWt3eWFHeFpWMUo2VERJeGFHRlhOSGRQUVZsTFMzZFpRa0pCUjBSMmVrRkNRMmRSY1VSRFp6TUtUMVJWTkU1dFRYaGFSMVpvV2tSb2FVOUVhRzFaYlZwb1QxZEZNMDlFWjNsYVIwVjRUVlJhYTFwWFRtMU9hbU13V1ZSUmVVMUNNRWREYVhOSFFWRlJRZ3BuTnpoM1FWRnpSVVIzZDA1YU1td3dZVWhXYVV4WGFIWmpNMUpzV2tSQ1pVSm5iM0pDWjBWRlFWbFBMMDFCUlUxQ1JrRk5WRzFvTUdSSVFucFBhVGgyQ2xveWJEQmhTRlpwVEcxT2RtSlRPWHBoVjJSNlpFYzVlVnBUTVdwaU1qVnRZak5LZEZsWE5XcGFVemxzWlVoU2VWcFhNV3hpU0d0MFdrZEdkVm95Vm5rS1lqTldla3hZUWpGWmJYaHdXWGt4ZG1GWFVtcE1WMHBzV1ZkT2RtSnFRVFJDWjI5eVFtZEZSVUZaVHk5TlFVVk9Ra052VFV0RVl6Vk9WR2N5V1hwR2F3cGFWMFpyVDBkSk5FOUhXbWxhYlVVMVdWUmpORTlFU210WlZFVjRUbTFTYkZreVdUSk9lbEpvVGtSSmQwaDNXVXRMZDFsQ1FrRkhSSFo2UVVKRVoxRlNDa1JCT1hsYVYxcDZUREpvYkZsWFVucE1NakZvWVZjMGQwZFJXVXRMZDFsQ1FrRkhSSFo2UVVKRWQxRk1SRUZyTWsxNlNURlBWRmswVDFSamQwNTNXVXNLUzNkWlFrSkJSMFIyZWtGQ1JVRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkbU50VlhSWk1qbDFXbTA1ZVFwaVYwWjFXVEpWZDBkUldVdExkMWxDUWtGSFJIWjZRVUpGVVZGTVJFRnJlRTE2UlRSTlJGRXhUbXBOZDJkaFdVZERhWE5IUVZGUlFtYzNPSGRCVWtsRkNtZGFZMDFuV2xKdlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVblpqYlZWMFdUSTVkVnB0T1hsaVYwWjFXVEpWZGxwWWFEQUtZMjFXZEZwWGVEVk1WMUpvWW0xa2JHTnRPVEZqZVRGM1pGZEtjMkZYVFhSaU1teHJXWGt4YVZwWFJtcGlNalIyVEcxa2NHUkhhREZaYVRrellqTktjZ3BhYlhoMlpETk5kbHBZYURCamJWWjBXbGQ0TlV4WFVtaGliV1JzWTIwNU1XTjVNWFpoVjFKcVRGZEtiRmxYVG5aaWFUVTFZbGQ0UVdOdFZtMWplVGx2Q2xwWFJtdGplVGwwV1Zkc2RVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVrMUZTMmQzYjA1NmF6RlBSRnBxVFZkU2JGbFhVVFJaYW1jMFdtMUtiVmxVYkdnS1RucG5ORTF0VW1oTlZFVXlXa2RXYWxwcVdUTk9SMFV3VFdwQmFFSm5iM0pDWjBWRlFWbFBMMDFCUlZWQ1FrMU5SVmhrZG1OdGRHMWlSemt6V0RKU2NBcGpNMEpvWkVkT2IwMUpSME5DWjI5eVFtZEZSVUZaVHk5TlFVVldRa2hSVFdOdGFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1hwaFYyUjZDbVJIT1hsYVV6RnFZakkxYldJelNuUlpWelZxV2xNNWJHVklVbmxhVnpGc1lraHJkRnBIUm5WYU1sWjVZak5XZWt4WVFqRlpiWGh3V1hreGRtRlhVbW9LVEZkS2JGbFhUblppYVRsb1dUTlNjR0l5TlhwTU0wb3hZbTVOZGsxVWF6Vk9SRTEzVFhwQmVFOVVhM1paV0ZJd1dsY3hkMlJJVFhaTlZFRlhRbWR2Y2dwQ1owVkZRVmxQTDAxQlJWZENRV2ROUW01Q01WbHRlSEJaZWtOQ2FXZFpTMHQzV1VKQ1FVaFhaVkZKUlVGblVqaENTRzlCWlVGQ01rRkRjM2QyVG5odkNtbE5ibWswWkdkdFMxWTFNRWd3WnpWTldsbERPSEIzZW5reE5VUlJVRFo1Y2tsYU5rRkJRVUp0ZFhOU1NERjNRVUZCVVVSQlJXTjNVbEZKWjAxcWNrSUtNVEJGZDAxVVMwRlhiV3BVVFdGaU1rRllNVmxOWnpGc1YwcHdOM2hXTTNsb1FuZzNSM2xaUTBsUlF6VjZjalEwV25wVlUxQnVTak5IZEcwMmF6aE5NUXBEUW05SmVrUlpWM3AzUWtsdlVrRmthRFZPUlROVVFVdENaMmR4YUd0cVQxQlJVVVJCZDA1d1FVUkNiVUZxUlVGbmVXWm9VMUZ1Ynl0b00xVjFaMUF3Q2tFMlZqQkJPR0lySzNFeFNWWTBhRmx0TkdSV1ptazJjMnRhUzJ0RldVMUdZbk13YjJOTlYzRlhiRXBuT0ZCaFJrRnFSVUYwTnl0SVJHbFhUMlpoTTAwS2VFeFFia2xrZW1WRlVIbzBhMkpuV0ZweFMxRkVUVWhaTlcweFdrYzVOelppYmtwQ2QzUjBUVUZpUVVWWWFFVk1abmxJYkFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9XX19"}]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoic2hlbGZfcmVhZGVyLTAuMS1weTItbm9uZS1hbnkud2hsIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjJlY2ViMTY0M2MxMGM1ZTRhNjU5NzBiYWY2M2JkZTQzYjc5Y2JkYWM3ZGU4MWRhZTg1M2NlNDdhYjA1MTk3ZTkifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9kb2NzLnB5cGkub3JnL2F0dGVzdGF0aW9ucy9wdWJsaXNoL3YxIiwicHJlZGljYXRlIjpudWxsfQ==","signature":"MEQCIDzJ2efQS/qHnAwPx+5gWcNJXG6kHnwI2jo1yrrlsRXMAiA486C0h+zF8F7iCIn02LbsHZpHgGfxCXY+he6AMZQNHA=="}}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes