devpi-server 6.11.0__tar.gz → 6.12.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 (131) hide show
  1. {devpi_server-6.11.0 → devpi-server-6.12.1}/CHANGELOG +33 -0
  2. {devpi_server-6.11.0 → devpi-server-6.12.1}/PKG-INFO +34 -52
  3. devpi-server-6.12.1/devpi_server/__init__.py +1 -0
  4. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/config.py +4 -16
  5. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/hookspecs.py +31 -1
  6. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/keyfs.py +3 -7
  7. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/mirror.py +2 -2
  8. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/model.py +6 -7
  9. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/replica.py +15 -8
  10. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/vendor/_pip.py +6 -6
  11. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/views.py +58 -23
  12. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server.egg-info/PKG-INFO +34 -52
  13. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server.egg-info/SOURCES.txt +0 -1
  14. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server.egg-info/requires.txt +4 -1
  15. {devpi_server-6.11.0 → devpi-server-6.12.1}/setup.py +4 -3
  16. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/functional.py +35 -0
  17. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/plugin.py +23 -41
  18. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_fileutil.py +2 -2
  19. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_keyfs.py +44 -4
  20. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_mirror.py +6 -0
  21. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_nginx.py +2 -2
  22. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_views.py +59 -1
  23. devpi_server-6.11.0/CHANGELOG.short.rst +0 -95
  24. devpi_server-6.11.0/devpi_server/__init__.py +0 -1
  25. {devpi_server-6.11.0 → devpi-server-6.12.1}/.flake8 +0 -0
  26. {devpi_server-6.11.0 → devpi-server-6.12.1}/AUTHORS +0 -0
  27. {devpi_server-6.11.0 → devpi-server-6.12.1}/LICENSE +0 -0
  28. {devpi_server-6.11.0 → devpi-server-6.12.1}/MANIFEST.in +0 -0
  29. {devpi_server-6.11.0 → devpi-server-6.12.1}/README.rst +0 -0
  30. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/__main__.py +0 -0
  31. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/auth.py +0 -0
  32. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/auth_basic.py +0 -0
  33. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/auth_devpi.py +0 -0
  34. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/__init__.py +0 -0
  35. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/crontab.template +0 -0
  36. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/devpi.service.template +0 -0
  37. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/launchd-macos.txt.template +0 -0
  38. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi-caching-http.conf.template +0 -0
  39. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi-caching-location.conf.template +0 -0
  40. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi-caching-proxy.conf.template +0 -0
  41. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi-caching-server.conf.template +0 -0
  42. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi.conf.template +0 -0
  43. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/supervisor-devpi.conf.template +0 -0
  44. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/supervisord.conf.template +0 -0
  45. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/cfg/windows-service.txt.template +0 -0
  46. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/exceptions.py +0 -0
  47. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/filestore.py +0 -0
  48. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/filestore_fs.py +0 -0
  49. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/fileutil.py +0 -0
  50. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/fsck.py +0 -0
  51. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/genconfig.py +0 -0
  52. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/importexport.py +0 -0
  53. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/init.py +0 -0
  54. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/interfaces.py +0 -0
  55. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/keyfs_sqlite.py +0 -0
  56. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/keyfs_sqlite_fs.py +0 -0
  57. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/keyfs_types.py +0 -0
  58. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/log.py +0 -0
  59. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/main.py +0 -0
  60. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/markers.py +0 -0
  61. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/middleware.py +0 -0
  62. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/mythread.py +0 -0
  63. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/passwd.py +0 -0
  64. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/readonly.py +0 -0
  65. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/sizeof.py +0 -0
  66. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/vendor/__init__.py +0 -0
  67. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server/view_auth.py +0 -0
  68. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server.egg-info/dependency_links.txt +0 -0
  69. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server.egg-info/entry_points.txt +0 -0
  70. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server.egg-info/not-zip-safe +0 -0
  71. {devpi_server-6.11.0 → devpi-server-6.12.1}/devpi_server.egg-info/top_level.txt +0 -0
  72. {devpi_server-6.11.0 → devpi-server-6.12.1}/pyproject.toml +0 -0
  73. {devpi_server-6.11.0 → devpi-server-6.12.1}/pytest_devpi_server/__init__.py +0 -0
  74. {devpi_server-6.11.0 → devpi-server-6.12.1}/setup.cfg +0 -0
  75. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/__init__.py +0 -0
  76. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/conftest.py +0 -0
  77. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/example.py +0 -0
  78. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/badindexname/dataindex.json +0 -0
  79. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/badusername/dataindex.json +0 -0
  80. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/basescycle/dataindex.json +0 -0
  81. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/createdmodified/dataindex.json +0 -0
  82. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/deletedbase/dataindex.json +0 -0
  83. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/mirrordata/dataindex.json +0 -0
  84. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/mirrordata/root/pypi/dddttt/0.1.dev1/dddttt-0.1.dev1.tar.gz +0 -0
  85. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/modifiedpypi/dataindex.json +0 -0
  86. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/nocreatedmodified/dataindex.json +0 -0
  87. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/normalization/dataindex.json +0 -0
  88. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/normalization/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  89. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/normalization_merge/dataindex.json +0 -0
  90. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello-pkg/hello.pkg-1.1.tar.gz +0 -0
  91. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  92. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/norootpypi/dataindex.json +0 -0
  93. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/nouser/dataindex.json +0 -0
  94. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/removedindexplugin/dataindex.json +0 -0
  95. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/removedindexplugin/user/dev/pkg/pkg-1.0.tar.gz +0 -0
  96. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/toxresult_naming_scheme/dataindex.json +0 -0
  97. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/0.9/hello-0.9.tar.gz +0 -0
  98. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/sha256=ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73/hello-0.9.tar.gz.toxresult0 +0 -0
  99. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/toxresult_upload_default/dataindex.json +0 -0
  100. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/reqmock.py +0 -0
  101. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/simpypi.py +0 -0
  102. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_auth.py +0 -0
  103. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_authcheck.py +0 -0
  104. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_config.py +0 -0
  105. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_conftest.py +0 -0
  106. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_filestore.py +0 -0
  107. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_fsck.py +0 -0
  108. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_genconfig.py +0 -0
  109. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_importexport.py +0 -0
  110. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_keyfs_sqlite_fs.py +0 -0
  111. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_log.py +0 -0
  112. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_main.py +0 -0
  113. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_mirror_no_project_list.py +0 -0
  114. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_model.py +0 -0
  115. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_mythread.py +0 -0
  116. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_nginx_replica.py +0 -0
  117. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_permissions.py +0 -0
  118. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_readonly.py +0 -0
  119. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_replica.py +0 -0
  120. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_replica_functional.py +0 -0
  121. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_reqmock.py +0 -0
  122. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_stage_customizer.py +0 -0
  123. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_streaming.py +0 -0
  124. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_streaming_nginx.py +0 -0
  125. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_streaming_replica.py +0 -0
  126. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_streaming_replica_nginx.py +0 -0
  127. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_view_auth.py +0 -0
  128. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_views_patch.py +0 -0
  129. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_views_push_external.py +0 -0
  130. {devpi_server-6.11.0 → devpi-server-6.12.1}/test_devpi_server/test_views_status.py +0 -0
  131. {devpi_server-6.11.0 → devpi-server-6.12.1}/tox.ini +0 -0
