devpi-server 6.12.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.12.0 → devpi-server-6.12.1}/CHANGELOG +14 -0
  2. {devpi_server-6.12.0 → devpi-server-6.12.1}/PKG-INFO +16 -28
  3. devpi-server-6.12.1/devpi_server/__init__.py +1 -0
  4. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/config.py +4 -16
  5. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/keyfs.py +3 -7
  6. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/mirror.py +2 -2
  7. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/model.py +6 -7
  8. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/replica.py +15 -8
  9. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/vendor/_pip.py +6 -6
  10. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/views.py +10 -16
  11. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server.egg-info/PKG-INFO +16 -28
  12. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server.egg-info/SOURCES.txt +0 -1
  13. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server.egg-info/requires.txt +4 -1
  14. {devpi_server-6.12.0 → devpi-server-6.12.1}/setup.py +4 -3
  15. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/functional.py +12 -0
  16. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/plugin.py +23 -41
  17. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_fileutil.py +2 -2
  18. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_keyfs.py +44 -4
  19. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_mirror.py +6 -0
  20. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_nginx.py +2 -2
  21. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_views.py +1 -1
  22. devpi_server-6.12.0/CHANGELOG.short.rst +0 -97
  23. devpi_server-6.12.0/devpi_server/__init__.py +0 -1
  24. {devpi_server-6.12.0 → devpi-server-6.12.1}/.flake8 +0 -0
  25. {devpi_server-6.12.0 → devpi-server-6.12.1}/AUTHORS +0 -0
  26. {devpi_server-6.12.0 → devpi-server-6.12.1}/LICENSE +0 -0
  27. {devpi_server-6.12.0 → devpi-server-6.12.1}/MANIFEST.in +0 -0
  28. {devpi_server-6.12.0 → devpi-server-6.12.1}/README.rst +0 -0
  29. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/__main__.py +0 -0
  30. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/auth.py +0 -0
  31. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/auth_basic.py +0 -0
  32. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/auth_devpi.py +0 -0
  33. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/__init__.py +0 -0
  34. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/crontab.template +0 -0
  35. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/devpi.service.template +0 -0
  36. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/launchd-macos.txt.template +0 -0
  37. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi-caching-http.conf.template +0 -0
  38. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi-caching-location.conf.template +0 -0
  39. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi-caching-proxy.conf.template +0 -0
  40. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi-caching-server.conf.template +0 -0
  41. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/nginx-devpi.conf.template +0 -0
  42. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/supervisor-devpi.conf.template +0 -0
  43. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/supervisord.conf.template +0 -0
  44. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/cfg/windows-service.txt.template +0 -0
  45. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/exceptions.py +0 -0
  46. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/filestore.py +0 -0
  47. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/filestore_fs.py +0 -0
  48. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/fileutil.py +0 -0
  49. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/fsck.py +0 -0
  50. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/genconfig.py +0 -0
  51. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/hookspecs.py +0 -0
  52. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/importexport.py +0 -0
  53. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/init.py +0 -0
  54. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/interfaces.py +0 -0
  55. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/keyfs_sqlite.py +0 -0
  56. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/keyfs_sqlite_fs.py +0 -0
  57. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/keyfs_types.py +0 -0
  58. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/log.py +0 -0
  59. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/main.py +0 -0
  60. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/markers.py +0 -0
  61. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/middleware.py +0 -0
  62. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/mythread.py +0 -0
  63. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/passwd.py +0 -0
  64. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/readonly.py +0 -0
  65. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/sizeof.py +0 -0
  66. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/vendor/__init__.py +0 -0
  67. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server/view_auth.py +0 -0
  68. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server.egg-info/dependency_links.txt +0 -0
  69. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server.egg-info/entry_points.txt +0 -0
  70. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server.egg-info/not-zip-safe +0 -0
  71. {devpi_server-6.12.0 → devpi-server-6.12.1}/devpi_server.egg-info/top_level.txt +0 -0
  72. {devpi_server-6.12.0 → devpi-server-6.12.1}/pyproject.toml +0 -0
  73. {devpi_server-6.12.0 → devpi-server-6.12.1}/pytest_devpi_server/__init__.py +0 -0
  74. {devpi_server-6.12.0 → devpi-server-6.12.1}/setup.cfg +0 -0
  75. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/__init__.py +0 -0
  76. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/conftest.py +0 -0
  77. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/example.py +0 -0
  78. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/badindexname/dataindex.json +0 -0
  79. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/badusername/dataindex.json +0 -0
  80. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/basescycle/dataindex.json +0 -0
  81. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/createdmodified/dataindex.json +0 -0
  82. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/deletedbase/dataindex.json +0 -0
  83. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/mirrordata/dataindex.json +0 -0
  84. {devpi_server-6.12.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.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/modifiedpypi/dataindex.json +0 -0
  86. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/nocreatedmodified/dataindex.json +0 -0
  87. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/normalization/dataindex.json +0 -0
  88. {devpi_server-6.12.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.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/normalization_merge/dataindex.json +0 -0
  90. {devpi_server-6.12.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.12.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.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/norootpypi/dataindex.json +0 -0
  93. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/nouser/dataindex.json +0 -0
  94. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/removedindexplugin/dataindex.json +0 -0
  95. {devpi_server-6.12.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.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/toxresult_naming_scheme/dataindex.json +0 -0
  97. {devpi_server-6.12.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.12.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.12.0 → devpi-server-6.12.1}/test_devpi_server/importexportdata/toxresult_upload_default/dataindex.json +0 -0
  100. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/reqmock.py +0 -0
  101. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/simpypi.py +0 -0
  102. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_auth.py +0 -0
  103. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_authcheck.py +0 -0
  104. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_config.py +0 -0
  105. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_conftest.py +0 -0
  106. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_filestore.py +0 -0
  107. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_fsck.py +0 -0
  108. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_genconfig.py +0 -0
  109. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_importexport.py +0 -0
  110. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_keyfs_sqlite_fs.py +0 -0
  111. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_log.py +0 -0
  112. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_main.py +0 -0
  113. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_mirror_no_project_list.py +0 -0
  114. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_model.py +0 -0
  115. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_mythread.py +0 -0
  116. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_nginx_replica.py +0 -0
  117. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_permissions.py +0 -0
  118. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_readonly.py +0 -0
  119. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_replica.py +0 -0
  120. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_replica_functional.py +0 -0
  121. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_reqmock.py +0 -0
  122. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_stage_customizer.py +0 -0
  123. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_streaming.py +0 -0
  124. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_streaming_nginx.py +0 -0
  125. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_streaming_replica.py +0 -0
  126. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_streaming_replica_nginx.py +0 -0
  127. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_view_auth.py +0 -0
  128. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_views_patch.py +0 -0
  129. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_views_push_external.py +0 -0
  130. {devpi_server-6.12.0 → devpi-server-6.12.1}/test_devpi_server/test_views_status.py +0 -0
  131. {devpi_server-6.12.0 → devpi-server-6.12.1}/tox.ini +0 -0
@@ -2,6 +2,20 @@
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
+
5
19
  6.12.0 (2024-06-25)
6
20
  ===================
7
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-server
3
- Version: 6.12.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,6 +104,20 @@ Changelog
119
104
 
120
105
  .. towncrier release notes start
121
106
 
107
+ 6.12.1 (2024-07-24)
108
+ ===================
109
+
110
+ Bug Fixes
111
+ ---------
112
+
113
+ - Support Python 3.13 by depending on legacy-cgi.
114
+
115
+ - Preserve query string when proxying requests from replica to primary. This fixes force removal on non-volatile indexes and probably other bugs.
116
+
117
+ - Fix #1044: Correctly update cache expiry time when mirrored server returns 304 Not Modified.
118
+
119
+
120
+
122
121
  6.12.0 (2024-06-25)
123
122
  ===================
124
123
 
@@ -188,14 +187,3 @@ Bug Fixes
188
187
 
189
188
  - Prevent duplicates when adding values to lists in index configuration with ``+=`` operator.
190
189
 
191
-
192
- 6.9.1 (2023-07-02)
193
- ==================
194
-
195
- Bug Fixes
196
- ---------
197
-
198
- - Prevent error in find_pre_existing_file in case of incomplete metadata.
199
-
200
- - Fix #980: Remove long deprecated backward compatibility for old pluggy versions to fix error with pluggy 1.1.0.
201
-
@@ -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):
@@ -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
@@ -81,9 +81,7 @@ def _select_simple_content_type(request):
81
81
 
82
82
 
83
83
  def is_simple_json(request):
84
- if _select_simple_content_type(request) == SIMPLE_API_V1_JSON:
85
- return True
86
- return False
84
+ return _select_simple_content_type(request) == SIMPLE_API_V1_JSON
87
85
 
88
86
 
89
87
  def is_requested_by_installer(request):
@@ -449,9 +447,7 @@ def version_in_filename(version, filename):
449
447
  if version in filename:
450
448
  return True
451
449
  # PEP 427 escaped wheels
452
- if re.sub(r"[^\w\d.]+", "_", version, flags=re.UNICODE) in filename:
453
- return True
454
- return False
450
+ return re.sub(r"[^\w\d.]+", "_", version, flags=re.UNICODE) in filename
455
451
 
456
452
 
457
453
  class PyPIView:
@@ -596,9 +592,7 @@ class PyPIView:
596
592
  def _use_absolute_urls(self):
597
593
  if self.xom.config.args.absolute_urls:
598
594
  return True
599
- if 'HTTP_X_DEVPI_ABSOLUTE_URLS' in self.request.environ:
600
- return True
601
- return False
595
+ return 'HTTP_X_DEVPI_ABSOLUTE_URLS' in self.request.environ
602
596
 
603
597
  @view_config(route_name="/{user}/{index}/+simple/{project}")
604
598
  def simple_list_project_redirect(self):
@@ -786,18 +780,18 @@ class PyPIView:
786
780
  # depending on remote networks
787
781
  content_type = _select_simple_content_type(self.request)
788
782
  if content_type == SIMPLE_API_V1_JSON:
789
- app_iter = self._simple_list_all_json_v1(stage, stage_results)
783
+ app_iter = self._simple_list_all_json_v1(stage_results)
790
784
  elif is_requested_by_installer(self.request):
791
- app_iter = self._simple_list_all_installer(stage, stage_results)
785
+ app_iter = self._simple_list_all_installer(stage_results)
792
786
  else:
793
- app_iter = self._simple_list_all(stage, stage_results)
787
+ app_iter = self._simple_list_all(stage.name, stage_results)
794
788
  return Response(
795
789
  app_iter=buffered_iterator(app_iter),
796
790
  content_type=content_type,
797
791
  vary=set(["Accept", "User-Agent"]))
798
792
 
799
- def _simple_list_all(self, stage, stage_results):
800
- 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)"
801
795
  yield f"<!DOCTYPE html><html><head><title>{title}</title></head><body><h1>{title}</h1>".encode("utf-8")
802
796
  last_index = len(stage_results) - 1
803
797
  seen = set()
@@ -814,7 +808,7 @@ class PyPIView:
814
808
  seen.add(name)
815
809
  yield "</body></html>".encode("utf-8")
816
810
 
817
- def _simple_list_all_installer(self, stage, stage_results):
811
+ def _simple_list_all_installer(self, stage_results):
818
812
  yield "<!DOCTYPE html><html><body>".encode("utf-8")
819
813
  last_index = len(stage_results) - 1
820
814
  seen = set()
@@ -826,7 +820,7 @@ class PyPIView:
826
820
  seen.add(name)
827
821
  yield "</body></html>".encode("utf-8")
828
822
 
829
- def _simple_list_all_json_v1(self, stage, stage_results):
823
+ def _simple_list_all_json_v1(self, stage_results):
830
824
  yield '{"meta":{"api-version":"1.0"},"projects":['.encode("utf-8")
831
825
  last_index = len(stage_results) - 1
832
826
  seen = set()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-server
3
- Version: 6.12.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,6 +104,20 @@ Changelog
119
104
 
120
105
  .. towncrier release notes start
121
106
 
107
+ 6.12.1 (2024-07-24)
108
+ ===================
109
+
110
+ Bug Fixes
111
+ ---------
112
+
113
+ - Support Python 3.13 by depending on legacy-cgi.
114
+
115
+ - Preserve query string when proxying requests from replica to primary. This fixes force removal on non-volatile indexes and probably other bugs.
116
+
117
+ - Fix #1044: Correctly update cache expiry time when mirrored server returns 304 Not Modified.
118
+
119
+
120
+
122
121
  6.12.0 (2024-06-25)
123
122
  ===================
124
123
 
@@ -188,14 +187,3 @@ Bug Fixes
188
187
 
189
188
  - Prevent duplicates when adding values to lists in index configuration with ``+=`` operator.
190
189
 
191
-
192
- 6.9.1 (2023-07-02)
193
- ==================
194
-
195
- Bug Fixes
196
- ---------
197
-
198
- - Prevent error in find_pre_existing_file in case of incomplete metadata.
199
-
200
- - Fix #980: Remove long deprecated backward compatibility for old pluggy versions to fix error with pluggy 1.1.0.
201
-
@@ -1,7 +1,6 @@
1
1
  .flake8
2
2
  AUTHORS
3
3
  CHANGELOG
4
- CHANGELOG.short.rst
5
4
  LICENSE
6
5
  MANIFEST.in
7
6
  README.rst
@@ -1,7 +1,7 @@
1
1
  py>=1.4.23
2
2
  httpx
3
3
  argon2-cffi
4
- attrs>=21.3.0
4
+ attrs>=22.2.0
5
5
  defusedxml
6
6
  devpi_common<5,>3.6.0
7
7
  itsdangerous>=0.24
@@ -14,3 +14,6 @@ pluggy<2.0,>=0.6.0
14
14
  ruamel.yaml
15
15
  strictyaml
16
16
  lazy
17
+
18
+ [:python_version >= "3.13"]
19
+ legacy-cgi
@@ -29,10 +29,11 @@ if __name__ == "__main__":
29
29
  install_requires = ["py>=1.4.23",
30
30
  "httpx",
31
31
  "argon2-cffi",
32
- "attrs>=21.3.0",
32
+ "attrs>=22.2.0",
33
33
  "defusedxml",
34
34
  "devpi_common<5,>3.6.0",
35
35
  "itsdangerous>=0.24",
36
+ "legacy-cgi;python_version>='3.13'",
36
37
  "platformdirs",
37
38
  "pyramid>=2",
38
39
  "waitress>=1.0.1",
@@ -57,7 +58,7 @@ if __name__ == "__main__":
57
58
  'Documentation': 'https://doc.devpi.net',
58
59
  'Source Code': 'https://github.com/devpi/devpi'
59
60
  },
60
- version='6.12.0',
61
+ version='6.12.1',
61
62
  maintainer="Florian Schulze",
62
63
  maintainer_email="mail@pyfidelity.com",
63
64
  packages=[
@@ -82,7 +83,7 @@ if __name__ == "__main__":
82
83
  "Programming Language :: Python :: Implementation :: PyPy",
83
84
  ] + [
84
85
  ("Programming Language :: Python :: %s" % x)
85
- for x in "3.7 3.8 3.9 3.10 3.11 3.12".split()],
86
+ for x in "3.7 3.8 3.9 3.10 3.11 3.12 3.13".split()],
86
87
  install_requires=install_requires,
87
88
  extras_require=extras_require,
88
89
  python_requires='>=3.7',
@@ -349,6 +349,18 @@ class TestIndexPushThings:
349
349
 
350
350
  @pytest.mark.nomocking
351
351
  class TestProjectThings:
352
+ def test_non_volatile_force_delete(self, mapp, server_version):
353
+ force_delete_fix_version = parse_version("6.12.1dev")
354
+ if server_version < force_delete_fix_version:
355
+ pytest.skip("devpi-server without force delete via replica fix")
356
+ mapp.create_and_login_user("cuser3")
357
+ mapp.create_index("dev", indexconfig={"volatile": False})
358
+ mapp.use("cuser3/dev")
359
+ content = mapp.makepkg("hello-1.0.tar.gz", b"content", "hello", "1.0")
360
+ mapp.upload_file_pypi("hello-1.0.tar.gz", content, "hello", "1.0")
361
+ mapp.delete_project("hello", code=403)
362
+ mapp.delete_project("hello", force=True)
363
+
352
364
  def test_toxresult(self, mapp):
353
365
  import json
354
366
  mapp.create_and_use('pruser1/dev')
@@ -364,22 +364,21 @@ def httpget(pypiurls):
364
364
  self._md5 = hashlib.md5() # noqa: S324
365
365
  self.call_log = []
366
366
 
367
- async def async_httpget(self, url, allow_redirects, timeout=None, extra_headers=None):
368
- response = self.__call__(url, allow_redirects, extra_headers, timeout=timeout)
369
- if response.status_code < 300:
370
- text = response.text
371
- else:
372
- text = None
367
+ async def async_httpget(self, url, *, allow_redirects, timeout=None, extra_headers=None):
368
+ response = self.__call__(url, allow_redirects=allow_redirects, extra_headers=extra_headers, timeout=timeout)
369
+ text = response.text if response.status_code < 300 else None
373
370
  return (response, text)
374
371
 
375
- def __call__(self, url, allow_redirects=False, extra_headers=None, **kw):
372
+ def __call__(self, url, *, allow_redirects=False, extra_headers=None, **kw):
376
373
  class mockresponse:
377
374
  def __init__(xself, url):
378
375
  fakeresponse = self.url2response.get(url)
376
+ from_list = False
379
377
  if isinstance(fakeresponse, list):
380
378
  if not fakeresponse:
381
379
  pytest.fail(
382
380
  f"http_api call to {url} has no further replies")
381
+ from_list = True
383
382
  fakeresponse = fakeresponse.pop(0)
384
383
  if fakeresponse is None:
385
384
  fakeresponse = dict(
@@ -396,6 +395,13 @@ def httpget(pypiurls):
396
395
  xself.headers.setdefault('content-type', fakeresponse.get(
397
396
  'content_type', 'text/html'))
398
397
  if "etag" in fakeresponse:
398
+ # add follow up response
399
+ new_response = dict(fakeresponse, status_code=304)
400
+ new_response.pop("etag")
401
+ if from_list:
402
+ self.url2response[url].append(new_response)
403
+ else:
404
+ self.url2response[url] = new_response
399
405
  fakeresponse["headers"]["ETag"] = fakeresponse["etag"]
400
406
 
401
407
  def close(xself):
@@ -455,12 +461,7 @@ def httpget(pypiurls):
455
461
  if pypiserial is not None:
456
462
  headers["X-PYPI-LAST-SERIAL"] = str(pypiserial)
457
463
  kw.setdefault("url", URL(remoteurl).joinpath(name).asdir().url)
458
- if "etag" in kw:
459
- etag = kw.pop("etag")
460
- self.add(text=text, **kw, etag=etag)
461
- self.add(text=text, **kw, status_code=304)
462
- else:
463
- self.mockresponse(text=text, **kw)
464
+ self.mockresponse(text=text, **kw)
464
465
  return ret
465
466
 
466
467
  def _getmd5digest(self, s):
@@ -665,21 +666,14 @@ class Mapp(MappMixin):
665
666
  assert res.get("email") == email
666
667
 
667
668
  def modify_user(self, user, code=200, password=None, **kwargs):
668
- reqdict = {}
669
+ reqdict = dict(kwargs)
669
670
  if password:
670
671
  reqdict["password"] = password
671
- for key, value in kwargs.items():
672
- reqdict[key] = value
673
672
  r = self.testapp.patch_json("/%s" % user, reqdict, expect_errors=True)
674
673
  assert r.status_code == code
675
674
  if code == 200:
676
675
  assert r.json == dict(message="user updated")
677
676
 
678
- def create_user_fails(self, user, password, email="hello@example.com"):
679
- with pytest.raises(webtest.AppError) as excinfo:
680
- self.create_user(user, password)
681
- assert "409" in excinfo.value.args[0]
682
-
683
677
  def create_and_login_user(self, user="someuser", password="123"): # noqa: S107
684
678
  self.create_user(user, password)
685
679
  self.login(user, password)
@@ -796,11 +790,15 @@ class Mapp(MappMixin):
796
790
  r = self.testapp.get_json("/%s" % indexname)
797
791
  return r.json["result"]["mirror_whitelist"]
798
792
 
799
- def delete_project(self, project, code=200, indexname=None,
800
- waithooks=False):
793
+ def delete_project(
794
+ self, project, *,
795
+ code=200, indexname=None, force=False, waithooks=False,
796
+ ):
801
797
  indexname = self._getindexname(indexname)
802
- r = self.testapp.delete_json("/%s/%s" % (indexname,
803
- project), {}, expect_errors=True)
798
+ path = f"/{indexname}/{project}"
799
+ if force:
800
+ path = f"{path}?force"
801
+ r = self.testapp.delete_json(path, {}, expect_errors=True)
804
802
  assert r.status_code == code
805
803
  if waithooks:
806
804
  self._wait_for_serial_in_result(r)
@@ -1326,22 +1324,6 @@ def simpypi(simpypiserver):
1326
1324
  return simpypiserver.simpypi
1327
1325
 
1328
1326
 
1329
- # incremental testing
1330
-
1331
- def pytest_runtest_makereport(item, call):
1332
- if "incremental" in item.keywords:
1333
- if call.excinfo is not None:
1334
- parent = item.parent
1335
- parent._previousfailed = item
1336
-
1337
-
1338
- def pytest_runtest_setup(item):
1339
- if "incremental" in item.keywords:
1340
- previousfailed = getattr(item.parent, "_previousfailed", None)
1341
- if previousfailed is not None:
1342
- pytest.xfail("previous test failed (%s)" %previousfailed.name)
1343
-
1344
-
1345
1327
  @pytest.fixture
1346
1328
  def gen():
1347
1329
  return Gen()
@@ -72,10 +72,10 @@ def test_execnet_opcodes():
72
72
  def test_loads(data, expected):
73
73
  result = loads(data)
74
74
  assert result == expected
75
- assert type(result) == type(expected)
75
+ assert type(result) is type(expected)
76
76
  result = _loads(data)
77
77
  assert result == expected
78
- assert type(result) == type(expected)
78
+ assert type(result) is type(expected)
79
79
  # try round-trip
80
80
  dump = dumps(expected)
81
81
  assert len(dump) == dumplen(expected)