devpi-server 6.10.0__tar.gz → 6.11.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 (134) hide show
  1. {devpi-server-6.10.0 → devpi_server-6.11.0}/CHANGELOG +22 -1
  2. {devpi-server-6.10.0 → devpi_server-6.11.0}/CHANGELOG.short.rst +21 -19
  3. {devpi-server-6.10.0 → devpi_server-6.11.0}/PKG-INFO +23 -21
  4. devpi_server-6.11.0/devpi_server/__init__.py +1 -0
  5. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/auth.py +4 -2
  6. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/config.py +1 -1
  7. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/filestore.py +39 -21
  8. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/filestore_fs.py +1 -6
  9. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/fsck.py +7 -0
  10. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/hookspecs.py +12 -0
  11. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/importexport.py +19 -6
  12. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/keyfs.py +45 -57
  13. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/keyfs_sqlite.py +5 -6
  14. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/keyfs_sqlite_fs.py +1 -6
  15. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/main.py +4 -1
  16. devpi_server-6.11.0/devpi_server/markers.py +14 -0
  17. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/middleware.py +0 -2
  18. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/mirror.py +46 -10
  19. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/model.py +40 -22
  20. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/replica.py +71 -81
  21. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/views.py +139 -150
  22. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server.egg-info/PKG-INFO +23 -21
  23. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server.egg-info/SOURCES.txt +1 -0
  24. {devpi-server-6.10.0 → devpi_server-6.11.0}/setup.py +1 -1
  25. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/mirrordata/dataindex.json +1 -1
  26. devpi_server-6.11.0/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/0.9/hello-0.9.tar.gz +1 -0
  27. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/plugin.py +11 -22
  28. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/reqmock.py +16 -9
  29. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/simpypi.py +4 -4
  30. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_auth.py +3 -3
  31. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_config.py +9 -7
  32. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_filestore.py +30 -28
  33. devpi_server-6.11.0/test_devpi_server/test_fsck.py +41 -0
  34. devpi_server-6.11.0/test_devpi_server/test_genconfig.py +50 -0
  35. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_importexport.py +88 -68
  36. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_keyfs.py +11 -1
  37. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_main.py +1 -1
  38. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_mirror.py +32 -4
  39. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_model.py +29 -45
  40. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_nginx.py +4 -1
  41. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_permissions.py +8 -6
  42. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_replica.py +45 -33
  43. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_stage_customizer.py +8 -8
  44. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_streaming.py +1 -0
  45. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_view_auth.py +10 -8
  46. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_views.py +19 -24
  47. {devpi-server-6.10.0 → devpi_server-6.11.0}/tox.ini +7 -2
  48. devpi-server-6.10.0/devpi_server/__init__.py +0 -1
  49. devpi-server-6.10.0/devpi_server/markers.py +0 -6
  50. devpi-server-6.10.0/test_devpi_server/importexportdata/mirrordata/root/pypi/dddttt/0.1.dev1/dddttt-0.1.dev1.tar.gz +0 -0
  51. devpi-server-6.10.0/test_devpi_server/test_genconfig.py +0 -19
  52. {devpi-server-6.10.0 → devpi_server-6.11.0}/.flake8 +0 -0
  53. {devpi-server-6.10.0 → devpi_server-6.11.0}/AUTHORS +0 -0
  54. {devpi-server-6.10.0 → devpi_server-6.11.0}/LICENSE +0 -0
  55. {devpi-server-6.10.0 → devpi_server-6.11.0}/MANIFEST.in +0 -0
  56. {devpi-server-6.10.0 → devpi_server-6.11.0}/README.rst +0 -0
  57. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/__main__.py +0 -0
  58. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/auth_basic.py +0 -0
  59. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/auth_devpi.py +0 -0
  60. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/__init__.py +0 -0
  61. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/crontab.template +0 -0
  62. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/devpi.service.template +0 -0
  63. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/launchd-macos.txt.template +0 -0
  64. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/nginx-devpi-caching-http.conf.template +0 -0
  65. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/nginx-devpi-caching-location.conf.template +0 -0
  66. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/nginx-devpi-caching-proxy.conf.template +0 -0
  67. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/nginx-devpi-caching-server.conf.template +0 -0
  68. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/nginx-devpi.conf.template +0 -0
  69. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/supervisor-devpi.conf.template +0 -0
  70. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/supervisord.conf.template +0 -0
  71. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/cfg/windows-service.txt.template +0 -0
  72. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/exceptions.py +0 -0
  73. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/fileutil.py +0 -0
  74. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/genconfig.py +0 -0
  75. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/init.py +0 -0
  76. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/interfaces.py +0 -0
  77. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/keyfs_types.py +0 -0
  78. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/log.py +0 -0
  79. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/mythread.py +0 -0
  80. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/passwd.py +0 -0
  81. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/readonly.py +0 -0
  82. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/sizeof.py +0 -0
  83. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/vendor/__init__.py +0 -0
  84. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/vendor/_pip.py +0 -0
  85. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server/view_auth.py +0 -0
  86. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server.egg-info/dependency_links.txt +0 -0
  87. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server.egg-info/entry_points.txt +0 -0
  88. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server.egg-info/not-zip-safe +0 -0
  89. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server.egg-info/requires.txt +0 -0
  90. {devpi-server-6.10.0 → devpi_server-6.11.0}/devpi_server.egg-info/top_level.txt +0 -0
  91. {devpi-server-6.10.0 → devpi_server-6.11.0}/pyproject.toml +0 -0
  92. {devpi-server-6.10.0 → devpi_server-6.11.0}/pytest_devpi_server/__init__.py +0 -0
  93. {devpi-server-6.10.0 → devpi_server-6.11.0}/setup.cfg +0 -0
  94. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/__init__.py +0 -0
  95. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/conftest.py +0 -0
  96. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/example.py +0 -0
  97. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/functional.py +0 -0
  98. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/badindexname/dataindex.json +0 -0
  99. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/badusername/dataindex.json +0 -0
  100. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/basescycle/dataindex.json +0 -0
  101. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/createdmodified/dataindex.json +0 -0
  102. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/deletedbase/dataindex.json +0 -0
  103. /devpi-server-6.10.0/test_devpi_server/importexportdata/normalization/root/dev/hello.pkg/hello.pkg-1.0.tar.gz → /devpi_server-6.11.0/test_devpi_server/importexportdata/mirrordata/root/pypi/dddttt/0.1.dev1/dddttt-0.1.dev1.tar.gz +0 -0
  104. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/modifiedpypi/dataindex.json +0 -0
  105. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/nocreatedmodified/dataindex.json +0 -0
  106. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/normalization/dataindex.json +0 -0
  107. /devpi-server-6.10.0/test_devpi_server/importexportdata/removedindexplugin/user/dev/pkg/pkg-1.0.tar.gz → /devpi_server-6.11.0/test_devpi_server/importexportdata/normalization/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  108. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/normalization_merge/dataindex.json +0 -0
  109. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello-pkg/hello.pkg-1.1.tar.gz +0 -0
  110. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  111. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/norootpypi/dataindex.json +0 -0
  112. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/nouser/dataindex.json +0 -0
  113. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/removedindexplugin/dataindex.json +0 -0
  114. /devpi-server-6.10.0/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/0.9/hello-0.9.tar.gz → /devpi_server-6.11.0/test_devpi_server/importexportdata/removedindexplugin/user/dev/pkg/pkg-1.0.tar.gz +0 -0
  115. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/dataindex.json +0 -0
  116. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/sha256=ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73/hello-0.9.tar.gz.toxresult0 +0 -0
  117. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/importexportdata/toxresult_upload_default/dataindex.json +0 -0
  118. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_authcheck.py +0 -0
  119. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_conftest.py +0 -0
  120. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_fileutil.py +0 -0
  121. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_keyfs_sqlite_fs.py +0 -0
  122. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_log.py +0 -0
  123. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_mirror_no_project_list.py +0 -0
  124. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_mythread.py +0 -0
  125. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_nginx_replica.py +0 -0
  126. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_readonly.py +0 -0
  127. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_replica_functional.py +0 -0
  128. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_reqmock.py +0 -0
  129. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_streaming_nginx.py +0 -0
  130. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_streaming_replica.py +0 -0
  131. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_streaming_replica_nginx.py +0 -0
  132. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_views_patch.py +0 -0
  133. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_views_push_external.py +0 -0
  134. {devpi-server-6.10.0 → devpi_server-6.11.0}/test_devpi_server/test_views_status.py +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  .. towncrier release notes start