@@ -2,6 +2,39 @@
2
2
 
3
3
  .. towncrier release notes start
4
4
 
5
+ 6.12.1 (2024-07-24)
6
+ ===================
7
+
8
+ Bug Fixes
9
+ ---------
10
+
11
+ - Support Python 3.13 by depending on legacy-cgi.
12
+
13
+ - Preserve query string when proxying requests from replica to primary. This fixes force removal on non-volatile indexes and probably other bugs.
14
+
15
+ - Fix #1044: Correctly update cache expiry time when mirrored server returns 304 Not Modified.
16
+
17
+
18
+
19
+ 6.12.0 (2024-06-25)
20
+ ===================
21
+
22
+ Features
23
+ --------
24
+
25
+ - Added ``devpiserver_on_toxresult_store`` hook to allow blocking or skipping a toxresult upload on more specific conditions as ``acl_toxresult_upload`` would allow.
26
+
27
+ - Added ``devpiserver_on_toxresult_upload_forbidden`` hook to allow returning a custom message and result (403 or 200).
28
+
29
+
30
+
31
+ Bug Fixes
32
+ ---------
33
+
34
+ - Return json data if toxresult upload is forbidden.
35
+
36
+
37
+
5
38
  6.11.0 (2024-04-20)
6
39
  ===================
