devpi-server 6.19.2__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 (141) hide show
  1. {devpi_server-6.19.2 → devpi_server-6.20.0}/.flake8 +0 -1
  2. {devpi_server-6.19.2 → devpi_server-6.20.0}/CHANGELOG +27 -0
  3. devpi_server-6.20.0/CHANGELOG.short.rst +97 -0
  4. {devpi_server-6.19.2 → devpi_server-6.20.0}/PKG-INFO +28 -80
  5. devpi_server-6.20.0/devpi_server/__init__.py +1 -0
  6. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/config.py +13 -11
  7. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/filestore.py +1 -1
  8. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/filestore_db.py +5 -0
  9. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/filestore_hash_hl.py +12 -3
  10. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/importexport.py +84 -26
  11. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/keyfs.py +7 -1
  12. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/keyfs_sqlite.py +6 -0
  13. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/mirror.py +57 -17
  14. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/model.py +30 -5
  15. devpi_server-6.20.0/devpi_server/proxy.py +46 -0
  16. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/replica.py +6 -39
  17. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/views.py +86 -13
  18. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server.egg-info/PKG-INFO +28 -80
  19. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server.egg-info/SOURCES.txt +5 -0
  20. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/functional.py +0 -20
  21. devpi_server-6.20.0/test_devpi_server/importexportdata/dashes_v1/dataindex.json +69 -0
  22. devpi_server-6.20.0/test_devpi_server/importexportdata/no_history_log/dataindex.json +83 -0
  23. devpi_server-6.20.0/test_devpi_server/importexportdata/removedindexplugin/user/dev/pkg/pkg-1.0.tar.gz +1 -0
  24. devpi_server-6.20.0/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/0.9/hello-0.9.tar.gz +1 -0
  25. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/plugin.py +80 -34
  26. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_importexport.py +112 -137
  27. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_keyfs.py +17 -0
  28. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_mirror.py +40 -2
  29. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_model.py +19 -0
  30. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_streaming.py +24 -8
  31. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_streaming_nginx.py +4 -2
  32. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_streaming_replica.py +4 -2
  33. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_streaming_replica_nginx.py +4 -2
  34. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_views.py +35 -0
  35. {devpi_server-6.19.2 → devpi_server-6.20.0}/tox.ini +1 -1
  36. devpi_server-6.19.2/CHANGELOG.short.rst +0 -149
  37. devpi_server-6.19.2/devpi_server/__init__.py +0 -1
  38. {devpi_server-6.19.2 → devpi_server-6.20.0}/LICENSE +0 -0
  39. {devpi_server-6.19.2 → devpi_server-6.20.0}/MANIFEST.in +0 -0
  40. {devpi_server-6.19.2 → devpi_server-6.20.0}/README.rst +0 -0
  41. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/__main__.py +0 -0
  42. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/auth.py +0 -0
  43. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/auth_basic.py +0 -0
  44. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/auth_devpi.py +0 -0
  45. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/__init__.py +0 -0
  46. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/crontab.template +0 -0
  47. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/devpi.service.template +0 -0
  48. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/launchd-macos.txt.template +0 -0
  49. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi-caching-http.conf.template +0 -0
  50. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi-caching-location.conf.template +0 -0
  51. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi-caching-proxy.conf.template +0 -0
  52. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi-caching-server.conf.template +0 -0
  53. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/nginx-devpi.conf.template +0 -0
  54. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/supervisor-devpi.conf.template +0 -0
  55. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/supervisord.conf.template +0 -0
  56. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/cfg/windows-service.txt.template +0 -0
  57. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/compat.py +0 -0
  58. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/exceptions.py +0 -0
  59. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/filestore_fs.py +0 -0
  60. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/filestore_fs_base.py +0 -0
  61. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/fileutil.py +0 -0
  62. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/fsck.py +0 -0
  63. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/genconfig.py +0 -0
  64. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/hookspecs.py +0 -0
  65. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/htmlpage.py +0 -0
  66. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/httpclient.py +0 -0
  67. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/init.py +0 -0
  68. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/interfaces.py +0 -0
  69. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/keyfs_sqlite_fs.py +0 -0
  70. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/keyfs_types.py +0 -0
  71. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/log.py +0 -0
  72. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/main.py +0 -0
  73. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/markers.py +0 -0
  74. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/middleware.py +0 -0
  75. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/mythread.py +0 -0
  76. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/normalized.py +0 -0
  77. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/passwd.py +0 -0
  78. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/py.typed +0 -0
  79. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/readonly.py +0 -0
  80. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/sizeof.py +0 -0
  81. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server/view_auth.py +0 -0
  82. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server.egg-info/dependency_links.txt +0 -0
  83. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server.egg-info/entry_points.txt +0 -0
  84. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server.egg-info/requires.txt +0 -0
  85. {devpi_server-6.19.2 → devpi_server-6.20.0}/devpi_server.egg-info/top_level.txt +0 -0
  86. {devpi_server-6.19.2 → devpi_server-6.20.0}/mypy.ini +0 -0
  87. {devpi_server-6.19.2 → devpi_server-6.20.0}/pyproject.toml +0 -0
  88. {devpi_server-6.19.2 → devpi_server-6.20.0}/pytest_devpi_server/__init__.py +0 -0
  89. {devpi_server-6.19.2 → devpi_server-6.20.0}/setup.cfg +0 -0
  90. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/__init__.py +0 -0
  91. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/conftest.py +0 -0
  92. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/example.py +0 -0
  93. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/badindexname/dataindex.json +0 -0
  94. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/badusername/dataindex.json +0 -0
  95. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/basescycle/dataindex.json +0 -0
  96. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/createdmodified/dataindex.json +0 -0
  97. /devpi_server-6.19.2/test_devpi_server/importexportdata/mirrordata/root/pypi/dddttt/0.1.dev1/dddttt-0.1.dev1.tar.gz → /devpi_server-6.20.0/test_devpi_server/importexportdata/dashes_v1/user1/dev/hello/hello-1.2_3.tar.gz +0 -0
  98. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/deletedbase/dataindex.json +0 -0
  99. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/mirrordata/dataindex.json +0 -0
  100. /devpi_server-6.19.2/test_devpi_server/importexportdata/normalization/root/dev/hello.pkg/hello.pkg-1.0.tar.gz → /devpi_server-6.20.0/test_devpi_server/importexportdata/mirrordata/root/pypi/dddttt/0.1.dev1/dddttt-0.1.dev1.tar.gz +0 -0
  101. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/modifiedpypi/dataindex.json +0 -0
  102. /devpi_server-6.19.2/test_devpi_server/importexportdata/removedindexplugin/user/dev/pkg/pkg-1.0.tar.gz → /devpi_server-6.20.0/test_devpi_server/importexportdata/no_history_log/user1/dev/hello/hello-1.0.tar.gz +0 -0
  103. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/nocreatedmodified/dataindex.json +0 -0
  104. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization/dataindex.json +0 -0
  105. /devpi_server-6.19.2/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/0.9/hello-0.9.tar.gz → /devpi_server-6.20.0/test_devpi_server/importexportdata/normalization/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  106. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization_merge/dataindex.json +0 -0
  107. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello-pkg/hello.pkg-1.1.tar.gz +0 -0
  108. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  109. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/norootpypi/dataindex.json +0 -0
  110. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/nouser/dataindex.json +0 -0
  111. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/removedindexplugin/dataindex.json +0 -0
  112. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/dataindex.json +0 -0
  113. {devpi_server-6.19.2 → 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
  114. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/importexportdata/toxresult_upload_default/dataindex.json +0 -0
  115. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/py.typed +0 -0
  116. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/reqmock.py +0 -0
  117. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/simpypi.py +0 -0
  118. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_auth.py +0 -0
  119. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_authcheck.py +0 -0
  120. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_config.py +0 -0
  121. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_conftest.py +0 -0
  122. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_filestore.py +0 -0
  123. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_filestore_fs.py +0 -0
  124. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_fileutil.py +0 -0
  125. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_fsck.py +0 -0
  126. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_genconfig.py +0 -0
  127. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_log.py +0 -0
  128. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_main.py +0 -0
  129. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_mirror_no_project_list.py +0 -0
  130. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_mythread.py +0 -0
  131. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_nginx.py +0 -0
  132. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_nginx_replica.py +0 -0
  133. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_permissions.py +0 -0
  134. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_readonly.py +0 -0
  135. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_replica.py +0 -0
  136. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_replica_functional.py +0 -0
  137. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_stage_customizer.py +0 -0
  138. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_view_auth.py +0 -0
  139. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_views_patch.py +0 -0
  140. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_views_push_external.py +0 -0
  141. {devpi_server-6.19.2 → devpi_server-6.20.0}/test_devpi_server/test_views_status.py +0 -0
@@ -4,5 +4,4 @@ ignore = E203,E501,E704,E741,W503
4
4
  per-file-ignores =
5
5
  plugin.py:E127,E128,E231
6
6
  model.py:E128,E231
7
- test_importexport.py:E121
8
7
  test*.py:E126,E127,E128,E225,E226,E231,E251
@@ -2,6 +2,33 @@
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
+
19
+ 6.19.3 (2026-04-13)
20
+ ===================
21
+
22
+ Bug Fixes
23
+ ---------
24
+
25
+ - Fix #1112: Parse simple JSON reply even with wrong content-type in reply if the body seems to contain JSON.
26
+
27
+ - Return stale project list for mirrors when the lock can't be acquired within the timeout.
28
+
29
+ - Fix importing of toxresults from devpi-server 6.5.0 to 6.9.0 where the wrong hash was stored.
30
+
31
+
5
32
  6.19.2 (2026-03-17)
6
33
  ===================
7
34
 
@@ -0,0 +1,97 @@
1
+
2
+
3
+ =========
4
+ Changelog
5
+ =========
6
+
7
+
8
+
9
+
10
+ .. towncrier release notes start
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
+
26
+ 6.19.3 (2026-04-13)
27
+ ===================
28
+
29
+ Bug Fixes
30
+ ---------
31
+
32
+ - Fix #1112: Parse simple JSON reply even with wrong content-type in reply if the body seems to contain JSON.
33
+
34
+ - Return stale project list for mirrors when the lock can't be acquired within the timeout.
35
+
36
+ - Fix importing of toxresults from devpi-server 6.5.0 to 6.9.0 where the wrong hash was stored.
37
+
38
+
39
+ 6.19.2 (2026-03-17)
40
+ ===================
41
+
42
+ Bug Fixes
43
+ ---------
44
+
45
+ - Preserve log for documentation uploads in export.
46
+
47
+ - Any missing file on mirrors will be ignored during event processing as is already the case in other places.
48
+
49
+ - Use short timeout when project list is requested for ``has_project`` call on mirrors instead of the long one used for ``list_projects``. This prevents installers from timing out and retrying several times.
50
+
51
+ - Fix error handling for proxy requests from replica to primary.
52
+
53
+ Other Changes
54
+ -------------
55
+
56
+ - Removed limit of reported missing files for devpi-fsck.
57
+
58
+
59
+ 6.19.1 (2026-02-09)
60
+ ===================
61
+
62
+ Bug Fixes
63
+ ---------
64
+
65
+ - Pin setuptools as pyramid still requires pkg_resources.
66
+
67
+ - Always allow replicas to access deleted releases to get the proper ``410 Gone`` instead of ``403 Forbidden`` when ``devpi-lockdown`` is in use.
68
+
69
+
70
+ 6.19.0 (2026-02-06)
71
+ ===================
72
+
73
+ Features
74
+ --------
75
+
76
+ - Add ``--autocreate-users`` server option.
77
+ Automatically creates users that don't exist in devpi, but have successfully authenticated via an authentication plugin.
78
+ A typical example of when to enable this would be when authenticating via an LDAP directory.
79
+ Automatically created users do not have passwords, and have no password hash to prevent local authentication.
80
+
81
+ - Add ``replica-files-in-sync-at``, ``replica-init-queue-finished-at`` and ``replica-metadata-in-sync-at`` to status view, the existing ``replica-in-sync-at`` is now a combination of all three instead of just metadata.
82
+
83
+ - Warn when an unknown option is found in config file to detect typos. Be aware that some commands don't use all the options, that is why this only warns instead of exiting.
84
+
85
+ - Add new ``devpiserver_user_created`` hook which can be used to create default indexes or other setup for newly created users.
86
+
87
+ Bug Fixes
88
+ ---------
89
+
90
+ - Fix ``+status`` json encoding errors by making sure the ``FatalResponse.url`` attribute is a string.
91
+
92
+ - Ignore existing unknown index options from uninstalled plugins when patching other options with ``+=`` and ``-=``.
93
+
94
+ - Fix removal with ``-=`` of index options with default values from ``devpiserver_indexconfig_defaults`` hooks.
95
+
96
+ - Fix #1110: a list for the ``listen`` option in a config file stopped working in 6.18.0.
97
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devpi-server
3
- Version: 6.19.2
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,33 @@ 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
+
138
+ 6.19.3 (2026-04-13)
139
+ ===================
140
+
141
+ Bug Fixes
142
+ ---------
143
+
144
+ - Fix #1112: Parse simple JSON reply even with wrong content-type in reply if the body seems to contain JSON.
145
+
146
+ - Return stale project list for mirrors when the lock can't be acquired within the timeout.
147
+
148
+ - Fix importing of toxresults from devpi-server 6.5.0 to 6.9.0 where the wrong hash was stored.
149
+
150
+
124
151
  6.19.2 (2026-03-17)
125
152
  ===================
126
153
 
@@ -180,82 +207,3 @@ Bug Fixes
180
207
 
181
208
  - Fix #1110: a list for the ``listen`` option in a config file stopped working in 6.18.0.
182
209
 
183
-
184
- 6.18.0 (2026-01-27)
185
- ===================
186
-
187
- Features
188
- --------
189
-
190
- - Store all available hashes of files.
191
-
192
- - Validate hashes of all files during devpi-import, not only releases.
193
-
194
- Bug Fixes
195
- ---------
196
-
197
- - Apply argparse transformations on values read from config file or environment.
198
-
199
- - Restore Python and platform info in user agent string after switch to httpx.
200
-
201
- - Remove all database entries on project deletion instead of only emptying them.
202
-
203
- - Fix error at end of replica streaming caused by changed behavior from switch to httpx.
204
-
205
- - Fix #1102: The data stream was cut off after 64k when proxying from replica to primary after switching to httpx.
206
-
207
- - Fix #1107: retry file downloads if there has been an error during download.
208
-
209
- Other Changes
210
- -------------
211
-
212
- - 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.
213
-
214
-
215
- 6.17.0 (2025-08-27)
216
- ===================
217
-
218
- Deprecations and Removals
219
- -------------------------
220
-
221
- - Dropped support for migrating old password hashes that were replaced in devpi-server 4.2.0.
222
-
223
- - Removed support for basic authorization in primary URL. The connection is already secured by a bearer token header.
224
-
225
- - Removed the experimental ``--replica-cert`` option. The replica is already using a token via a shared secret, so this is redundant.
226
-
227
- - Removed ``--replica-max-retries`` option. It wasn't implemented for async_httpget and didn't work correctly when streaming data.
228
-
229
- Features
230
- --------
231
-
232
- - Use httpx for all data fetching for mirrors and fetch projects list asynchronously to allow update in background even after a timeout.
233
-
234
- - Use httpx instead of requests when proxying from replicas to primary.
235
-
236
- - Use httpx for all requests from replicas to primary.
237
-
238
- - Use httpx when pushing releases to external index.
239
-
240
- - Added ``mirror_ignore_serial_header`` mirror index option, which allows switching from PyPI to a mirror without serials header when set to ``True``, otherwise only stale links will be served and no updates be stored.
241
-
242
- - The HTTP cache information for mirrored projects is persisted and re-used on server restarts.
243
-
244
- - Added ``--file-replication-skip-indexes`` option to skip file replication for ``all``, by index type (i.e. ``mirror``) or index name (i.e. ``root/pypi``).
245
-
246
- Bug Fixes
247
- ---------
248
-
249
- - Correctly handle lists for ``Provides-Extra`` and ``License-File`` metadata in database.
250
-
251
- - Fix traceback by returning 401 error code when using wrong password with a user that was created using an authentication plugin like devpi-ldap which passes authentication through in that case.
252
-
253
- - Fix #1053: allow users to update their passwords when ``--restrict-modify`` is used.
254
-
255
- - Fix #1097: return 404 when trying to POST to +simple.
256
-
257
- Other Changes
258
- -------------
259
-
260
- - Changed User-Agent when fetching data for mirrors from just "server" to "devpi-server".
261
-
@@ -0,0 +1 @@
1
+ __version__ = "6.20.0"
@@ -4,6 +4,7 @@ from . import fileutil
4
4
  from . import hookspecs
5
5
  from .interfaces import IIOFileFactory
6
6
  from .log import threadlog
7
+ from copy import deepcopy
7
8
  from devpi_common.types import cached_property
8
9
  from devpi_common.url import URL
9
10
  from operator import itemgetter
@@ -244,6 +245,12 @@ def add_web_options(
244
245
  help="use absolute URLs everywhere. "
245
246
  "This will become the default at some point.")
246
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
+
247
254
  parser.addoption(
248
255
  "--profile-requests", type=int, metavar="NUM", default=0,
249
256
  help="profile NUM requests and print out cumulative stats. "
@@ -776,16 +783,11 @@ def get_io_file_factory(storage_info: dict) -> IIOFileFactory:
776
783
  _io_file_factory: Callable
777
784
  db_filestore = storage_info.setdefault("db_filestore", True)
778
785
  settings = storage_info.setdefault("settings", {})
779
- if db_filestore:
780
- from .filestore_db import DBIOFile
781
-
782
- _io_file_factory = DBIOFile
783
- else:
784
- fsbackend = settings.setdefault("fsbackend", "fs")
785
- _io_file_factory = __import__(
786
- f"filestore_{fsbackend}", globals=globals(), level=1
787
- ).fsiofile_factory
788
-
786
+ fsbackend = settings.setdefault("fsbackend", "db" if db_filestore else "fs")
787
+ _io_file_factory = __import__(
788
+ f"filestore_{fsbackend}", globals=globals(), level=1
789
+ ).fsiofile_factory
790
+ if not db_filestore:
789
791
  storage_info.setdefault("_test_markers", []).append("storage_with_filesystem")
790
792
  verifyObject(IIOFileFactory, _io_file_factory)
791
793
 
@@ -1147,7 +1149,7 @@ class Config:
1147
1149
  def _storage_info(self):
1148
1150
  name = self.storage_info["name"]
1149
1151
  settings = self.storage_info["settings"]
1150
- return self._storage_info_from_name(name, settings)
1152
+ return deepcopy(self._storage_info_from_name(name, settings))
1151
1153
 
1152
1154
  @property
1153
1155
  def io_file_factory(self) -> IIOFileFactory:
@@ -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
@@ -80,3 +80,8 @@ class DBIOFile:
80
80
 
81
81
  def _rollback(self) -> None:
82
82
  pass
83
+
84
+
85
+ @provider(IIOFileFactory)
86
+ def fsiofile_factory(conn: Any, settings: dict) -> DBIOFile:
87
+ return DBIOFile(conn, settings)
@@ -43,7 +43,7 @@ class File:
43
43
  basedir: Path = field(kw_only=True)
44
44
  file_path_info: FilePathInfo = field(kw_only=True)
45
45
  path: Path = field(kw_only=True)
46
- digest_path: Path = field(kw_only=True)
46
+ digest_path: Path | None = field(kw_only=True)
47
47
 
48
48
  def __repr__(self) -> str:
49
49
  return f"<{self.__class__.__name__} {self.path}>"
@@ -70,6 +70,7 @@ class File:
70
70
  with suppress(OSError):
71
71
  self.path.unlink()
72
72
  digest_path = self.digest_path
73
+ assert digest_path is not None
73
74
  if digest_path.exists() and digest_path.stat().st_nlink == 1:
74
75
  # if nothing else links to the digest file anymore, then remove it
75
76
  digest_path.unlink()
@@ -87,10 +88,13 @@ class DirtyFile(File):
87
88
  def commit(self) -> list[str]:
88
89
  assert tmpsuffix_for_path(self.path) is not None
89
90
  digest_path = self.digest_path
91
+ assert digest_path is not None
90
92
  if digest_path.exists():
91
93
  # additional check besides the content digest
92
94
  assert getsize(digest_path) == self.getsize()
93
95
  # link to the existing digest file
96
+ if self.dst_path.exists():
97
+ raise RuntimeError(f"{self.dst_path} -> {digest_path}")
94
98
  hardlink(digest_path, self.dst_path)
95
99
  # drop the temporary file
96
100
  self.drop()
@@ -112,8 +116,11 @@ class DirtyFile(File):
112
116
  @provider(IDirtyFileFactory, IFileFactory)
113
117
  class HashHLFactory:
114
118
  @classmethod
115
- def _make_digest_path(cls, basedir: Path, file_path_info: FilePathInfo) -> Path:
116
- assert file_path_info.hash_digest is not None
119
+ def _make_digest_path(
120
+ cls, basedir: Path, file_path_info: FilePathInfo
121
+ ) -> Path | None:
122
+ if file_path_info.hash_digest is None:
123
+ return None
117
124
  return basedir.joinpath("+h", *split_digest(file_path_info.hash_digest))
118
125
 
119
126
  @classmethod
@@ -184,6 +191,7 @@ class HashHLIOFile(FSIOFileBase):
184
191
  path.unlink()
185
192
  threadlog.warn("completed file-del from crashed tx: %s", path)
186
193
  digest_path = HashHLFactory._make_digest_path(basedir, file_path_info)
194
+ assert digest_path is not None
187
195
  with suppress(OSError):
188
196
  digest_path.unlink()
189
197
  threadlog.warn(
@@ -193,6 +201,7 @@ class HashHLIOFile(FSIOFileBase):
193
201
  dst = HashHLFactory._make_path(basedir, relpath)
194
202
  src = basedir / rel_rename
195
203
  digest_path = HashHLFactory._make_digest_path(basedir, file_path_info)
204
+ assert digest_path is not None
196
205
  if digest_path.exists() and src.exists():
197
206
  # additional check besides the content digest
198
207
  assert getsize(digest_path) == getsize(src)
@@ -9,12 +9,16 @@ from .main import Fatal
9
9
  from .main import init_default_indexes
10
10
  from .main import set_state_version
11
11
  from .main import xom_from_config
12
+ from .markers import absent
12
13
  from .model import Rel
13
14
  from .normalized import normalize_name
14
15
  from .readonly import ReadonlyView
15
16
  from .readonly import get_mutable_deepcopy
17
+ from attrs import define
18
+ from attrs import field
16
19
  from collections import defaultdict
17
20
  from devpi_common.metadata import BasenameMeta
21
+ from devpi_common.types import parse_hash_spec
18
22
  from devpi_common.url import URL
19
23
  from devpi_server import __version__ as server_version
20
24
  from devpi_server.model import get_stage_customizer_classes
@@ -350,6 +354,81 @@ class IndexDump:
350
354
  self.exporter.completed(f"{type}: {relpath} ")
351
355
 
352
356
 
357
+ @define(kw_only=True)
358
+ class Migrator:
359
+ dumpversion: int = field(converter=int)
360
+
361
+ @dumpversion.validator
362
+ def _validate_dumpversion(self, _attribute, value):
363
+ if value not in {1, 2}:
364
+ msg = f"incompatible dumpversion: {self.dumpversion}"
365
+ raise Fatal(msg)
366
+
367
+ def migrate(self, data: dict) -> dict:
368
+ data["indexes"] = {
369
+ k: self.migrate_index(v) for k, v in data.pop("indexes").items()
370
+ }
371
+ data["users"] = {k: self.migrate_user(v) for k, v in data.pop("users").items()}
372
+ return data
373
+
374
+ def migrate_file(self, data: dict) -> dict:
375
+ if self.dumpversion < 2:
376
+ # previous versions would not add a version attribute
377
+ data["version"] = BasenameMeta(Path(data["relpath"]).name).version
378
+ if "entrymapping" in data:
379
+ mapping = data["entrymapping"]
380
+ hashes = Digests(mapping.pop("hashes", {}))
381
+ # devpi-server-2.1 exported with md5 checksums
382
+ if "md5" in mapping:
383
+ hashes["md5"] = mapping.pop("md5")
384
+ # docs and toxresults didn't always have hashes stored in export dump
385
+ if "hash_spec" in mapping:
386
+ hashes.add_spec(mapping.pop("hash_spec"))
387
+ mapping["hashes"] = dict(hashes)
388
+ if "for_entrypath" in data:
389
+ self.migrate_toxresult(data)
390
+ return data
391
+
392
+ def migrate_index(self, data: dict) -> dict:
393
+ if "files" in data:
394
+ data["files"] = [self.migrate_file(v) for v in data.pop("files")]
395
+ indexconfig = data["indexconfig"]
396
+ if (
397
+ "uploadtrigger_jenkins" in indexconfig
398
+ and not indexconfig["uploadtrigger_jenkins"]
399
+ ):
400
+ # remove if not set, so if the trigger was never
401
+ # used, you don't need to install the plugin
402
+ del indexconfig["uploadtrigger_jenkins"]
403
+ if "pypi_whitelist" in indexconfig:
404
+ # this was renamed in 3.0.0
405
+ whitelist = indexconfig.pop("pypi_whitelist")
406
+ if "mirror_whitelist" not in indexconfig:
407
+ indexconfig["mirror_whitelist"] = whitelist
408
+ return data
409
+
410
+ def migrate_toxresult(self, data: dict) -> dict:
411
+ hash_type = None
412
+ hash_value = None
413
+ parts = Path(data["relpath"]).parts
414
+ if len(parts) == 5:
415
+ hash_spec = parse_hash_spec(parts[3])
416
+ if hash_spec[0] is not None:
417
+ hash_type = hash_spec[0]().name
418
+ hash_value = hash_spec[1]
419
+ if (entrymapping := data.get("entrymapping")) is not None:
420
+ hashes = entrymapping.get("hashes", {})
421
+ if hashes.get(hash_type, absent) == hash_value:
422
+ # from 6.5.0 until it was fixed in 6.9.0 the hash for toxresults
423
+ # was for the linked file, not for the contents, so we remove them
424
+ # here to prevent mismatch errors
425
+ hashes.clear()
426
+ return data
427
+
428
+ def migrate_user(self, data: dict) -> dict:
429
+ return data
430
+
431
+
353
432
  class Importer:
354
433
  import_indexes: dict[str, Any]
355
434
 
@@ -440,11 +519,10 @@ class Importer:
440
519
  def import_all(self, path: Path) -> None: # noqa: PLR0912
441
520
  self.import_rootdir = path
442
521
  json_path = path / "dataindex.json"
443
- self.import_data = self.read_json(json_path)
444
- self.dumpversion = self.import_data["dumpversion"]
445
- if self.dumpversion not in ("1", "2"):
446
- msg = f"incompatible dumpversion: {self.dumpversion!r}"
447
- raise Fatal(msg)
522
+ import_data = self.read_json(json_path)
523
+ self.import_data = Migrator(dumpversion=import_data["dumpversion"]).migrate(
524
+ import_data
525
+ )
448
526
  self.import_users = self.import_data["users"]
449
527
  self.import_indexes = self.import_data["indexes"]
450
528
  self.display_import_header(path)
@@ -495,16 +573,6 @@ class Importer:
495
573
  indexconfig = dict(import_index["indexconfig"])
496
574
  if indexconfig['type'] in self.types_to_skip:
497
575
  continue
498
- if 'uploadtrigger_jenkins' in indexconfig:
499
- if not indexconfig['uploadtrigger_jenkins']:
500
- # remove if not set, so if the trigger was never
501
- # used, you don't need to install the plugin
502
- del indexconfig['uploadtrigger_jenkins']
503
- if 'pypi_whitelist' in indexconfig:
504
- # this was renamed in 3.0.0
505
- whitelist = indexconfig.pop('pypi_whitelist')
506
- if 'mirror_whitelist' not in indexconfig:
507
- indexconfig['mirror_whitelist'] = whitelist
508
576
  username, index = stagename.split("/")
509
577
  user = self.xom.model.get_user(username)
510
578
  assert user is not None
@@ -605,22 +673,12 @@ class Importer:
605
673
  # docs and toxresults didn't always have entrymapping in export dump
606
674
  mapping = filedesc.get("entrymapping", {})
607
675
  hashes = Digests(mapping.get("hashes", {}))
608
- # devpi-server-2.1 exported with md5 checksums
609
- if "md5" in mapping:
610
- hashes["md5"] = mapping["md5"]
611
- # docs and toxresults didn't always have hashes stored in export dump
612
- if "hash_spec" in mapping:
613
- hashes.add_spec(mapping['hash_spec'])
614
676
  # note that the actual hash_type used within devpi-server is not
615
677
  # determined here but in store_releasefile/store_doczip/store_toxresult etc
616
678
  hashes.update(get_hashes(f, hash_types=hashes.get_missing_hash_types()))
617
679
 
618
680
  if filedesc["type"] == Rel.ReleaseFile:
619
- if self.dumpversion == "1":
620
- # previous versions would not add a version attribute
621
- version = BasenameMeta(p.name).version
622
- else:
623
- version = filedesc["version"]
681
+ version = filedesc["version"]
624
682
 
625
683
  if hasattr(stage, 'store_releasefile'):
626
684
  stage = cast("PrivateStage", stage)
@@ -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
@@ -742,6 +745,9 @@ class TransactionRootModel(RootModel):
742
745
  del self.model_cache[key]
743
746
  super().delete_stage(username, index)
744
747
 
748
+ def get_index(self, user: str, index: str | None = None) -> BaseStage | None:
749
+ return self.getstage(user, index)
750
+
745
751
  def get_user(self, name):
746
752
  if name not in self.model_cache:
747
753
  self.model_cache[name] = super().get_user(name)
@@ -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(