4
4
 
5
+ 6.11.0 (2024-04-20)
6
+ ===================
7
+
8
+ Features
9
+ --------
10
+
11
+ - The ``devpi-fsck`` script now returns an error code when there have been missing files or checksum errors.
12
+
13
+ - Fix #983: Add plugin hook for mirror authentication header.
14
+
15
+
16
+
17
+ Bug Fixes
18
+ ---------
19
+
20
+ - Preserve last modified of docs and toxresults during export/import.
21
+
22
+ - Fix #1033: Use ``int`` for ``--mirror-cache-expiry`` to fix value of ``proxy_cache_valid`` in nginx caching config.
23
+
24
+
25
+
5
26
  6.10.0 (2023-12-19)
6
27
  ===================
7
28
 
@@ -10,7 +31,7 @@ Features
10
31
 
11
32
  - Use ``Authorization`` header instead of adding username/password to URL when fetching from mirror.
12
33
 
13
- - Fix #993: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
34
+ - Fix #998: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
14
35
 
15
36
 
16
37
 
@@ -9,6 +9,27 @@ Changelog
9
9
 
10
10
  .. towncrier release notes start
11
11
 
12
+ 6.10.0 (2023-12-19)
13
+ ===================
14
+
15
+ Features
16
+ --------
17
+
18
+ - Use ``Authorization`` header instead of adding username/password to URL when fetching from mirror.
19
+
20
+ - Fix #998: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
21
+
22
+
23
+
24
+ Bug Fixes
25
+ ---------
26
+
27
+ - Fix #996: support hashes other than sha256 in application/vnd.pypi.simple.v1+json responses.
28
+
29
+ - Only compare hostname instead of full URL prefix when parsing mirror packages to fix mirrors with basic authentication and absolute URLs. See #1006
30
+
31
+
32
+
12
33
  6.9.2 (2023-08-06)