7
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-server
3
- Version: 6.11.0
3
+ Version: 6.12.1
4
4
  Summary: devpi-server: reliable private and pypi.org caching server
5
5
  Home-page: https://devpi.net
6
6
  Maintainer: Florian Schulze
@@ -27,25 +27,10 @@ Classifier: Programming Language :: Python :: 3.9
27
27
  Classifier: Programming Language :: Python :: 3.10
28
28
  Classifier: Programming Language :: Python :: 3.11
29
29
  Classifier: Programming Language :: Python :: 3.12
30
+ Classifier: Programming Language :: Python :: 3.13
30
31
  Requires-Python: >=3.7
31
32
  License-File: LICENSE
32
33
  License-File: AUTHORS
33
- Requires-Dist: py>=1.4.23
34
- Requires-Dist: httpx
35
- Requires-Dist: argon2-cffi
36
- Requires-Dist: attrs>=21.3.0
37
- Requires-Dist: defusedxml
38
- Requires-Dist: devpi_common<5,>3.6.0
39
- Requires-Dist: itsdangerous>=0.24
40
- Requires-Dist: platformdirs
41
- Requires-Dist: pyramid>=2
42
- Requires-Dist: waitress>=1.0.1
43
- Requires-Dist: repoze.lru>=0.6
44
- Requires-Dist: passlib[argon2]
45
- Requires-Dist: pluggy<2.0,>=0.6.0
46
- Requires-Dist: ruamel.yaml
47
- Requires-Dist: strictyaml
48
- Requires-Dist: lazy
49
34
 
50
35
  =============================================================================
51
36
  devpi-server: server for private package indexes and PyPI caching
@@ -119,89 +104,86 @@ Changelog
119
104
 
120
105
  .. towncrier release notes start
121
106
 
122
- 6.11.0 (2024-04-20)
107
+ 6.12.1 (2024-07-24)
123
108
  ===================
124
109
 
125
- Features
126
- --------
127
-
128
- - The ``devpi-fsck`` script now returns an error code when there have been missing files or checksum errors.
129
-
130
- - Fix #983: Add plugin hook for mirror authentication header.
131
-
132
-
133
-
134
110
  Bug Fixes
135
111
  ---------
136
112
 
137
- - Preserve last modified of docs and toxresults during export/import.
113
+ - Support Python 3.13 by depending on legacy-cgi.
138
114
 
139
- - Fix #1033: Use ``int`` for ``--mirror-cache-expiry`` to fix value of ``proxy_cache_valid`` in nginx caching config.
115
+ - Preserve query string when proxying requests from replica to primary. This fixes force removal on non-volatile indexes and probably other bugs.
140
116
 
117
+ - Fix #1044: Correctly update cache expiry time when mirrored server returns 304 Not Modified.
141
118
 
142
119
 
143
- 6.10.0 (2023-12-19)
120
+
121
+ 6.12.0 (2024-06-25)
144
122
  ===================
145
123
 
146
124
  Features
147
125
  --------
148
126
 
149
- - Use ``Authorization`` header instead of adding username/password to URL when fetching from mirror.
127
+ - Added ``devpiserver_on_toxresult_store`` hook to allow blocking or skipping a toxresult upload on more specific conditions as ``acl_toxresult_upload`` would allow.
150
128
 
151
- - Fix #998: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
129
+ - Added ``devpiserver_on_toxresult_upload_forbidden`` hook to allow returning a custom message and result (403 or 200).
152
130
 
153
131
 
154
132
 
155
133
  Bug Fixes
156
134
  ---------
157
135
 
158
- - Fix #996: support hashes other than sha256 in application/vnd.pypi.simple.v1+json responses.
136
+ - Return json data if toxresult upload is forbidden.
159
137
 
160
- - Only compare hostname instead of full URL prefix when parsing mirror packages to fix mirrors with basic authentication and absolute URLs. See #1006
161
138
 
162
139
 
140
+ 6.11.0 (2024-04-20)
141
+ ===================
163
142
 
164
- 6.9.2 (2023-08-06)
165
- ==================
143
+ Features
144
+ --------
166
145
 
