devpi-server 6.19.3__tar.gz → 6.20.0__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 (140) hide show
  1. {devpi_server-6.19.3 → devpi_server-6.20.0}/CHANGELOG +14 -0
  2. {devpi_server-6.19.3 → devpi_server-6.20.0}/CHANGELOG.short.rst +14 -31
  3. {devpi_server-6.19.3 → devpi_server-6.20.0}/PKG-INFO +15 -32
  4. devpi_server-6.20.0/devpi_server/__init__.py +1 -0
  5. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/config.py +6 -0
  6. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/filestore.py +1 -1
  7. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/keyfs.py +4 -1
  8. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/keyfs_sqlite.py +6 -0
  9. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/mirror.py +38 -9
  10. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/model.py +24 -5
  11. devpi_server-6.20.0/devpi_server/proxy.py +46 -0
  12. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/replica.py +4 -37
  13. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/views.py +86 -13
  14. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server.egg-info/PKG-INFO +15 -32
  15. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server.egg-info/SOURCES.txt +1 -0
  16. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/plugin.py +19 -9
  17. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_keyfs.py +17 -0
  18. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_mirror.py +34 -0
  19. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_views.py +35 -0
  20. {devpi_server-6.19.3 → devpi_server-6.20.0}/tox.ini +1 -1
  21. devpi_server-6.19.3/devpi_server/__init__.py +0 -1
  22. {devpi_server-6.19.3 → devpi_server-6.20.0}/.flake8 +0 -0
  23. {devpi_server-6.19.3 → devpi_server-6.20.0}/LICENSE +0 -0
  24. {devpi_server-6.19.3 → devpi_server-6.20.0}/MANIFEST.in +0 -0
  25. {devpi_server-6.19.3 → devpi_server-6.20.0}/README.rst +0 -0
  26. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/__main__.py +0 -0
  27. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/auth.py +0 -0
  28. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/auth_basic.py +0 -0
  29. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/auth_devpi.py +0 -0
  30. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/__init__.py +0 -0
  31. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/crontab.template +0 -0
  32. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/devpi.service.template +0 -0
  33. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/launchd-macos.txt.template +0 -0
  34. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi-caching-http.conf.template +0 -0
  35. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi-caching-location.conf.template +0 -0
  36. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi-caching-proxy.conf.template +0 -0
  37. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi-caching-server.conf.template +0 -0
  38. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi.conf.template +0 -0
  39. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/supervisor-devpi.conf.template +0 -0
  40. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/supervisord.conf.template +0 -0
  41. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/cfg/windows-service.txt.template +0 -0
  42. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/compat.py +0 -0
  43. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/exceptions.py +0 -0
  44. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/filestore_db.py +0 -0
  45. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/filestore_fs.py +0 -0
  46. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/filestore_fs_base.py +0 -0
  47. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/filestore_hash_hl.py +0 -0
  48. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/fileutil.py +0 -0
  49. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/fsck.py +0 -0
  50. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/genconfig.py +0 -0
  51. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/hookspecs.py +0 -0
  52. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/htmlpage.py +0 -0
  53. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/httpclient.py +0 -0
  54. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/importexport.py +0 -0
  55. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/init.py +0 -0
  56. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/interfaces.py +0 -0
  57. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/keyfs_sqlite_fs.py +0 -0
  58. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/keyfs_types.py +0 -0
  59. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/log.py +0 -0
  60. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/main.py +0 -0
  61. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/markers.py +0 -0
  62. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/middleware.py +0 -0
  63. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/mythread.py +0 -0
  64. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/normalized.py +0 -0
  65. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/passwd.py +0 -0
  66. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/py.typed +0 -0
  67. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/readonly.py +0 -0
  68. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/sizeof.py +0 -0
  69. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server/view_auth.py +0 -0
  70. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server.egg-info/dependency_links.txt +0 -0
  71. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server.egg-info/entry_points.txt +0 -0
  72. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server.egg-info/requires.txt +0 -0
  73. {devpi_server-6.19.3 → devpi_server-6.20.0}/devpi_server.egg-info/top_level.txt +0 -0
  74. {devpi_server-6.19.3 → devpi_server-6.20.0}/mypy.ini +0 -0
  75. {devpi_server-6.19.3 → devpi_server-6.20.0}/pyproject.toml +0 -0
  76. {devpi_server-6.19.3 → devpi_server-6.20.0}/pytest_devpi_server/__init__.py +0 -0
  77. {devpi_server-6.19.3 → devpi_server-6.20.0}/setup.cfg +0 -0
  78. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/__init__.py +0 -0
  79. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/conftest.py +0 -0
  80. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/example.py +0 -0
  81. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/functional.py +0 -0
  82. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/badindexname/dataindex.json +0 -0
  83. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/badusername/dataindex.json +0 -0
  84. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/basescycle/dataindex.json +0 -0
  85. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/createdmodified/dataindex.json +0 -0
  86. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/dashes_v1/dataindex.json +0 -0
  87. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/dashes_v1/user1/dev/hello/hello-1.2_3.tar.gz +0 -0
  88. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/deletedbase/dataindex.json +0 -0
  89. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/mirrordata/dataindex.json +0 -0
  90. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/mirrordata/root/pypi/dddttt/0.1.dev1/dddttt-0.1.dev1.tar.gz +0 -0
  91. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/modifiedpypi/dataindex.json +0 -0
  92. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/no_history_log/dataindex.json +0 -0
  93. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/no_history_log/user1/dev/hello/hello-1.0.tar.gz +0 -0
  94. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/nocreatedmodified/dataindex.json +0 -0
  95. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization/dataindex.json +0 -0
  96. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  97. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization_merge/dataindex.json +0 -0
  98. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello-pkg/hello.pkg-1.1.tar.gz +0 -0
  99. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  100. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/norootpypi/dataindex.json +0 -0
  101. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/nouser/dataindex.json +0 -0
  102. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/removedindexplugin/dataindex.json +0 -0
  103. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/removedindexplugin/user/dev/pkg/pkg-1.0.tar.gz +0 -0
  104. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/dataindex.json +0 -0
  105. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/0.9/hello-0.9.tar.gz +0 -0
  106. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/sha256=ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73/hello-0.9.tar.gz.toxresult0 +0 -0
  107. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/importexportdata/toxresult_upload_default/dataindex.json +0 -0
  108. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/py.typed +0 -0
  109. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/reqmock.py +0 -0
  110. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/simpypi.py +0 -0
  111. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_auth.py +0 -0
  112. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_authcheck.py +0 -0
  113. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_config.py +0 -0
  114. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_conftest.py +0 -0
  115. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_filestore.py +0 -0
  116. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_filestore_fs.py +0 -0
  117. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_fileutil.py +0 -0
  118. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_fsck.py +0 -0
  119. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_genconfig.py +0 -0
  120. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_importexport.py +0 -0
  121. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_log.py +0 -0
  122. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_main.py +0 -0
  123. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_mirror_no_project_list.py +0 -0
  124. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_model.py +0 -0
  125. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_mythread.py +0 -0
  126. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_nginx.py +0 -0
  127. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_nginx_replica.py +0 -0
  128. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_permissions.py +0 -0
  129. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_readonly.py +0 -0
  130. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_replica.py +0 -0
  131. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_replica_functional.py +0 -0
  132. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_stage_customizer.py +0 -0
  133. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_streaming.py +0 -0
  134. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_streaming_nginx.py +0 -0
  135. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_streaming_replica.py +0 -0
  136. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_streaming_replica_nginx.py +0 -0
  137. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_view_auth.py +0 -0
  138. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_views_patch.py +0 -0
  139. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_views_push_external.py +0 -0
  140. {devpi_server-6.19.3 → devpi_server-6.20.0}/test_devpi_server/test_views_status.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  .. towncrier release notes start