13
34
  ==================
14
35
 
@@ -72,22 +93,3 @@ Bug Fixes
72
93
 
73
94
  - Add locking to mirror name cache to prevent race condition on updates.
74
95
 
75
-
76
- 6.7.0 (2022-09-28)
77
- ==================
78
-
79
- Features
80
- --------
81
-
82
- - Add nginx example to ``devpi-gen-config`` with caching of simple pages for installers like pip.
83
-
84
- - Automatically check for ``+files`` when using ``--replica-file-search-path``.
85
-
86
- - Set headers to prevent caching for simple links with stale results.
87
-
88
-
89
- Bug Fixes
90
- ---------
91
-
92
- - Fix #840: Correct url scheme in config if nginx is behind another proxy.
93
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-server
3
- Version: 6.10.0
3
+ Version: 6.11.0
4
4
  Summary: devpi-server: reliable private and pypi.org caching server
5
5
  Home-page: https://devpi.net
6
6
  Maintainer: Florian Schulze
@@ -119,6 +119,27 @@ Changelog
119
119
 
120
120
  .. towncrier release notes start
121
121
 
122
+ 6.11.0 (2024-04-20)
123
+ ===================
124
+
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
+ Bug Fixes
135
+ ---------
136
+
137
+ - Preserve last modified of docs and toxresults during export/import.
138
+
139
+ - Fix #1033: Use ``int`` for ``--mirror-cache-expiry`` to fix value of ``proxy_cache_valid`` in nginx caching config.
140
+
141
+
142
+
122
143
  6.10.0 (2023-12-19)
123
144
  ===================
124
145
 
@@ -127,7 +148,7 @@ Features
127
148
 
128
149
  - Use ``Authorization`` header instead of adding username/password to URL when fetching from mirror.
129
150
 
130
- - Fix #993: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
151
+ - Fix #998: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
131
152
 
132
153
 
133
154
 
@@ -184,22 +205,3 @@ Bug Fixes
184
205
 
185
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.
186
207
 
187
-
188
- 6.8.0 (2022-12-05)
189
- ==================
190
-
191
- Features
192
- --------
193
-
194
- - Fix #929: Cache normalized project names per transaction on mirror index instances.
195
-
196
-
197
- Bug Fixes
198
- ---------
199
-
200
- - Fix #914: add locking to list_projects_perstage of mirror indexes to prevent multiple slow concurrent updates of the full project name list.
201
-
202
- - Catch exceptions in async_httpget analog to httpget.
203
-
204
- - Add locking to mirror name cache to prevent race condition on updates.
205
-
@@ -0,0 +1 @@
1
+ __version__ = '6.11.0'
@@ -1,5 +1,6 @@
1
1
  import base64
2
2
  import hashlib
3
+ import itertools
3
4
  import itsdangerous
4
5
  import secrets
5
6
  from .log import threadlog
@@ -53,7 +54,8 @@ class Auth:
53
54
  # one of the plugins returned valid userinfo
54
55
  # return union of all groups which may be contained in that info