167
- Bug Fixes
168
- ---------
146
+ - The ``devpi-fsck`` script now returns an error code when there have been missing files or checksum errors.
169
147
 
170
- - Prevent duplicates when adding values to lists in index configuration with ``+=`` operator.
148
+ - Fix #983: Add plugin hook for mirror authentication header.
171
149
 
172
150
 
173
- 6.9.1 (2023-07-02)
174
- ==================
175
151
 
176
152
  Bug Fixes
177
153
  ---------
178
154
 
179
- - Prevent error in find_pre_existing_file in case of incomplete metadata.
155
+ - Preserve last modified of docs and toxresults during export/import.
180
156
 
181
- - Fix #980: Remove long deprecated backward compatibility for old pluggy versions to fix error with pluggy 1.1.0.
157
+ - Fix #1033: Use ``int`` for ``--mirror-cache-expiry`` to fix value of ``proxy_cache_valid`` in nginx caching config.
182
158
 
183
159
 
184
- 6.9.0 (2023-05-23)
185
- ==================
160
+
161
+ 6.10.0 (2023-12-19)
162
+ ===================
186
163
 
187
164
  Features
188
165
  --------
189
166
 
190
- - Support export directory layout for ``--replica-file-search-path`` option.
167
+ - Use ``Authorization`` header instead of adding username/password to URL when fetching from mirror.
168
+
169
+ - Fix #998: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
191
170
 
192
- - Fix #931: Add ``mirror_no_project_list`` setting for mirror indexes that have no full project list like google cloud artifacts or if you want to prevent downloading the full list for huge indexes like PyPI.
193
171
 
194
172
 
195
173
  Bug Fixes
196
174
  ---------
197
175
 
198
- - Keep a reference to async tasks to avoid their removal mid execution.
176
+ - Fix #996: support hashes other than sha256 in application/vnd.pypi.simple.v1+json responses.
177
+
178
+ - Only compare hostname instead of full URL prefix when parsing mirror packages to fix mirrors with basic authentication and absolute URLs. See #1006
179
+
199
180
 
200
- - Support changed default of ``enforce_content_length`` in urllib3 >= 2.
201
181
 
202
- - Fix #934: Properly set PATH_INFO when outside URL is used with sub-path.
182
+ 6.9.2 (2023-08-06)
183
+ ==================
203
184
 
204
- - Fix #945: Adapt FatalError to be usable as an async HTTP response when updating a project on a mirror.
185
+ Bug Fixes
186
+ ---------
205
187
 
206
- - Fix wrong hash metadata introduced in 6.5.0 for toxresults which prevents replication. The metadata can be fixed by an export/import cycle.
188
+ - Prevent duplicates when adding values to lists in index configuration with ``+=`` operator.
207
189
 
@@ -0,0 +1 @@
1
+ __version__ = '6.12.1'
@@ -321,27 +321,15 @@ def add_deploy_options(parser, pluginmanager):
321
321
 
322
322
  parser.addoption(
323
323
  "--argon2-memory-cost", type=int, default=DEFAULT_ARGON2_MEMORY_COST,
324
- help="Argon2 memory cost parameter for key derivation. "
325
- "This is *not* for the user password hashes. "
326
- "There should be no need to touch this setting, "
327
- "except you really know what this is about! "
328
- "Replicas need to use the same parameters as the master.")
324
+ help=argparse.SUPPRESS)
329
325
 
330
326
  parser.addoption(
331
327
  "--argon2-parallelism", type=int, default=DEFAULT_ARGON2_PARALLELISM,
332
- help="Argon2 parallelism parameter for key derivation. "
333
- "This is *not* for the user password hashes. "
334
- "There should be no need to touch this setting, "
335
- "except you really know what this is about! "
336
- "Replicas need to use the same parameters as the master.")
328
+ help=argparse.SUPPRESS)
337
329
 
338
330
  parser.addoption(
339
331
  "--argon2-time-cost", type=int, default=DEFAULT_ARGON2_TIME_COST,
340
- help="Argon2 time cost parameter for key derivation. "
341
- "This is *not* for the user password hashes. "
342
- "There should be no need to touch this setting, "
343
- "except you really know what this is about! "
344
- "Replicas need to use the same parameters as the master.")
332
+ help=argparse.SUPPRESS)
345
333
 