4
4
 
5
+ 6.20.0 (2026-04-30)
6
+ ===================
7
+
8
+ Features
9
+ --------
10
+
11
+ - Add experimental bare bones core-metadata ([PEP 658](https://peps.python.org/pep-0658/), [PEP 714](https://peps.python.org/pep-0714/)) support with ``--enable-core-metadata`` command line option and ``mirror_provides_core_metadata`` mirror index option. Refs #1018
12
+
13
+ Bug Fixes
14
+ ---------
15
+
16
+ - Update replica status when the replica is waiting for new serials using the streaming changelog endpoint.
17
+
18
+
5
19
  6.19.3 (2026-04-13)
6
20
  ===================
7
21
 
@@ -9,6 +9,20 @@ Changelog
9
9
 
10
10
  .. towncrier release notes start
11
11
 
12
+ 6.20.0 (2026-04-30)
13
+ ===================
14
+
15
+ Features
16
+ --------
17
+
18
+ - Add experimental bare bones core-metadata ([PEP 658](https://peps.python.org/pep-0658/), [PEP 714](https://peps.python.org/pep-0714/)) support with ``--enable-core-metadata`` command line option and ``mirror_provides_core_metadata`` mirror index option. Refs #1018
19
+
20
+ Bug Fixes
21
+ ---------
22
+
23
+ - Update replica status when the replica is waiting for new serials using the streaming changelog endpoint.
24
+
25
+
12
26
  6.19.3 (2026-04-13)
13
27
  ===================
14
28
 
@@ -81,34 +95,3 @@ Bug Fixes
81
95
 
82
96
  - Fix #1110: a list for the ``listen`` option in a config file stopped working in 6.18.0.
83
97
 
84
-
85
- 6.18.0 (2026-01-27)
86
- ===================
87
-
88
- Features
89
- --------
90
-
91
- - Store all available hashes of files.
92
-
93
- - Validate hashes of all files during devpi-import, not only releases.
94
-
95
- Bug Fixes
96
- ---------
97
-
98
- - Apply argparse transformations on values read from config file or environment.
99
-
100
- - Restore Python and platform info in user agent string after switch to httpx.
101
-
102
- - Remove all database entries on project deletion instead of only emptying them.
103
-
104
- - Fix error at end of replica streaming caused by changed behavior from switch to httpx.
105
-
106
- - Fix #1102: The data stream was cut off after 64k when proxying from replica to primary after switching to httpx.
107
-
108
- - Fix #1107: retry file downloads if there has been an error during download.
109
-
110
- Other Changes
111
- -------------
112
-
113
- - The filenames of some exported doczip files change due to normalization of the project name caused by changing the internals during export to allow ``--hard-links`` to work.
114
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devpi-server
3
- Version: 6.19.3
3
+ Version: 6.20.0
4
4
  Summary: devpi-server: backend for hosting private package indexes and PyPI on-demand mirrors
5
5
  Maintainer-email: Florian Schulze <mail@pyfidelity.com>
6
6
  License-Expression: MIT
@@ -121,6 +121,20 @@ Changelog
121
121
 
122
122
  .. towncrier release notes start
123
123
 
124
+ 6.20.0 (2026-04-30)
125
+ ===================
126
+
127
+ Features
128
+ --------
129
+
130
+ - Add experimental bare bones core-metadata ([PEP 658](https://peps.python.org/pep-0658/), [PEP 714](https://peps.python.org/pep-0714/)) support with ``--enable-core-metadata`` command line option and ``mirror_provides_core_metadata`` mirror index option. Refs #1018
131
+
132
+ Bug Fixes
133
+ ---------
134
+
135
+ - Update replica status when the replica is waiting for new serials using the streaming changelog endpoint.
136
+
137
+
124
138
  6.19.3 (2026-04-13)
125
139
  ===================
126
140
 
@@ -193,34 +207,3 @@ Bug Fixes
193
207
 
194
208
  - Fix #1110: a list for the ``listen`` option in a config file stopped working in 6.18.0.
195
209
 
196
-
197
- 6.18.0 (2026-01-27)
198
- ===================
199
-
200
- Features
201
- --------
202
-
203
- - Store all available hashes of files.
204
-
205
- - Validate hashes of all files during devpi-import, not only releases.
206
-
207
- Bug Fixes
208
- ---------
209
-
210
- - Apply argparse transformations on values read from config file or environment.
211
-
212
- - Restore Python and platform info in user agent string after switch to httpx.
213
-
214
- - Remove all database entries on project deletion instead of only emptying them.
215
-
216
- - Fix error at end of replica streaming caused by changed behavior from switch to httpx.
217
-
218
- - Fix #1102: The data stream was cut off after 64k when proxying from replica to primary after switching to httpx.
219
-
220
- - Fix #1107: retry file downloads if there has been an error during download.
221
-
222
- Other Changes
223
- -------------
224
-
225
- - The filenames of some exported doczip files change due to normalization of the project name caused by changing the internals during export to allow ``--hard-links`` to work.
226
-
@@ -0,0 +1 @@
1
+ __version__ = "6.20.0"
@@ -245,6 +245,12 @@ def add_web_options(
245
245
  help="use absolute URLs everywhere. "
246
246
  "This will become the default at some point.")
247
247
 
248
+ parser.addoption(
249
+ "--enable-core-metadata",
250
+ action="store_true",
251
+ help="(experimental) Enable minimal core-metadata support in simple API.",
252
+ )
253
+
248
254
  parser.addoption(
249
255
  "--profile-requests", type=int, metavar="NUM", default=0,
250
256
  help="profile NUM requests and print out cumulative stats. "
@@ -700,7 +700,7 @@ class BaseFileEntry:
700
700
  headers = {}
701
701
  headers["last-modified"] = str(self.last_modified)
702
702
  m = mimetypes.guess_type(self.basename)[0]
703
- headers["content-type"] = str(m)
703
+ headers["content-type"] = "application/octet-stream" if m is None else m
704
704
  headers["content-length"] = str(self.file_size())
705
705
  headers["cache-control"] = "max-age=365000000, immutable, public"
706
706
  return headers
@@ -386,7 +386,10 @@ class KeyFS:
386
386
  relpaths: Iterable[RelPath],
387
387
  ) -> Iterable[FilePathInfo]:
388
388
  for relpath in relpaths:
389
- (_, _, val) = conn.get_relpath_at(relpath, serial)
389
+ (_, back_serial, val) = conn.get_relpath_at(relpath, serial)
390
+ if val is None:
391
+ # the file was deleted, get the data from before
392
+ (_, _, val) = conn.get_relpath_at(relpath, back_serial)
390
393
  if (
391
394
  isinstance(val, (dict, DictViewReadonly))
392
395
  and "hash_spec" in val
@@ -464,6 +464,7 @@ class BaseStorage(object):
464
464
  self._execute_conn_pragmas(sqlconn)
465
465
  if write:
466
466
  start_time = time.monotonic()
467
+ log_delay = 2
467
468
  thread = current_thread()
468
469
  while 1:
469
470
  try:
@@ -475,6 +476,11 @@ class BaseStorage(object):
475
476
  if hasattr(thread, "exit_if_shutdown"):
476
477
  thread.exit_if_shutdown()
477
478
  elapsed = time.monotonic() - start_time
479
+ if elapsed >= log_delay:
480
+ threadlog.warn(
481
+ "Waiting on database connection for %s seconds", log_delay
482
+ )
483
+ log_delay = log_delay * 1.5
478
484
  if elapsed > timeout:
479
485
  # if it takes this long, something is wrong
480
486
  raise KeyfsTimeoutError(
@@ -485,6 +485,10 @@ class MirrorStage(BaseStage):
485
485
  return f"Basic {b64encode(auth).decode()}"
486
486
  return None
487
487
 
488
+ @property
489
+ def provides_core_metadata(self) -> bool:
490
+ return self.ixconfig.get("mirror_provides_core_metadata", False)
491
+
488
492
  @property
489
493
  def no_project_list(self) -> bool:
490
494
  return self.ixconfig.get('mirror_no_project_list', False)
@@ -501,6 +505,7 @@ class MirrorStage(BaseStage):
501
505
  "mirror_cache_expiry",
502
506
  "mirror_ignore_serial_header",
503
507
  "mirror_no_project_list",
508
+ "mirror_provides_core_metadata",
504
509
  "mirror_url",
505
510
  "mirror_use_external_urls",
506
511
  "mirror_web_url_fmt",
@@ -529,6 +534,8 @@ class MirrorStage(BaseStage):
529
534
  return ensure_boolean(value)
530
535
  if key == "mirror_no_project_list":
531
536
  return ensure_boolean(value)
537
+ if key == "mirror_provides_core_metadata":
538
+ return ensure_boolean(value)
532
539
  if key == "mirror_use_external_urls":
533
540
  return ensure_boolean(value)
534
541
  if key in ("custom_data", "description", "mirror_web_url_fmt", "title"):
@@ -747,6 +754,9 @@ class MirrorStage(BaseStage):
747
754
  return (self.cache_projectnames.get(), False)
748
755
  lock = self._list_projects_perstage_lock
749
756
  projects_timeout = self.get_projects_timeout(timeout)
757
+ threadlog.debug(
758
+ "Acquiring projects list lock (%r) with timeout %s", lock, timeout
759
+ )
750
760
  if lock.acquire(timeout=projects_timeout):
751
761
  try:
752
762
  # retry in case it was updated in another thread
@@ -756,6 +766,7 @@ class MirrorStage(BaseStage):
756
766
  return self._update_projects(timeout=timeout)
757
767
  finally:
758
768
  lock.release()
769
+ threadlog.debug("Released projects list lock (%r)", lock)
759
770
  return (self._stale_list_projects_perstage(), True)
760
771
 
761
772
  def list_projects_perstage(self) -> dict[str, NormalizedName | str]:
@@ -968,7 +979,9 @@ class MirrorStage(BaseStage):
968
979
  self.keyfs.tx.on_commit_success(
969
980
  partial(self.cache_retrieve_times.refresh, project, info.etag)
970
981
  )
971
- return self.SimpleLinks(links)
982
+ return self.SimpleLinks(
983
+ links, core_metadata=self.provides_core_metadata
984
+ )
972
985
  raise self.UpstreamError("no cache links from primary for %s" %
973
986
  project)
974
987
 
@@ -992,7 +1005,7 @@ class MirrorStage(BaseStage):
992
1005
  info.serial,
993
1006
  info.etag,
994
1007
  )
995
- return self.SimpleLinks(newlinks)
1008
+ return self.SimpleLinks(newlinks, core_metadata=self.provides_core_metadata)
996
1009
 
997
1010
  async def _update_simplelinks_in_future(
998
1011
  self,
@@ -1038,7 +1051,9 @@ class MirrorStage(BaseStage):
1038
1051
  threadlog.warn(
1039
1052
  "serving stale links for %r, waiting for existing request timed out after %s seconds",
1040
1053
  project, self.timeout)
1041
- return self.SimpleLinks(links, stale=True)
1054
+ return self.SimpleLinks(
1055
+ links, core_metadata=self.provides_core_metadata, stale=True
1056
+ )
1042
1057
  raise self.UpstreamError(
1043
1058
  f"timeout after {self.timeout} seconds while getting data for {project!r}")
1044
1059
 
@@ -1049,7 +1064,9 @@ class MirrorStage(BaseStage):
1049
1064
  threadlog.debug(
1050
1065
  "using stale links for %r due to offline mode", project)
1051
1066
  self._offline_logging.add(project)
1052
- return self.SimpleLinks(links, stale=True)
1067
+ return self.SimpleLinks(
1068
+ links, core_metadata=self.provides_core_metadata, stale=True
1069
+ )
1053
1070
 
1054
1071
  if links is None:
1055
1072
  is_retrieval_expired = self.cache_retrieve_times.is_expired(
@@ -1090,14 +1107,18 @@ class MirrorStage(BaseStage):
1090
1107
  threadlog.warn(
1091
1108
  "serving stale links for %r, getting data timed out after %s seconds",
1092
1109
  project, self.timeout)
1093
- return self.SimpleLinks(links, stale=True)
1110
+ return self.SimpleLinks(
1111
+ links, core_metadata=self.provides_core_metadata, stale=True
1112
+ )
1094
1113
  raise self.UpstreamError(
1095
1114
  f"timeout after {self.timeout} seconds while getting data for {project!r}")
1096
1115
  except self.UpstreamNotModified as e:
1097
1116
  if links is not None:
1098
1117
  # immediately update the cache
1099
1118
  self.cache_retrieve_times.refresh(project, e.etag)
1100
- return self.SimpleLinks(links)
1119
+ return self.SimpleLinks(
1120
+ links, core_metadata=self.provides_core_metadata
1121
+ )
1101
1122
  if e.etag is None:
1102
1123
  threadlog.error(
1103
1124
  "server returned 304 Not Modified, but we have no links")
@@ -1113,7 +1134,9 @@ class MirrorStage(BaseStage):
1113
1134
  threadlog.warn(
1114
1135
  "serving stale links, because of exception %s",
1115
1136
  lazy_format_exception(e))
1116
- return self.SimpleLinks(links, stale=True)
1137
+ return self.SimpleLinks(
1138
+ links, core_metadata=self.provides_core_metadata, stale=True
1139
+ )
1117
1140
  raise
1118
1141
 
1119
1142
  info = newlinks_future.result()
@@ -1122,7 +1145,7 @@ class MirrorStage(BaseStage):
1122
1145
  if links is not None and set(links) == set(newlinks):
1123
1146
  # no changes
1124
1147
  self.cache_retrieve_times.refresh(project, info.etag)
1125
- return self.SimpleLinks(links)
1148
+ return self.SimpleLinks(links, core_metadata=self.provides_core_metadata)
1126
1149
 
1127
1150
  return self._update_simplelinks(project, info, links, newlinks)
1128
1151
 
@@ -1300,7 +1323,12 @@ class ProjectUpdateLock:
1300
1323
  self.project = project
1301
1324
 
1302
1325
  def acquire(self, timeout: float) -> bool:
1303
- threadlog.debug("Acquiring lock (%r) for %r", self.lock, self.project)
1326
+ threadlog.debug(
1327
+ "Acquiring lock (%r) for %r with timeout %s",
1328
+ self.lock,
1329
+ self.project,
1330
+ timeout,
1331
+ )
1304
1332
  assert self.lock is not None
1305
1333
  return self.lock.acquire(timeout=timeout)
1306
1334
 
@@ -1317,6 +1345,7 @@ class ProjectUpdateLock:
1317
1345
  def release(self) -> None:
1318
1346
  if self.lock is not None:
1319
1347
  self.lock.release()
1348
+ threadlog.debug("Released lock (%r) for %r", self.lock, self.project)
1320
1349
  self.lock = None
1321
1350
 
1322
1351
  def __repr__(self) -> str:
@@ -675,14 +675,20 @@ class SimpleLinks:
675
675
  stale: bool
676
676
 
677
677
  def __init__(
678
- self, links: Sequence[JoinedLink] | SimpleLinks, *, stale: bool = False
678
+ self,
679
+ links: Sequence[JoinedLink] | SimpleLinks,
680
+ *,
681
+ core_metadata: bool = False,
682
+ stale: bool = False,
679
683
  ) -> None:
680
684
  assert links is not None
681
685
  if isinstance(links, SimpleLinks):
682
686
  self._links = links._links
683
687
  self.stale = links.stale or stale
684
688
  else:
685
- self._links = [SimplelinkMeta(x) for x in links]
689
+ self._links = [
690
+ SimplelinkMeta(x, core_metadata=core_metadata) for x in links
691
+ ]
686
692
  self.stale = stale
687
693
 
688
694
  def __hash__(self):
@@ -1596,7 +1602,8 @@ class PrivateStage(BaseStage):
1596
1602
  requires_python = cast("RequiresPythonList", data.get("requires_python", []))
1597
1603
  yanked: YankedList = [] # PEP 592 isn't supported for private stages yet
1598
1604
  return self.SimpleLinks(
1599
- join_links_data(links, requires_python, yanked))
1605
+ join_links_data(links, requires_python, yanked), core_metadata=True
1606
+ )
1600
1607
 
1601
1608
  def _regen_simplelinks(self, project_input):
1602
1609
  project = normalize_name(project_input)
@@ -2129,6 +2136,7 @@ class SimplelinkMeta:
2129
2136
  "__path",
2130
2137
  "__url",
2131
2138
  "__version",
2139
+ "core_metadata",
2132
2140
  "href",
2133
2141
  "key",
2134
2142
  "require_python",
@@ -2137,7 +2145,12 @@ class SimplelinkMeta:
2137
2145
  __cmpval: tuple | NotSet
2138
2146
  __hashes: Digests | NotSet
2139
2147
 
2140
- def __init__(self, link_info: tuple[str, str, RequiresPython, Yanked]) -> None:
2148
+ def __init__(
2149
+ self,
2150
+ link_info: tuple[str, str, RequiresPython, Yanked],
2151
+ *,
2152
+ core_metadata: bool = False,
2153
+ ) -> None:
2141
2154
  self.__basename = notset
2142
2155
  self.__cmpval = notset
2143
2156
  self.__ext = notset
@@ -2147,7 +2160,10 @@ class SimplelinkMeta:
2147
2160
  self.__path = notset
2148
2161
  self.__url = notset
2149
2162
  self.__version = notset
2163
+ self.core_metadata = False
2150
2164
  (self.key, self.href, self.require_python, self.yanked) = link_info
2165
+ if core_metadata and self.basename.endswith(".whl"):
2166
+ self.core_metadata = True
2151
2167
 
2152
2168
  def __hash__(self) -> int:
2153
2169
  return hash(
@@ -2161,6 +2177,7 @@ class SimplelinkMeta:
2161
2177
  self.__path,
2162
2178
  self.__url,
2163
2179
  self.__version,
2180
+ self.core_metadata,
2164
2181
  self.href,
2165
2182
  self.key,
2166
2183
  self.require_python,
@@ -2283,8 +2300,10 @@ class SimplelinkMeta:
2283
2300
  f"<{clsname} "
2284
2301
  f"key={self.key!r} "
2285
2302
  f"href={self.href!r} "
2303
+ f"core_metadata={self.core_metadata!r} "
2286
2304
  f"require_python={self.require_python!r} "
2287
- f"yanked={self.yanked!r}>")
2305
+ f"yanked={self.yanked!r}>"
2306
+ )
2288
2307
 
2289
2308
 
2290
2309
  def make_key_and_href(entry):
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ from webob.headers import EnvironHeaders
5
+ from webob.headers import ResponseHeaders
6
+
7
+
8
+ if TYPE_CHECKING:
9
+ from httpx import Response
10
+ from pyramid.request import Request
11
+
12
+
13
+ hop_by_hop = frozenset(
14
+ {
15
+ "connection",
16
+ "keep-alive",
17
+ "proxy-authenticate",
18
+ "proxy-authorization",
19
+ "te",
20
+ "trailers",
21
+ "transfer-encoding",
22
+ "upgrade",
23
+ }
24
+ )
25
+
26
+
27
+ def clean_request_headers(request: Request) -> EnvironHeaders:
28
+ result = EnvironHeaders({})
29
+ result.update(request.headers)
30
+ result.pop("host", None)
31
+ return result
32
+
33
+
34
+ def clean_response_headers(response: Response) -> ResponseHeaders:
35
+ headers = ResponseHeaders()
36
+ # remove hop by hop headers, see:
37
+ # https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
38
+ hop_keys = set(hop_by_hop)
39
+ connection = response.headers.get("connection")
40
+ if connection and connection.lower() != "close":
41
+ hop_keys.update(x.strip().lower() for x in connection.split(","))
42
+ for k, v in response.headers.items():
43
+ if k.lower() in hop_keys:
44
+ continue
45
+ headers[k] = v
46
+ return headers
@@ -19,6 +19,8 @@ from .markers import Absent
19
19
  from .markers import absent
20
20
  from .model import UpstreamError
21
21
  from .normalized import normalize_name
22
+ from .proxy import clean_request_headers
23
+ from .proxy import clean_response_headers
22
24
  from .views import FileStreamer
23
25
  from .views import H_MASTER_UUID
24
26
  from .views import H_PRIMARY_UUID
@@ -33,8 +35,6 @@ from pyramid.response import Response
33
35
  from pyramid.view import view_config
34
36
  from repoze.lru import LRUCache
35
37
  from typing import TYPE_CHECKING
36
- from webob.headers import EnvironHeaders
37
- from webob.headers import ResponseHeaders
38
38
  import contextlib
39
39
  import io
40
40
  import itsdangerous
@@ -330,7 +330,8 @@ class PrimaryChangelogRequest:
330
330
  start_serial = int(self.request.matchdict["serial"])
331
331
 
332
332
  keyfs = self.xom.keyfs
333
- self._wait_for_serial(start_serial)
333
+ with self.update_replica_status(start_serial):
334
+ self._wait_for_serial(start_serial)
334
335
  devpi_serial = keyfs.tx.conn.last_changelog_serial
335
336
  threadlog.info("Streaming from %s to %s", start_serial, devpi_serial)
336
337
 
@@ -1441,40 +1442,6 @@ class SimpleLinksChanged:
1441
1442
  cache_projectnames.add(project)
1442
1443
 
1443
1444
 
1444
- hop_by_hop = frozenset((
1445
- 'connection',
1446
- 'keep-alive',
1447
- 'proxy-authenticate',
1448
- 'proxy-authorization',
1449
- 'te',
1450
- 'trailers',
1451
- 'transfer-encoding',
1452
- 'upgrade'
1453
- ))
1454
-
1455
-
1456
- def clean_request_headers(request):
1457
- result = EnvironHeaders({})
1458
- result.update(request.headers)
1459
- result.pop('host', None)
1460
- return result
1461
-
1462
-
1463
- def clean_response_headers(response):
1464
- headers = ResponseHeaders()
1465
- # remove hop by hop headers, see:
1466
- # https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
1467
- hop_keys = set(hop_by_hop)
1468
- connection = response.headers.get('connection')
1469
- if connection and connection.lower() != 'close':
1470
- hop_keys.update(x.strip().lower() for x in connection.split(','))
1471
- for k, v in response.headers.items():
1472
- if k.lower() in hop_keys:
1473
- continue
1474
- headers[k] = v
1475
- return headers
1476
-
1477
-
1478
1445
  class BodyFileWrapper:
1479
1446
  # required to provide length to prevent transfer-encoding: chunked
1480
1447