55
56
  groups = (ui.get('groups', []) for ui in userinfo_list)
56
- return dict(status="ok", groups=sorted(set(sum(groups, []))))
57
+ return dict(status="ok", groups=sorted(
58
+ set(itertools.chain.from_iterable(groups))))
57
59
 
58
60
  def _validate(self, authuser, authpassword, request=None):
59
61
  """ Validates user credentials.
@@ -131,7 +133,7 @@ class Auth:
131
133
  username,
132
134
  result.get("groups", []),
133
135
  result.get("from_user_object", False)])
134
- assert not isinstance(pseudopass, str) or pseudopass.encode('ascii')
136
+ assert not isinstance(pseudopass, str) or pseudopass.encode('ascii') # type: ignore[attr-defined]
135
137
  return {"password": pseudopass,
136
138
  "expiration": self.LOGIN_EXPIRATION}
137
139
 
@@ -179,7 +179,7 @@ def add_web_options(parser, pluginmanager):
179
179
 
180
180
  def add_mirror_options(parser, pluginmanager):
181
181
  parser.addoption(
182
- "--mirror-cache-expiry", type=float, metavar="SECS",
182
+ "--mirror-cache-expiry", type=int, metavar="SECS",
183
183
  default=DEFAULT_MIRROR_CACHE_EXPIRY,
184
184
  help="(experimental) time after which projects in mirror indexes "
185
185
  "are checked for new releases.")
@@ -10,9 +10,14 @@ import re
10
10
  from devpi_common.metadata import splitbasename
11
11
  from devpi_common.types import parse_hash_spec
12
12
  from devpi_server.log import threadlog
13
+ from inspect import currentframe
13
14
  from urllib.parse import unquote
14
15
 
15
16
 
17
+ class ChecksumError(ValueError):
18
+ pass
19
+
20
+
16
21
  _nodefault = object()
17
22
 
18
23
 
@@ -38,23 +43,37 @@ def get_file_hash(fp, hash_type):
38
43
  return running_hash.hexdigest()
39
44
 
40
45
 
46
+ def get_seekable_content_or_file(content_or_file):
47
+ if isinstance(content_or_file, bytes):
48
+ return content_or_file
49
+ seekable_method = getattr(content_or_file, "seekable", None)
50
+ seekable = seekable_method() if callable(seekable_method) else False
51
+ if not seekable:
52
+ content_or_file = content_or_file.read()
53
+ if len(content_or_file) > 1048576:
54
+ frame = currentframe()
55
+ if frame is not None and frame.f_back is not None:
56
+ frame = frame.f_back
57
+ if frame is None:
58
+ f_name = "get_seekable_content_or_file"
59
+ else:
60
+ f_name = frame.f_code.co_name
61
+ threadlog.warn(
62
+ "Read %.1f megabytes into memory in %s",
63
+ len(content_or_file) / 1048576, f_name)
64
+ return content_or_file
65
+
66
+
41
67
  def get_hash_spec(content_or_file, hash_type):
42
68
  if not isinstance(content_or_file, bytes):
43
- if content_or_file.seekable():
44
- content_or_file.seek(0)
45
- hexdigest = get_file_hash(
46
- content_or_file, hash_type)
47
- content_or_file.seek(0)
48
- return f"{hash_type}={hexdigest}"
49
- else:
50
- content_or_file = content_or_file.read()
51
- if len(content_or_file) > 1048576:
52
- threadlog.warn(
53
- "Read %.1f megabytes into memory in get_default_hash_spec",
54
- len(content_or_file) / 1048576)
55
- if isinstance(content_or_file, bytes):
56
- running_hash = getattr(hashlib, hash_type)(content_or_file)
57
- return f"{running_hash.name}={running_hash.hexdigest()}"
69
+ assert content_or_file.seekable()
70
+ content_or_file.seek(0)
71
+ hexdigest = get_file_hash(
72
+ content_or_file, hash_type)
73
+ content_or_file.seek(0)
74
+ return f"{hash_type}={hexdigest}"
75
+ running_hash = getattr(hashlib, hash_type)(content_or_file)
76
+ return f"{running_hash.name}={running_hash.hexdigest()}"
58
77
 
59
78
 
60
79
  def make_splitdir(hash_spec):
@@ -150,8 +169,7 @@ class FileStore:
150
169
 
151
170
  def metaprop(name):
152
171
  def fget(self):
153
- if self.meta is not None:
154
- return self.meta.get(name)
172
+ return None if self.meta is None else self.meta.get(name)
155
173
 
156
174
  def fset(self, val):
157
175
  val = unicode_if_bytes(val)
@@ -251,7 +269,7 @@ class FileEntry(object):
251
269
  def file_os_path(self):
252
270
  return self.tx.conn.io_file_os_path(self._storepath)
253
271
 
254
- def file_set_content(self, content_or_file, last_modified=None, hash_spec=None):
272
+ def file_set_content(self, content_or_file, *, last_modified=None, hash_spec=None):
255
273
  if last_modified != -1:
256
274
  if last_modified is None:
257
275
  last_modified = unicode_if_bytes(format_date_time(None))
@@ -294,7 +312,7 @@ class FileEntry(object):
294
312
  self.file_delete()
295
313
 
296
314
  def has_existing_metadata(self):
297
- return self.hash_spec and self.last_modified
315
+ return bool(self.hash_spec and self.last_modified)
298
316
 
299
317
 
300
318
  def get_checksum_error(content_or_hash, relpath, hash_spec):
@@ -306,12 +324,12 @@ def get_checksum_error(content_or_hash, relpath, hash_spec):
306
324
  if callable(hexdigest):
307
325
  hexdigest = hexdigest()
308
326
  if content_or_hash.name != hash_type:
309
- return ValueError(
327
+ return ChecksumError(
310
328
  f"{relpath}: hash type mismatch, "
311
329
  f"got {content_or_hash.name}, expected {hash_type}")
312
330
  else:
313
331
  hexdigest = hash_algo(content_or_hash).hexdigest()
314
332
  if hexdigest != hash_value:
315
- return ValueError(
333
+ return ChecksumError(
316
334
  f"{relpath}: {hash_type} mismatch, "
317
335
  f"got {hexdigest}, expected {hash_value}")
@@ -44,15 +44,10 @@ class DirtyFile:
44
44
  os.link(content_or_file.devpi_srcpath, self.tmppath)
45
45
  else:
46
46
  with get_write_file_ensure_dir(self.tmppath) as f:
47
- if not isinstance(content_or_file, bytes) and not callable(getattr(content_or_file, "seekable", None)):
48
- content_or_file = content_or_file.read()
49
- if len(content_or_file) > 1048576:
50
- threadlog.warn(
51
- "Read %.1f megabytes into memory in keyfs_sqlite_fs from_content for %s, because of unseekable file",
52
- len(content_or_file) / 1048576, path)
53
47
  if isinstance(content_or_file, bytes):
54
48
  f.write(content_or_file)
55
49
  else:
50
+ assert content_or_file.seekable()
56
51
  content_or_file.seek(0)
57
52
  shutil.copyfileobj(content_or_file, f)
58
53
  return self
@@ -1,6 +1,7 @@
1
1
  from .filestore import FileEntry
2
2
  from .log import configure_cli_logging
3
3
  from .main import CommandRunner
4
+ from .main import Fatal
4
5
  from .main import xom_from_config
5
6
  import sys
6
7
  import time
@@ -38,6 +39,7 @@ def fsck():
38
39
  last_time = time.time()
39
40
  processed = 0
40
41
  missing_files = 0
42
+ got_errors = False
41
43
  with xom.keyfs.read_transaction() as tx:
42
44
  log.info("Checking at serial %s" % tx.at_serial)
43
45
  relpaths = tx.iter_relpaths_at(keys, tx.at_serial)
@@ -57,6 +59,7 @@ def fsck():
57
59
  if not entry.file_exists():
58
60
  missing_files += 1
59
61
  if missing_files < 10:
62
+ got_errors = True
60
63
  log.error("Missing file %s" % entry.relpath)
61
64
  elif missing_files == 10:
62
65
  log.error("Further missing files will be omitted.")
@@ -65,6 +68,7 @@ def fsck():
65
68
  continue
66
69
  checksum = entry.file_get_checksum(entry.hash_type)
67
70
  if entry.hash_value != checksum:
71
+ got_errors = True
68
72
  log.error(
69
73
  "%s - %s mismatch, got %s, expected %s"
70
74
  % (entry.relpath, entry.hash_type, checksum, entry.hash_value))
@@ -75,4 +79,7 @@ def fsck():
75
79
  log.error(
76
80
  "A total of %s files are missing."
77
81
  % missing_files)
82
+ if got_errors:
83
+ msg = "There have been errors during consistency check."
84
+ raise Fatal(msg)
78
85
  return runner.return_code
@@ -200,6 +200,18 @@ def devpiserver_auth_denials(request, acl, user, stage):
200
200
  """