346
334
  parser.addoption(
347
335
  "--requests-only", action="store_true",
@@ -567,7 +555,7 @@ class MyArgumentParser(argparse.ArgumentParser):
567
555
  default = action.default
568
556
  if isinstance(action, argparse._StoreFalseAction):
569
557
  default = not default
570
- if action.help and action.default != '==SUPPRESS==':
558
+ if action.help and action.help is not argparse.SUPPRESS and action.default is not argparse.SUPPRESS:
571
559
  action.help += " [%s]" % default
572
560
 
573
561
  def addgroup(self, *args, **kwargs):
@@ -1,5 +1,13 @@
1
-
1
+ from __future__ import annotations
2
2
  from pluggy import HookspecMarker
3
+ from typing import Optional
4
+ from typing import TYPE_CHECKING
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ from devpi_server.views import ToxResultHandling
9
+ from pyramid.request import Request
10
+
3
11
 
4
12
  hookspec = HookspecMarker("devpiserver")
5
13
 
@@ -255,6 +263,28 @@ def devpiserver_on_replicated_file(stage, project, version, link, serial, back_s
255
263
  """Called when a file was downloaded from master on replica."""
256
264
 
257
265
 
266
+ @hookspec(firstresult=True)
267
+ def devpiserver_on_toxresult_store(request: Request, tox_result_handling: ToxResultHandling) -> Optional[ToxResultHandling]:
268
+ """Called when a toxresult is about to be stored.
269
+
270
+ Stops at first non-None result.
271
+
272
+ :returns: A ToxResultHandling object which determines how the upload is processed.
273
+ """
274
+
275
+
276
+ @hookspec(firstresult=True)
277
+ def devpiserver_on_toxresult_upload_forbidden(request: Request, tox_result_handling: ToxResultHandling) -> Optional[str]:
278
+ """Called when the permission check for toxresult upload failed.
279
+
280
+ Stops at first non-None result.
281
+
282
+ :returns: A ToxResultHandling object which determines whether an error
283
+ with message (default using ``block``) or a success with a message
284
+ (using ``skip``) is returned.
285
+ """
286
+
287
+
258
288
  @hookspec
259
289
  def devpiserver_metrics(request):
260
290
  """ called for status view.
@@ -673,13 +673,9 @@ class Transaction(object):
673
673
  def exists(self, typedkey):
674
674
  if typedkey in self.cache:
675
675
  val = self.cache[typedkey]
676
- if val in (absent, deleted):
677
- return False
678
- return True
676
+ return val not in (absent, deleted)
679
677
  (serial, val) = self.get_original(typedkey)
680
- if val in (absent, deleted):
681
- return False
682
- return True
678
+ return val not in (absent, deleted)
683
679
 
684
680
  def delete(self, typedkey):
685
681
  if not self.write:
@@ -692,7 +688,7 @@ class Transaction(object):
692
688
  raise self.keyfs.ReadOnly()
693
689
  # sanity check for dictionaries: we always want to have unicode
694
690
  # keys, not bytes
695
- if typedkey.type == dict:
691
+ if typedkey.type is dict:
696
692
  check_unicode_keys(val)
697
693
  assert val is not None
698
694
  self.cache[typedkey] = val
@@ -794,8 +794,8 @@ class MirrorStage(BaseStage):
794
794
  f"timeout after {self.timeout} seconds while getting data for {project!r}")
795
795
  except self.UpstreamNotModified as e:
796
796
  if links is not None:
797
- self.keyfs.tx.on_commit_success(partial(
798
- self.cache_retrieve_times.refresh, project, e.etag))
797
+ # immediately update the cache
798
+ self.cache_retrieve_times.refresh(project, e.etag)
799
799
  return self.SimpleLinks(links)
800
800
  if e.etag is None:
801
801
  threadlog.error(
@@ -42,9 +42,9 @@ def join_links_data(links, requires_python, yanked):
42
42
  # build list of (key, href, require_python, yanked) tuples
43
43
  result = []
44
44
  links = zip_longest(links, requires_python, yanked, fillvalue=None)
45
- for link, require_python, yanked in links:
45
+ for link, require_python, link_yanked in links:
46
46
  key, href = link
47
- result.append((key, href, require_python, yanked))
47
+ result.append((key, href, require_python, link_yanked))
48
48
  return result
49
49
 
50
50
 
@@ -1615,11 +1615,10 @@ class LinkStore:
1615
1615
 
1616
1616
  @property
1617
1617
  def metadata(self):
1618
- metadata = {}
1619
- for k, v in get_mutable_deepcopy(self.verdata).items():
1620
- if not k.startswith("+"):
1621
- metadata[k] = v
1622
- return metadata
1618
+ return {
1619
+ k: v
1620
+ for k, v in get_mutable_deepcopy(self.verdata).items()
1621
+ if not k.startswith("+")}
1623
1622
 
1624
1623
  def get_file_entry(self, relpath):
1625
1624
  return self.filestore.get_file_entry(relpath)
@@ -6,6 +6,7 @@ import secrets
6
6
  import threading
7
7
  import time
8
8
  import traceback
9
+ from contextlib import suppress
9
10
  from functools import partial
10
11
  from pluggy import HookimplMarker
11
12
  from pyramid.httpexceptions import HTTPNotFound, HTTPAccepted, HTTPBadRequest
@@ -1174,18 +1175,22 @@ class BodyFileWrapper:
1174
1175
  self.len = length
1175
1176
 
1176
1177
 
1177
- def proxy_request_to_master(xom, request, stream=False):
1178
+ def proxy_request_to_master(xom, request, *, stream=False):
1178
1179
  master_url = xom.config.master_url
1179
- url = master_url.joinpath(request.path).url
1180
+ request_url = URL(request.url)
1181
+ url = (
1182
+ master_url
1183
+ .joinpath(request_url.path)
1184
+ .replace(query=request_url.query)
1185
+ .url)
1180
1186
  assert url.startswith(master_url.url)
1181
1187
  http = xom._httpsession
1182
1188
  with threadlog.around("info", "relaying: %s %s", request.method, url):
1183
1189
  try:
1184
1190
  headers = clean_request_headers(request)
1185
- try:
1191
+ length = None
1192
+ with suppress(ValueError, TypeError):
1186
1193
  length = int(headers.get('Content-Length'))
1187
- except (ValueError, TypeError):
1188
- length = None
1189
1194
  if length:
1190
1195
  body = BodyFileWrapper(request.body_file, length)
1191
1196
  else:
@@ -1197,7 +1202,8 @@ def proxy_request_to_master(xom, request, stream=False):
1197
1202
  allow_redirects=False,
1198
1203
  timeout=xom.config.args.proxy_timeout)
1199
1204
  except http.Errors as e:
1200
- raise UpstreamError("proxy-write-to-master %s: %s" % (url, e))
1205
+ msg = f"proxy-write-to-master {url}: {e}"
1206
+ raise UpstreamError(msg) from e
1201
1207
 
1202
1208
 
1203
1209
  def proxy_write_to_master(xom, request):
@@ -1231,10 +1237,11 @@ def proxy_write_to_master(xom, request):
1231
1237
  headers=headers)
1232
1238
 
1233
1239
 
1234
- def proxy_view_to_master(context, request):
1240
+ def proxy_view_to_master(_context, request):
1235
1241
  xom = request.registry["xom"]
1236
1242
  tx = getattr(xom.keyfs, "tx", None)
1237
- assert getattr(tx, "write", False) is False, "there should be no write transaction"
1243
+ if getattr(tx, "write", False):
1244
+ raise RuntimeError("there should be no write transaction")
1238
1245
  return proxy_write_to_master(xom, request)
1239
1246
 
1240
1247
 
@@ -14,12 +14,12 @@ class HTMLPage(object):
14
14
  """Represents one page, along with its URL"""
15
15
 
16
16
  # FIXME: these regexes are horrible hacks:
17
- _homepage_re = re.compile(r'<th>\s*home\s*page', re.I)
18
- _download_re = re.compile(r'<th>\s*download\s+url', re.I)
17
+ _homepage_re = re.compile(r'<th>\s*home\s*page', re.IGNORECASE)
18
+ _download_re = re.compile(r'<th>\s*download\s+url', re.IGNORECASE)
19
19
  # These aren't so awful:
20
- _rel_re = re.compile(r"""<[^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*>""", re.I)
21
- _href_re = re.compile('href=(?:"([^"]*)"|\'([^\']*)\'|([^>\\s\\n]*))', re.I | re.S)
22
- _base_re = re.compile(r"""<base\s+href\s*=\s*['"]?([^'">]+)""", re.I)
20
+ _rel_re = re.compile(r"""<[^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*>""", re.IGNORECASE)
21
+ _href_re = re.compile('href=(?:"([^"]*)"|\'([^\']*)\'|([^>\\s\\n]*))', re.IGNORECASE | re.DOTALL)
22
+ _base_re = re.compile(r"""<base\s+href\s*=\s*['"]?([^'">]+)""", re.IGNORECASE)
23
23
 
24
24
  def __init__(self, content, url, headers=None):
25
25
  self.content = content
@@ -113,7 +113,7 @@ class HTMLPage(object):
113
113
  url = self.clean_link(urljoin(self.base_url, url))
114
114
  yield Link(url, self)
115
115
 
116
- _clean_re = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.I)
116
+ _clean_re = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.IGNORECASE)
117
117
 
118
118
  def clean_link(self, url):
119
119
  """Makes sure a link is fully encoded. That is, if a ' ' shows up in
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  import contextlib
2
3
  import os
3
4
  import re
@@ -50,6 +51,7 @@ from .readonly import get_mutable_deepcopy
50
51
  from .log import thread_push_log, thread_pop_log, threadlog
51
52
 
52
53
  from .auth import Auth
54
+ import attrs
53
55
 
54
56
  devpiweb_hookimpl = HookimplMarker("devpiweb")
55
57
  server_version = devpi_server.__version__
@@ -79,9 +81,7 @@ def _select_simple_content_type(request):
79
81
 
80
82
 
81
83
  def is_simple_json(request):
82
- if _select_simple_content_type(request) == SIMPLE_API_V1_JSON:
83
- return True
84
- return False
84
+ return _select_simple_content_type(request) == SIMPLE_API_V1_JSON
85
85
 
86
86
 
87
87
  def is_requested_by_installer(request):
@@ -412,6 +412,34 @@ def devpiserver_authcheck_forbidden(request):
412
412
  return True
413
413
 
414
414
 
415
+ TOXRESULT_UPLOAD_FORBIDDEN = (
416
+ "No permission to upload tox results. "
417
+ "You can use the devpi test --no-upload option to skip the upload.")
418
+
419
+
420
+ @attrs.define(frozen=True)
421
+ class ToxResultHandling:
422
+ _block: bool = attrs.field(default=False, alias="_block")
423
+ _skip: bool = attrs.field(default=False, alias="_skip")
424
+ msg: None | str = None
425
+
426
+ def block(self, msg=TOXRESULT_UPLOAD_FORBIDDEN):
427
+ return ToxResultHandling(_block=True, msg=msg)
428
+
429
+ def skip(self, msg=None):
430
+ return ToxResultHandling(_skip=True, msg=msg)
431
+
432
+
433
+ @hookimpl(trylast=True)
434
+ def devpiserver_on_toxresult_store(request, tox_result_handling):
435
+ return tox_result_handling
436
+
437
+
438
+ @hookimpl(trylast=True)
439
+ def devpiserver_on_toxresult_upload_forbidden(request, tox_result_handling):
440
+ return tox_result_handling
441
+
442
+
415
443
  def version_in_filename(version, filename):
416
444
  if version is None:
417
445
  # no version set, so skip check
@@ -419,9 +447,7 @@ def version_in_filename(version, filename):
419
447
  if version in filename:
420
448
  return True
421
449
  # PEP 427 escaped wheels
422
- if re.sub(r"[^\w\d.]+", "_", version, flags=re.UNICODE) in filename:
423
- return True
424
- return False
450
+ return re.sub(r"[^\w\d.]+", "_", version, flags=re.UNICODE) in filename
425
451
 
426
452
 
427
453
  class PyPIView:
@@ -530,14 +556,25 @@ class PyPIView:
530
556
 
531
557
  @view_config(
532
558
  route_name="/{user}/{index}/+f/{relpath:.*}",
533
- request_method="POST",
534
- permission="toxresult_upload")
559
+ request_method="POST")
535
560
  def post_toxresult(self):
536
- stage = self.context.stage
537
- relpath = self.request.path_info.strip("/")
538
- link = stage.get_link_from_entrypath(relpath)
539
- if link is None or link.rel != "releasefile":
540
- apireturn(404, message="no release file found at %s" % relpath)
561
+ if not self.request.has_permission("toxresult_upload"):
562
+ default_tox_result_handling = ToxResultHandling().block(TOXRESULT_UPLOAD_FORBIDDEN)
563
+ tox_result_handling = self.xom.config.hook.devpiserver_on_toxresult_upload_forbidden(
564
+ request=self.request, tox_result_handling=default_tox_result_handling)
565
+ else:
566
+ stage = self.context.stage
567
+ relpath = self.request.path_info.strip("/")
568
+ link = stage.get_link_from_entrypath(relpath)
569
+ if link is None or link.rel != "releasefile":
570
+ apireturn(404, message="no release file found at %s" % relpath)
571
+ default_tox_result_handling = ToxResultHandling()
572
+ tox_result_handling = self.xom.config.hook.devpiserver_on_toxresult_store(
573
+ request=self.request, tox_result_handling=default_tox_result_handling)
574
+ if tox_result_handling._block:
575
+ apireturn(403, tox_result_handling.msg)
576
+ if tox_result_handling._skip:
577
+ apireturn(200, tox_result_handling.msg)
541
578
  # the getjson call validates that we got valid json
542
579
  getjson(self.request)
543
580
  # but we store the original body
@@ -555,9 +592,7 @@ class PyPIView:
555
592
  def _use_absolute_urls(self):
556
593
  if self.xom.config.args.absolute_urls:
557
594
  return True
558
- if 'HTTP_X_DEVPI_ABSOLUTE_URLS' in self.request.environ:
559
- return True
560
- return False
595
+ return 'HTTP_X_DEVPI_ABSOLUTE_URLS' in self.request.environ
561
596
 
562
597
  @view_config(route_name="/{user}/{index}/+simple/{project}")
563
598
  def simple_list_project_redirect(self):
@@ -745,18 +780,18 @@ class PyPIView:
745
780
  # depending on remote networks
746
781
  content_type = _select_simple_content_type(self.request)
747
782
  if content_type == SIMPLE_API_V1_JSON:
748
- app_iter = self._simple_list_all_json_v1(stage, stage_results)
783
+ app_iter = self._simple_list_all_json_v1(stage_results)
749
784
  elif is_requested_by_installer(self.request):
750
- app_iter = self._simple_list_all_installer(stage, stage_results)
785
+ app_iter = self._simple_list_all_installer(stage_results)
751
786
  else:
752
- app_iter = self._simple_list_all(stage, stage_results)
787
+ app_iter = self._simple_list_all(stage.name, stage_results)
753
788
  return Response(
754
789
  app_iter=buffered_iterator(app_iter),
755
790
  content_type=content_type,
756
791
  vary=set(["Accept", "User-Agent"]))
757
792
 
758
- def _simple_list_all(self, stage, stage_results):
759
- title = "%s: simple list (including inherited indices)" % (stage.name)
793
+ def _simple_list_all(self, stage_name, stage_results):
794
+ title = f"{stage_name}: simple list (including inherited indices)"
760
795
  yield f"<!DOCTYPE html><html><head><title>{title}</title></head><body><h1>{title}</h1>".encode("utf-8")
761
796
  last_index = len(stage_results) - 1
762
797
  seen = set()
@@ -773,7 +808,7 @@ class PyPIView:
773
808
  seen.add(name)
774
809
  yield "</body></html>".encode("utf-8")
775
810
 
776
- def _simple_list_all_installer(self, stage, stage_results):
811
+ def _simple_list_all_installer(self, stage_results):
777
812
  yield "<!DOCTYPE html><html><body>".encode("utf-8")
778
813
  last_index = len(stage_results) - 1
779
814
  seen = set()
@@ -785,7 +820,7 @@ class PyPIView:
785
820
  seen.add(name)
786
821
  yield "</body></html>".encode("utf-8")
787
822
 
788
- def _simple_list_all_json_v1(self, stage, stage_results):
823
+ def _simple_list_all_json_v1(self, stage_results):
789
824
  yield '{"meta":{"api-version":"1.0"},"projects":['.encode("utf-8")
790
825
  last_index = len(stage_results) - 1
791
826
  seen = set()