201
201
 
202
202
 
203
+ @hookspec
204
+ def devpiserver_get_mirror_auth(mirror_url, www_authenticate_header):
205
+ """Provide an http "Authorization" header to access a mirror.
206
+
207
+ Return a string which will be set as the authorization header for all http
208
+ requests to this mirror.
209
+
210
+ Return None or an empty string if no credentials can be determined for the
211
+ supplied url.
212
+ """
213
+
214
+
203
215
  @hookspec
204
216
  def devpiserver_get_stage_customizer_classes():
205
217
  """EXPERIMENTAL!
@@ -1,3 +1,4 @@
1
+ import itertools
1
2
  import sys
2
3
  import json
3
4
  import os
@@ -279,6 +280,7 @@ class IndexDump:
279
280
  project=linkstore.project,
280
281
  relpath=relpath,
281
282
  version=linkstore.version,
283
+ entrymapping=tox_link.entry.meta,
282
284
  for_entrypath=reflink.entrypath,
283
285
  log=tox_link.get_logs())
284
286
 
@@ -297,7 +299,9 @@ class IndexDump:
297
299
  relpath = self.exporter.copy_file(
298
300
  entry,
299
301
  self.basedir.join("%s-%s.doc.zip" % (project, version)))
300
- self.add_filedesc("doczip", project, relpath, version=version)
302
+ self.add_filedesc(
303
+ "doczip", project, relpath,
304
+ version=version, entrymapping=entry.meta)
301
305
 
302
306
 
303
307
  class Importer:
@@ -535,8 +539,11 @@ class Importer:
535
539
  if self.xom.config.hard_links:
536
540
  # additional attribute for hard links
537
541
  f.devpi_srcpath = p.strpath
542
+
543
+ # docs and toxresults didn't always have entrymapping in export dump
544
+ mapping = filedesc.get("entrymapping", {})
545
+
538
546
  if filedesc["type"] == "releasefile":
539
- mapping = filedesc["entrymapping"]
540
547
  if self.dumpversion == "1":
541
548
  # previous versions would not add a version attribute
542
549
  version = BasenameMeta(p.basename).version
@@ -554,7 +561,7 @@ class Importer:
554
561
  url = URL(mapping['url']).replace(fragment=mapping['hash_spec'])
555
562
  entry = self.xom.filestore.maplink(
556
563
  url, stage.username, stage.index, project)
557
- entry.file_set_content(f, mapping["last_modified"])
564
+ entry.file_set_content(f, last_modified=mapping["last_modified"])
558
565
  (_, links_with_data, serial) = stage._load_cache_links(project)
559
566
  if links_with_data is None:
560
567
  links_with_data = []
@@ -579,16 +586,22 @@ class Importer:
579
586
  # determined here but in store_releasefile/store_doczip/store_toxresult etc
580
587
  elif filedesc["type"] == "doczip":
581
588
  version = filedesc["version"]
582
- link = stage.store_doczip(project, version, f)
589
+ # docs didn't always have entrymapping in export dump
590
+ last_modified = mapping.get("last_modified")
591
+ link = stage.store_doczip(
592
+ project, version, f, last_modified=last_modified)
583
593
  elif filedesc["type"] == "toxresult":
584
594
  linkstore = stage.get_linkstore_perstage(
585
595
  filedesc["projectname"], filedesc["version"])
586
596
  # we can not search for the full relative path because
587
597
  # it might use a different checksum
588
598
  basename = posixpath.basename(filedesc["for_entrypath"])
599
+ # toxresults didn't always have entrymapping in export dump
600
+ last_modified = mapping.get("last_modified")
589
601
  link, = linkstore.get_links(basename=basename)
590
602
  link = stage.store_toxresult(
591
- link, f, filename=posixpath.basename(filedesc["relpath"]))
603
+ link, f, filename=posixpath.basename(filedesc["relpath"]),
604
+ last_modified=last_modified)
592
605
  else:
593
606
  msg = f"unknown file type: {type}"
594
607
  raise Fatal(msg)
@@ -622,7 +635,7 @@ class IndexTree:
622
635
  children.append(name)
623
636
 
624
637
  def validate(self):
625
- all_bases = set(sum(self.name2bases.values(), []))
638
+ all_bases = set(itertools.chain.from_iterable(self.name2bases.values()))
626
639
  all_indexes = set(self.name2bases)
627
640
  missing = all_bases - all_indexes
628
641
  if missing:
@@ -14,7 +14,7 @@ from .keyfs_types import PTypedKey
14
14
  from .keyfs_types import TypedKey
15
15
  from .log import threadlog, thread_push_log, thread_pop_log
16
16
  from .log import thread_change_log_prefix
17
- from .markers import absent
17
+ from .markers import absent, deleted
18
18
  from .model import RootModel
19
19
  from .readonly import ensure_deeply_readonly
20
20
  from .readonly import get_mutable_deepcopy
@@ -633,7 +633,7 @@ class Transaction(object):
633
633
 
634
634
  def get_key_in_transaction(self, relpath):
635
635
  for key in self.cache:
636
- if key.relpath == relpath and self.cache[key] is not absent:
636
+ if key.relpath == relpath and self.cache[key] not in (absent, deleted):
637
637
  return key
638
638
  raise KeyError(relpath)
639
639
 
@@ -643,36 +643,28 @@ class Transaction(object):
643
643
  def get_original(self, typedkey):
644
644
  """ Return original value from start of transaction,
645
645
  without changes from current transaction."""
646
- try:
647
- return self._original[typedkey]
648
- except KeyError:
649
- # will raise KeyError if it doesn't exist
650
- val = self.get_value_at(typedkey, self.at_serial)
651
- assert is_deeply_readonly(val)
652
- self._original[typedkey] = val
653
- return val
646
+ if typedkey not in self._original:
647
+ tup = self.get_last_serial_and_value_at(
648
+ typedkey, self.at_serial, raise_on_error=False)
649
+ if tup is None:
650
+ serial = -1
651
+ val = absent
652
+ else:
653
+ (serial, val) = tup
654
+ assert is_deeply_readonly(val)
655
+ if val is None:
656
+ val = deleted
657
+ self._original[typedkey] = (serial, val)
658
+ return self._original[typedkey]
654
659
 
655
660
  def get(self, typedkey, readonly=True):
656
- """ Return current value referenced by typedkey,
657
- either as a readonly-view or as a mutable deep copy. """
658
- try:
661
+ if typedkey in self.cache:
659
662
  val = self.cache[typedkey]
660
- if val is absent:
661
- raise KeyError
662
- except KeyError:
663
- absent_from_dirty = typedkey in self.dirty
664
- if not absent_from_dirty:
665
- try:
666
- val = self.get_original(typedkey)
667
- except KeyError:
668
- absent_from_dirty = True
669
- if absent_from_dirty:
670
- # for convenience we return an empty instance
671
- # but below we still respect the readonly property
672
- val = typedkey.type()
673
- else:
674
- assert is_deeply_readonly(val)
675
- self.cache[typedkey] = val
663
+ else:
664
+ (back_serial, val) = self.get_original(typedkey)
665
+ if val in (absent, deleted):
666
+ # for convenience we return an empty instance
667
+ val = typedkey.type()
676
668
  if readonly:
677
669
  return ensure_deeply_readonly(val)
678
670
  else:
@@ -680,42 +672,31 @@ class Transaction(object):
680
672
 
681
673
  def exists(self, typedkey):
682
674
  if typedkey in self.cache:
683
- return self.cache[typedkey] is not absent
684
- if typedkey in self.dirty:
685
- return False
686
- try:
687
- val = self.get_value_at(typedkey, self.at_serial)
688
- except KeyError:
689
- self.cache[typedkey] = absent
690
- return False
691
- else:
692
- assert val is not absent
693
- assert is_deeply_readonly(val)
694
- self.cache[typedkey] = val
675
+ val = self.cache[typedkey]
676
+ if val in (absent, deleted):
677
+ return False
695
678
  return True
679
+ (serial, val) = self.get_original(typedkey)
680
+ if val in (absent, deleted):
681
+ return False
682
+ return True
696
683
 
697
684
  def delete(self, typedkey):
698
685
  if not self.write:
699
686
  raise self.keyfs.ReadOnly()
700
- self.cache.pop(typedkey, None)
687
+ self.cache[typedkey] = deleted
701
688
  self.dirty.add(typedkey)
702
689
 
703
- def set(self, typedkey, val):
690
+ def set(self, typedkey, val): # noqa: A003
704
691
  if not self.write:
705
692
  raise self.keyfs.ReadOnly()
706
693
  # sanity check for dictionaries: we always want to have unicode
707
694
  # keys, not bytes
708
695
  if typedkey.type == dict:
709
696
  check_unicode_keys(val)
710
- try:
711
- old_val = self.get_original(typedkey)
712
- except KeyError:
713
- old_val = absent
697
+ assert val is not None
714
698
  self.cache[typedkey] = val
715
- if val != old_val:
716
- self.dirty.add(typedkey)
717
- else:
718
- self.dirty.discard(typedkey)
699
+ self.dirty.add(typedkey)
719
700
 
720
701
  def commit(self):
721
702
  if self.doomed:
@@ -727,18 +708,25 @@ class Transaction(object):
727
708
  result = self._close()
728
709
  self._run_listeners(self._finished_listeners)
729
710
  return result
730
- if not self.dirty and not self.conn.dirty_files:
711
+ records = []
712
+ for typedkey in self.dirty:
713
+ val = self.cache[typedkey]
714
+ assert val is not absent
715
+ (back_serial, old_val) = self.get_original(typedkey)
716
+ if val == old_val:
717
+ continue
718
+ if val is deleted:
719
+ val = None
720
+ records.append((typedkey, val, back_serial, old_val))
721
+ if not records and not self.conn.dirty_files:
731
722
  threadlog.debug("nothing to commit, just closing tx")
732
723
  result = self._close()
733
724
  self._run_listeners(self._finished_listeners)
734
725
  return result
735
726
  try:
736
727
  with self.conn.write_transaction() as fswriter:
737
- for typedkey in self.dirty:
738
- val = self.cache.get(typedkey)
739
- assert val is not absent
740
- # None signals deletion
741
- fswriter.record_set(typedkey, val)
728
+ for (typedkey, val, back_serial, old_val) in records:
729
+ fswriter.record_set(typedkey, val, back_serial)
742
730
  commit_serial = getattr(fswriter, "commit_serial", absent)
743
731
  if commit_serial is absent:
744
732
  # for storages which don't have the attribute yet
@@ -254,16 +254,11 @@ class Connection(BaseConnection):
254
254
  f = self.dirty_files.get(path, None)
255
255
  if f is None:
256
256
  f = SpooledTemporaryFile(max_size=1048576)
257
- if not isinstance(content_or_file, bytes) and not callable(getattr(content_or_file, "seekable", None)):
258
- content_or_file = content_or_file.read()
259
- if len(content_or_file) > 1048576:
260
- threadlog.warn(
261
- "Read %.1f megabytes into memory in postgresql io_file_set for %s, because of unseekable file",
262
- len(content_or_file) / 1048576, path)
263
257
  if isinstance(content_or_file, bytes):
264
258
  f.write(content_or_file)
265
259
  f.seek(0)
266
260
  else:
261
+ assert content_or_file.seekable()
267
262
  content_or_file.seek(0)
268
263
  shutil.copyfileobj(content_or_file, f)
269
264
  self.dirty_files[path] = f
@@ -394,6 +389,9 @@ class BaseStorage(object):
394
389
  return sqlite3.connect(
395
390
  self.sqlpath.strpath, timeout=60, isolation_level=None)
396
391
 
392
+ def _execute_conn_pragmas(self, sqlconn):
393
+ pass
394
+
397
395
  def _get_sqlconn(self, uri):
398
396
  # we will try different connection methods and overwrite _get_sqlconn
399
397
  # with the first successful one
@@ -450,6 +448,7 @@ class BaseStorage(object):
450
448
  mode = "rwc"
451
449
  uri = "file:%s?mode=%s" % (self.sqlpath, mode)
452
450
  sqlconn = self._get_sqlconn(uri)
451
+ self._execute_conn_pragmas(sqlconn)
453
452
  if write:
454
453
  start_time = time.monotonic()
455
454
  thread = current_thread()
@@ -52,15 +52,10 @@ class DirtyFile(object):
52
52
  os.link(content_or_file.devpi_srcpath, self.tmppath)
53
53
  else:
54
54
  with get_write_file_ensure_dir(self.tmppath) as f:
55
- if not isinstance(content_or_file, bytes) and not callable(getattr(content_or_file, "seekable", None)):
56
- content_or_file = content_or_file.read()
57
- if len(content_or_file) > 1048576:
58
- threadlog.warn(
59
- "Read %.1f megabytes into memory in keyfs_sqlite_fs from_content for %s, because of unseekable file",
60
- len(content_or_file) / 1048576, path)
61
55
  if isinstance(content_or_file, bytes):
62
56
  f.write(content_or_file)
63
57
  else:
58
+ assert content_or_file.seekable()
64
59
  content_or_file.seek(0)
65
60
  shutil.copyfileobj(content_or_file, f)
66
61
  return self