devpi-server 6.13.0__tar.gz → 6.14.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 (130) hide show
  1. {devpi-server-6.13.0 → devpi-server-6.14.0}/CHANGELOG +21 -0
  2. {devpi-server-6.13.0 → devpi-server-6.14.0}/PKG-INFO +22 -22
  3. devpi-server-6.14.0/devpi_server/__init__.py +1 -0
  4. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/main.py +5 -2
  5. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/views.py +46 -27
  6. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server.egg-info/PKG-INFO +22 -22
  7. {devpi-server-6.13.0 → devpi-server-6.14.0}/setup.py +1 -1
  8. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/plugin.py +7 -0
  9. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_views.py +86 -1
  10. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_views_push_external.py +54 -24
  11. devpi-server-6.13.0/devpi_server/__init__.py +0 -1
  12. {devpi-server-6.13.0 → devpi-server-6.14.0}/.flake8 +0 -0
  13. {devpi-server-6.13.0 → devpi-server-6.14.0}/AUTHORS +0 -0
  14. {devpi-server-6.13.0 → devpi-server-6.14.0}/LICENSE +0 -0
  15. {devpi-server-6.13.0 → devpi-server-6.14.0}/MANIFEST.in +0 -0
  16. {devpi-server-6.13.0 → devpi-server-6.14.0}/README.rst +0 -0
  17. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/__main__.py +0 -0
  18. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/auth.py +0 -0
  19. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/auth_basic.py +0 -0
  20. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/auth_devpi.py +0 -0
  21. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/__init__.py +0 -0
  22. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/crontab.template +0 -0
  23. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/devpi.service.template +0 -0
  24. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/launchd-macos.txt.template +0 -0
  25. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/nginx-devpi-caching-http.conf.template +0 -0
  26. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/nginx-devpi-caching-location.conf.template +0 -0
  27. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/nginx-devpi-caching-proxy.conf.template +0 -0
  28. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/nginx-devpi-caching-server.conf.template +0 -0
  29. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/nginx-devpi.conf.template +0 -0
  30. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/supervisor-devpi.conf.template +0 -0
  31. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/supervisord.conf.template +0 -0
  32. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/cfg/windows-service.txt.template +0 -0
  33. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/config.py +0 -0
  34. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/exceptions.py +0 -0
  35. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/filestore.py +0 -0
  36. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/filestore_fs.py +0 -0
  37. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/fileutil.py +0 -0
  38. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/fsck.py +0 -0
  39. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/genconfig.py +0 -0
  40. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/hookspecs.py +0 -0
  41. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/importexport.py +0 -0
  42. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/init.py +0 -0
  43. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/interfaces.py +0 -0
  44. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/keyfs.py +0 -0
  45. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/keyfs_sqlite.py +0 -0
  46. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/keyfs_sqlite_fs.py +0 -0
  47. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/keyfs_types.py +0 -0
  48. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/log.py +0 -0
  49. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/markers.py +0 -0
  50. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/middleware.py +0 -0
  51. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/mirror.py +0 -0
  52. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/model.py +0 -0
  53. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/mythread.py +0 -0
  54. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/passwd.py +0 -0
  55. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/readonly.py +0 -0
  56. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/replica.py +0 -0
  57. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/sizeof.py +0 -0
  58. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/vendor/__init__.py +0 -0
  59. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/vendor/_pip.py +0 -0
  60. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server/view_auth.py +0 -0
  61. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server.egg-info/SOURCES.txt +0 -0
  62. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server.egg-info/dependency_links.txt +0 -0
  63. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server.egg-info/entry_points.txt +0 -0
  64. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server.egg-info/not-zip-safe +0 -0
  65. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server.egg-info/requires.txt +0 -0
  66. {devpi-server-6.13.0 → devpi-server-6.14.0}/devpi_server.egg-info/top_level.txt +0 -0
  67. {devpi-server-6.13.0 → devpi-server-6.14.0}/pyproject.toml +0 -0
  68. {devpi-server-6.13.0 → devpi-server-6.14.0}/pytest_devpi_server/__init__.py +0 -0
  69. {devpi-server-6.13.0 → devpi-server-6.14.0}/setup.cfg +0 -0
  70. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/__init__.py +0 -0
  71. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/conftest.py +0 -0
  72. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/example.py +0 -0
  73. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/functional.py +0 -0
  74. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/badindexname/dataindex.json +0 -0
  75. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/badusername/dataindex.json +0 -0
  76. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/basescycle/dataindex.json +0 -0
  77. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/createdmodified/dataindex.json +0 -0
  78. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/deletedbase/dataindex.json +0 -0
  79. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/mirrordata/dataindex.json +0 -0
  80. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/mirrordata/root/pypi/dddttt/0.1.dev1/dddttt-0.1.dev1.tar.gz +0 -0
  81. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/modifiedpypi/dataindex.json +0 -0
  82. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/nocreatedmodified/dataindex.json +0 -0
  83. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/normalization/dataindex.json +0 -0
  84. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/normalization/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  85. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/normalization_merge/dataindex.json +0 -0
  86. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello-pkg/hello.pkg-1.1.tar.gz +0 -0
  87. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  88. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/norootpypi/dataindex.json +0 -0
  89. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/nouser/dataindex.json +0 -0
  90. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/removedindexplugin/dataindex.json +0 -0
  91. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/removedindexplugin/user/dev/pkg/pkg-1.0.tar.gz +0 -0
  92. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/dataindex.json +0 -0
  93. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/0.9/hello-0.9.tar.gz +0 -0
  94. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/sha256=ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73/hello-0.9.tar.gz.toxresult0 +0 -0
  95. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/importexportdata/toxresult_upload_default/dataindex.json +0 -0
  96. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/reqmock.py +0 -0
  97. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/simpypi.py +0 -0
  98. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_auth.py +0 -0
  99. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_authcheck.py +0 -0
  100. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_config.py +0 -0
  101. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_conftest.py +0 -0
  102. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_filestore.py +0 -0
  103. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_fileutil.py +0 -0
  104. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_fsck.py +0 -0
  105. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_genconfig.py +0 -0
  106. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_importexport.py +0 -0
  107. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_keyfs.py +0 -0
  108. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_keyfs_sqlite_fs.py +0 -0
  109. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_log.py +0 -0
  110. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_main.py +0 -0
  111. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_mirror.py +0 -0
  112. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_mirror_no_project_list.py +0 -0
  113. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_model.py +0 -0
  114. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_mythread.py +0 -0
  115. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_nginx.py +0 -0
  116. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_nginx_replica.py +0 -0
  117. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_permissions.py +0 -0
  118. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_readonly.py +0 -0
  119. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_replica.py +0 -0
  120. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_replica_functional.py +0 -0
  121. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_reqmock.py +0 -0
  122. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_stage_customizer.py +0 -0
  123. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_streaming.py +0 -0
  124. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_streaming_nginx.py +0 -0
  125. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_streaming_replica.py +0 -0
  126. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_streaming_replica_nginx.py +0 -0
  127. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_view_auth.py +0 -0
  128. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_views_patch.py +0 -0
  129. {devpi-server-6.13.0 → devpi-server-6.14.0}/test_devpi_server/test_views_status.py +0 -0
  130. {devpi-server-6.13.0 → devpi-server-6.14.0}/tox.ini +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  .. towncrier release notes start
4
4
 
5
+ 6.14.0 (2024-10-16)
6
+ ===================
7
+
8
+ Features
9
+ --------
10
+
11
+ - Allow pushing of versions which only have documentation and no releases.
12
+
13
+ - Allow pushing of release files only with no documentation. Requires devpi-client 7.2.0.
14
+
15
+ - Allow pushing of documentation only with no release files. Requires devpi-client 7.2.0.
16
+
17
+
18
+
19
+ Bug Fixes
20
+ ---------
21
+
22
+ - No longer automatically "register" a project when pushing releases to PyPI. The reply changed from HTTP status 410 to 400 breaking the upload. With devpi-client 7.2.0 there is a ``--register-project`` option if it is still required for some other package registry.
23
+
24
+
25
+
5
26
  6.13.0 (2024-09-19)
6
27
  ===================
7
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-server
3
- Version: 6.13.0
3
+ Version: 6.14.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
@@ -105,6 +105,27 @@ Changelog
105
105
 
106
106
  .. towncrier release notes start
107
107
 
108
+ 6.14.0 (2024-10-16)
109
+ ===================
110
+
111
+ Features
112
+ --------
113
+
114
+ - Allow pushing of versions which only have documentation and no releases.
115
+
116
+ - Allow pushing of release files only with no documentation. Requires devpi-client 7.2.0.
117
+
118
+ - Allow pushing of documentation only with no release files. Requires devpi-client 7.2.0.
119
+
120
+
121
+
122
+ Bug Fixes
123
+ ---------
124
+
125
+ - No longer automatically "register" a project when pushing releases to PyPI. The reply changed from HTTP status 410 to 400 breaking the upload. With devpi-client 7.2.0 there is a ``--register-project`` option if it is still required for some other package registry.
126
+
127
+
128
+
108
129
  6.13.0 (2024-09-19)
109
130
  ===================
110
131
 
@@ -193,24 +214,3 @@ Bug Fixes
193
214
  - Fix #1033: Use ``int`` for ``--mirror-cache-expiry`` to fix value of ``proxy_cache_valid`` in nginx caching config.
194
215
 
195
216
 
196
-
197
- 6.10.0 (2023-12-19)
198
- ===================
199
-
200
- Features
201
- --------
202
-
203
- - Use ``Authorization`` header instead of adding username/password to URL when fetching from mirror.
204
-
205
- - Fix #998: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
206
-
207
-
208
-
209
- Bug Fixes
210
- ---------
211
-
212
- - Fix #996: support hashes other than sha256 in application/vnd.pypi.simple.v1+json responses.
213
-
214
- - Only compare hostname instead of full URL prefix when parsing mirror packages to fix mirrors with basic authentication and absolute URLs. See #1006
215
-
216
-
@@ -0,0 +1 @@
1
+ __version__ = '6.14.0'
@@ -365,9 +365,12 @@ class XOM:
365
365
 
366
366
  @cached_property
367
367
  def supported_features(self):
368
- results = set((
368
+ results = {
369
+ 'push-no-docs',
370
+ 'push-only-docs',
371
+ 'push-register-project',
369
372
  'server-keyvalue-parsing',
370
- ))
373
+ }
371
374
  for features in self.config.hook.devpiserver_get_features():
372
375
  results.update(features)
373
376
  return tuple(sorted(results))
@@ -10,6 +10,7 @@ from devpi_common.url import URL
10
10
  from devpi_common.metadata import get_pyversion_filetype
11
11
  import devpi_server
12
12
  from html import escape
13
+ from http import HTTPStatus
13
14
  from lazy import lazy
14
15
  from operator import attrgetter
15
16
  from pluggy import HookimplMarker
@@ -141,7 +142,7 @@ class HTTPResponse(HTTPSuccessful):
141
142
  Exception.__init__(self)
142
143
 
143
144
 
144
- def apireturn(code, message=None, result=None, type=None):
145
+ def apiresult(code, message=None, result=None, type=None): # noqa: A002
145
146
  d = dict()
146
147
  if result is not None:
147
148
  assert type is not None
@@ -151,7 +152,11 @@ def apireturn(code, message=None, result=None, type=None):
151
152
  d["message"] = message
152
153
  data = json.dumps(d, indent=2) + "\n"
153
154
  headers = {"content-type": "application/json"}
154
- raise HTTPResponse(body=data, status=code, headers=headers)
155
+ return HTTPResponse(body=data, status=code, headers=headers)
156
+
157
+
158
+ def apireturn(code, message=None, result=None, type=None): # noqa: A002
159
+ raise apiresult(code, message=message, result=result, type=type)
155
160
 
156
161
 
157
162
  def json_preferred(request):
@@ -1007,8 +1012,8 @@ class PyPIView:
1007
1012
  stage = self.context.stage
1008
1013
  pushdata = getjson(request)
1009
1014
  try:
1010
- name = pushdata["name"]
1011
- version = pushdata["version"]
1015
+ name = pushdata.pop("name")
1016
+ version = pushdata.pop("version")
1012
1017
  except KeyError:
1013
1018
  apireturn(400, message="no name/version specified in json")
1014
1019
 
@@ -1018,14 +1023,21 @@ class PyPIView:
1018
1023
  except stage.MissesRegistration:
1019
1024
  apireturn(400, "there are no files for %s-%s on stage %s" % (
1020
1025
  name, version, stage.name))
1026
+ rels = {'releasefile', 'doczip', 'toxresult'}
1027
+ no_docs = pushdata.pop("no_docs", False)
1028
+ only_docs = pushdata.pop("only_docs", False)
1029
+ if no_docs and only_docs:
1030
+ return apiresult(400, "can't use 'no_docs' and 'only_docs' together")
1031
+ if no_docs:
1032
+ rels.remove('doczip')
1033
+ elif only_docs:
1034
+ rels = {'doczip'}
1021
1035
  links = {
1022
1036
  rel: sorted(linkstore.get_links(rel=rel), key=attrgetter('basename'))
1023
- for rel in ('releasefile', 'doczip', 'toxresult')}
1024
- if not links["releasefile"]:
1025
- self.log.info("%s: no release files for version %s-%s" %
1026
- (stage.name, name, version))
1027
- apireturn(404, message="no release/files found for %s-%s" % (
1028
- name, version))
1037
+ for rel in rels}
1038
+ if not any(links.values()):
1039
+ self.log.info("%s: no files for version %s %s", stage.name, name, version)
1040
+ apireturn(404, f"no release/files found for {name} {version}")
1029
1041
 
1030
1042
  if not request.has_permission("pkg_read"):
1031
1043
  abort(request, 403, "package read forbidden")
@@ -1033,8 +1045,11 @@ class PyPIView:
1033
1045
  metadata = linkstore.metadata
1034
1046
 
1035
1047
  results = []
1036
- targetindex = pushdata.get("targetindex", None)
1048
+ targetindex = pushdata.pop("targetindex", None)
1037
1049
  if targetindex is not None: # in-server push
1050
+ if pushdata:
1051
+ keys = ', '.join(sorted(pushdata.keys()))
1052
+ return apiresult(400, f"unknown additional options: {keys}")
1038
1053
  parts = targetindex.split("/")
1039
1054
  if len(parts) != 2:
1040
1055
  apireturn(400, message="targetindex not in format user/index")
@@ -1057,6 +1072,7 @@ class PyPIView:
1057
1072
  return apireturn(502, e.args[0])
1058
1073
  apireturn(200, result=results, type="actionlog")
1059
1074
  else:
1075
+ register_project = pushdata.pop("register_project", False)
1060
1076
  posturl = pushdata["posturl"]
1061
1077
  username = pushdata["username"]
1062
1078
  password = pushdata["password"]
@@ -1064,22 +1080,25 @@ class PyPIView:
1064
1080
  # prepare metadata for submission
1065
1081
  metadata[":action"] = "submit"
1066
1082
  metadata["metadata_version"] = "2.1"
1067
- self.log.info("registering %s-%s to %s", name, version, posturl)
1068
1083
  session = new_requests_session(agent=("server", server_version))
1069
1084
  with contextlib.closing(session):
1070
- try:
1071
- r = session.post(posturl, data=metadata, auth=pypiauth)
1072
- r.close()
1073
- except Exception as e:
1074
- exc_msg = ''.join(traceback.format_exception_only(e.__class__, e))
1075
- results.append((-1, "exception on register:", exc_msg))
1076
- apireturn(502, result=results, type="actionlog")
1077
- self.log.debug("register returned: %s", r.status_code)
1078
- results.append((r.status_code, "register", name, version))
1079
- ok_codes = (200, 201, 410)
1080
- proceed = (r.status_code in ok_codes)
1085
+ ok_codes = {HTTPStatus.OK, HTTPStatus.CREATED}
1086
+ if register_project:
1087
+ self.log.info("registering %s-%s to %s", name, version, posturl)
1088
+ try:
1089
+ r = session.post(posturl, data=metadata, auth=pypiauth)
1090
+ r.close()
1091
+ except Exception as e: # noqa: BLE001
1092
+ exc_msg = ''.join(traceback.format_exception_only(e.__class__, e))
1093
+ results.append((-1, "exception on register:", exc_msg))
1094
+ apireturn(502, result=results, type="actionlog")
1095
+ self.log.debug("register returned: %s", r.status_code)
1096
+ results.append((r.status_code, "register", name, version))
1097
+ proceed = (r.status_code in ok_codes | {HTTPStatus.GONE})
1098
+ else:
1099
+ proceed = True
1081
1100
  if proceed:
1082
- for link in links["releasefile"]:
1101
+ for link in links.get("releasefile", ()):
1083
1102
  entry = link.entry
1084
1103
  file_metadata = metadata.copy()
1085
1104
  file_metadata[":action"] = "file_upload"
@@ -1106,7 +1125,7 @@ class PyPIView:
1106
1125
  results.append((
1107
1126
  r.status_code, "upload", entry.relpath, text))
1108
1127
  r.close()
1109
- if links["doczip"]:
1128
+ if links.get("doczip", ()):
1110
1129
  doc_metadata = metadata.copy()
1111
1130
  doc_metadata[":action"] = "doc_upload"
1112
1131
  doczip = links["doczip"][0].entry.file_get_content()
@@ -1128,7 +1147,7 @@ class PyPIView:
1128
1147
 
1129
1148
  def _push_links(self, links, target_stage, name, version):
1130
1149
  stage = self.context.stage
1131
- for link in links["releasefile"]:
1150
+ for link in links.get("releasefile", ()):
1132
1151
  entry = link.entry
1133
1152
  logs = link.get_logs()
1134
1153
  del link
@@ -1170,7 +1189,7 @@ class PyPIView:
1170
1189
  src=self.context.stage.name,
1171
1190
  dst=target_stage.name)
1172
1191
  yield (200, "store_toxresult", tlink.entrypath)
1173
- for link in links["doczip"]:
1192
+ for link in links.get("doczip", ()):
1174
1193
  with link.entry.file_open_read() as doczip:
1175
1194
  new_link = target_stage.store_doczip(
1176
1195
  name, version, doczip, hashes=link.entry.hashes)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-server
3
- Version: 6.13.0
3
+ Version: 6.14.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
@@ -105,6 +105,27 @@ Changelog
105
105
 
106
106
  .. towncrier release notes start
107
107
 
108
+ 6.14.0 (2024-10-16)
109
+ ===================
110
+
111
+ Features
112
+ --------
113
+
114
+ - Allow pushing of versions which only have documentation and no releases.
115
+
116
+ - Allow pushing of release files only with no documentation. Requires devpi-client 7.2.0.
117
+
118
+ - Allow pushing of documentation only with no release files. Requires devpi-client 7.2.0.
119
+
120
+
121
+
122
+ Bug Fixes
123
+ ---------
124
+
125
+ - No longer automatically "register" a project when pushing releases to PyPI. The reply changed from HTTP status 410 to 400 breaking the upload. With devpi-client 7.2.0 there is a ``--register-project`` option if it is still required for some other package registry.
126
+
127
+
128
+
108
129
  6.13.0 (2024-09-19)
109
130
  ===================
110
131
 
@@ -193,24 +214,3 @@ Bug Fixes
193
214
  - Fix #1033: Use ``int`` for ``--mirror-cache-expiry`` to fix value of ``proxy_cache_valid`` in nginx caching config.
194
215
 
195
216
 
196
-
197
- 6.10.0 (2023-12-19)
198
- ===================
199
-
200
- Features
201
- --------
202
-
203
- - Use ``Authorization`` header instead of adding username/password to URL when fetching from mirror.
204
-
205
- - Fix #998: Use the pure Python httpx library instead of aiohttp to prevent delays in supporting newest Python releases.
206
-
207
-
208
-
209
- Bug Fixes
210
- ---------
211
-
212
- - Fix #996: support hashes other than sha256 in application/vnd.pypi.simple.v1+json responses.
213
-
214
- - Only compare hostname instead of full URL prefix when parsing mirror packages to fix mirrors with basic authentication and absolute URLs. See #1006
215
-
216
-
@@ -59,7 +59,7 @@ if __name__ == "__main__":
59
59
  'Funding': 'https://github.com/sponsors/devpi',
60
60
  'Source Code': 'https://github.com/devpi/devpi'
61
61
  },
62
- version='6.13.0',
62
+ version='6.14.0',
63
63
  maintainer="Florian Schulze",
64
64
  maintainer_email="mail@pyfidelity.com",
65
65
  packages=[
@@ -910,6 +910,13 @@ class Mapp(MappMixin):
910
910
  assert r.status_code == code
911
911
  if waithooks:
912
912
  self._wait_for_serial_in_result(r)
913
+
914
+ # return the file url so users/callers can easily use it
915
+ # (probably the official server response should include the url)
916
+ r.file_url = make_file_url(basename, content, stagename=indexname)
917
+ r.file_url_no_hash = make_file_url(
918
+ basename, content, stagename=indexname, add_hash=False)
919
+
913
920
  return r
914
921
 
915
922
  def upload_toxresult(self, path, content, code=200, waithooks=False):
@@ -1389,8 +1389,57 @@ def test_upload_docs_for_version_without_release(mapp, testapp, monkeypatch):
1389
1389
  mapp.upload_doc("pkg1-2.7.doc.zip", b'', "pkg1", "2.7")
1390
1390
 
1391
1391
 
1392
+ def test_upload_and_push_docs_only(mapp, testapp):
1393
+ mapp.create_and_login_user()
1394
+ mapp.create_index("prod")
1395
+ mapp.create_index("dev")
1396
+ mapp.upload_file_pypi("pkg1-2.6.tgz", b"123", "pkg1", "2.6")
1397
+ content26 = zip_dict({"index.html": "<html><body>2.6</body></html>"})
1398
+ mapp.upload_doc("pkg1.zip", content26, "pkg1", "")
1399
+ mapp.upload_file_pypi("pkg1-2.7.tgz", b"123", "pkg1", "2.7")
1400
+ content27 = zip_dict({"index.html": "<html><body>2.7</body></html>"})
1401
+ mapp.upload_doc("pkg1.zip", content27, "pkg1", "")
1402
+ req = dict(name="pkg1", version="2.6", targetindex="someuser/prod", only_docs=True)
1403
+ testapp.post("/someuser/dev", params=json.dumps(req), code=200)
1404
+ vv = get_view_version_links(testapp, "/someuser/prod", "pkg1", "2.6")
1405
+ (link,) = vv.get_links()
1406
+ assert link.basename == "pkg1-2.6.doc.zip"
1407
+ assert link.rel == "doczip"
1408
+ mapp.getjson("/someuser/prod/2.7", code=404)
1409
+
1410
+
1411
+ def test_upload_and_push_no_docs(mapp, testapp):
1412
+ mapp.create_and_login_user()
1413
+ mapp.create_index("prod")
1414
+ mapp.create_index("dev")
1415
+ mapp.upload_file_pypi("pkg1-2.6.tgz", b"123", "pkg1", "2.6")
1416
+ content26 = zip_dict({"index.html": "<html><body>2.6</body></html>"})
1417
+ mapp.upload_doc("pkg1.zip", content26, "pkg1", "")
1418
+ mapp.upload_file_pypi("pkg1-2.7.tgz", b"123", "pkg1", "2.7")
1419
+ content27 = zip_dict({"index.html": "<html><body>2.7</body></html>"})
1420
+ mapp.upload_doc("pkg1.zip", content27, "pkg1", "")
1421
+ req = dict(name="pkg1", version="2.6", targetindex="someuser/prod", no_docs=True)
1422
+ testapp.post("/someuser/dev", params=json.dumps(req), code=200)
1423
+ vv = get_view_version_links(testapp, "/someuser/prod", "pkg1", "2.6")
1424
+ (link,) = vv.get_links()
1425
+ assert link.basename == "pkg1-2.6.tgz"
1426
+ assert link.rel == "releasefile"
1427
+ mapp.getjson("/someuser/prod/2.7", code=404)
1428
+
1429
+
1430
+ def test_upload_and_push_no_docs_and_only_docs(mapp, testapp):
1431
+ mapp.create_and_login_user()
1432
+ mapp.create_index("dev")
1433
+ mapp.upload_file_pypi("pkg1-2.6.tgz", b"123", "pkg1", "2.6")
1434
+ content26 = zip_dict({"index.html": "<html><body>2.6</body></html>"})
1435
+ mapp.upload_doc("pkg1.zip", content26, "pkg1", "")
1436
+ req = dict(name="pkg1", version="2.6", targetindex="someuser/prod", no_docs=True, only_docs=True)
1437
+ r = testapp.post("/someuser/dev", params=json.dumps(req), code=400)
1438
+ assert r.json["message"] == "can't use 'no_docs' and 'only_docs' together"
1439
+
1440
+
1392
1441
  @proj
1393
- def test_upload_and_push_internal(mapp, testapp, monkeypatch, proj):
1442
+ def test_upload_and_push_internal(mapp, testapp, proj):
1394
1443
  mapp.create_user("user1", "1")
1395
1444
  mapp.create_and_login_user("user2")
1396
1445
  mapp.create_index("prod", indexconfig=dict(acl_upload=["user1", "user2"]))
@@ -1449,6 +1498,42 @@ def test_upload_and_push_internal(mapp, testapp, monkeypatch, proj):
1449
1498
  assert link.href.endswith("/pkg1-2.6.tgz")
1450
1499
 
1451
1500
 
1501
+ @proj
1502
+ def test_upload_and_push_internal_no_releases_but_docs(mapp, testapp, proj):
1503
+ mapp.create_user("user1", "1")
1504
+ mapp.create_and_login_user("user2")
1505
+ mapp.create_index("prod", indexconfig=dict(acl_upload=["user1", "user2"]))
1506
+ mapp.create_index("dev", indexconfig=dict(acl_upload=["user2"]))
1507
+
1508
+ mapp.login("user1", "1")
1509
+ mapp.create_index("dev")
1510
+ mapp.use("user1/dev")
1511
+ content = zip_dict({"index.html": "<html/>"})
1512
+ mapp.upload_doc("pkg1.zip", content, "pkg1", "2.6")
1513
+
1514
+ # check that push is authorized and executed towards user2/prod index
1515
+ req = dict(name="pkg1", version="2.6", targetindex="user2/prod")
1516
+ r = testapp.push("/user1/dev", json.dumps(req))
1517
+ assert r.status_code == 200
1518
+ vv = get_view_version_links(testapp, "/user2/prod", "pkg1", "2.6",
1519
+ proj=proj)
1520
+ (link,) = vv.get_links()
1521
+ assert link.rel == "doczip"
1522
+ history_log = link.log
1523
+ assert len(history_log) == 2
1524
+ assert history_log[0]['what'] == 'upload'
1525
+ assert history_log[0]['who'] == 'user1'
1526
+ assert history_log[0]['dst'] == 'user1/dev'
1527
+ assert history_log[1]['what'] == 'push'
1528
+ assert history_log[1]['who'] == 'user1'
1529
+ assert history_log[1]['src'] == 'user1/dev'
1530
+ assert history_log[1]['dst'] == 'user2/prod'
1531
+ assert link.href.endswith("/pkg1-2.6.doc.zip")
1532
+ r = testapp.get(link.href)
1533
+ archive = Archive(BytesIO(r.body))
1534
+ assert 'index.html' in archive.namelist()
1535
+
1536
+
1452
1537
  def test_acl_toxresults_upload(mapp, testapp, tox_result_data):
1453
1538
  mapp.create_and_login_user("user1", "1")
1454
1539
  mapp.create_index("prod")
@@ -33,7 +33,7 @@ def test_upload_and_push_external(mapp, testapp, reqmock):
33
33
 
34
34
  # push OK
35
35
  req = dict(name="pkg1", version="2.6", posturl="http://whatever.com/",
36
- username="user", password="password")
36
+ username="user", password="password", register_project=True)
37
37
  rec = reqmock.mockresponse(url=None, code=200, method="POST", data="msg")
38
38
  body = json.dumps(req).encode("utf-8")
39
39
  r = testapp.request(api.index, method="POST", body=body,
@@ -56,7 +56,7 @@ def test_upload_and_push_external(mapp, testapp, reqmock):
56
56
  rec = reqmock.mockresponse(url=None, code=410, method="POST", data="msg")
57
57
  r = testapp.request(api.index, method="POST", body=body,
58
58
  expect_errors=True)
59
- assert r.status_code == 200
59
+ assert r.status_code == 502
60
60
  (action_register, action_upload, action_docfile) = r.json["result"]
61
61
  assert action_register == [410, 'register', 'pkg1', '2.6']
62
62
  assert action_upload == [
@@ -73,6 +73,41 @@ def test_upload_and_push_external(mapp, testapp, reqmock):
73
73
  assert result[0][1] == 'register'
74
74
 
75
75
 
76
+ def test_upload_and_push_external_no_docs(mapp, testapp, reqmock):
77
+ from devpi_server.filestore import relpath_prefix
78
+ api = mapp.create_and_use()
79
+ content = b"123"
80
+ hashdir = relpath_prefix(content)
81
+ mapp.upload_file_pypi("pkg1-2.6.tgz", content, "pkg1", "2.6")
82
+ zipcontent = zip_dict({"index.html": "<html/>"})
83
+ mapp.upload_doc("pkg1.zip", zipcontent, "pkg1", "")
84
+ req = dict(name="pkg1", version="2.6", posturl="http://whatever.com/",
85
+ username="user", password="password", no_docs=True)
86
+ reqmock.mockresponse(url=None, code=200, method="POST", data="msg")
87
+ body = json.dumps(req).encode()
88
+ r = testapp.request(api.index, method="POST", body=body)
89
+ assert r.status_code == 200
90
+ (action_upload,) = r.json["result"]
91
+ assert action_upload == [
92
+ 200, 'upload', f'user1/dev/+f/{hashdir}/pkg1-2.6.tgz', '']
93
+
94
+
95
+ def test_upload_and_push_external_only_docs(mapp, testapp, reqmock):
96
+ api = mapp.create_and_use()
97
+ content = b"123"
98
+ mapp.upload_file_pypi("pkg1-2.6.tgz", content, "pkg1", "2.6")
99
+ zipcontent = zip_dict({"index.html": "<html/>"})
100
+ mapp.upload_doc("pkg1.zip", zipcontent, "pkg1", "")
101
+ req = dict(name="pkg1", version="2.6", posturl="http://whatever.com/",
102
+ username="user", password="password", only_docs=True)
103
+ reqmock.mockresponse(url=None, code=200, method="POST", data="msg")
104
+ body = json.dumps(req).encode()
105
+ r = testapp.request(api.index, method="POST", body=body)
106
+ assert r.status_code == 200
107
+ (action_docfile,) = r.json["result"]
108
+ assert action_docfile == [200, 'docfile', 'pkg1']
109
+
110
+
76
111
  def test_upload_and_push_external_exception(mapp, testapp, reqmock):
77
112
  import requests
78
113
  api = mapp.create_and_use()
@@ -89,7 +124,7 @@ def test_upload_and_push_external_exception(mapp, testapp, reqmock):
89
124
 
90
125
  # first test should fail during register
91
126
  req = dict(name="pkg1", version="2.6", posturl="http://whatever.com/",
92
- username="user", password="password")
127
+ username="user", password="password", register_project=True)
93
128
  body = json.dumps(req).encode("utf-8")
94
129
  r = testapp.request(api.index, method="POST", body=body,
95
130
  expect_errors=True)
@@ -106,11 +141,11 @@ def test_upload_and_push_external_exception(mapp, testapp, reqmock):
106
141
  status=410, preload_content=False,
107
142
  reason="Project pre-registration is no longer required or supported, so continue directly to uploading files."))
108
143
  req = dict(name="pkg1", version="2.6", posturl="http://whatever.com/",
109
- username="user", password="password")
144
+ username="user", password="password", register_project=True)
110
145
  body = json.dumps(req).encode("utf-8")
111
146
  r = testapp.request(api.index, method="POST", body=body,
112
147
  expect_errors=True)
113
- assert r.status_code == 200
148
+ assert r.status_code == 502
114
149
  assert r.json['type'] == 'actionlog'
115
150
  assert len(r.json['result']) == 2
116
151
  assert r.json['result'][0][0] == 410
@@ -132,18 +167,16 @@ def test_upload_and_push_external_metadata12(mapp, reqmock, testapp):
132
167
  assert verdata['requires_python'] == ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
133
168
  req = dict(name="pkg1", version="2.6", posturl="http://whatever.com/",
134
169
  username="user", password="password")
135
- rec = reqmock.mockresponse(url=None, code=200, method="POST", data="msg")
170
+ rep = reqmock.mockresponse(url=None, code=200, method="POST", data="msg")
136
171
  body = json.dumps(req).encode("utf-8")
137
172
  r = testapp.request(api.index, method="POST", body=body,
138
173
  expect_errors=True)
139
174
  assert r.status_code == 200
140
- assert len(rec.requests) == 2
141
- for i in range(2):
142
- assert rec.requests[i].url == req["posturl"]
143
- req = rec.requests[1]
175
+ (rec,) = rep.requests
176
+ assert rec.url == req["posturl"]
144
177
  fs = cgi_FieldStorage(
145
- fp=BytesIO(req.body),
146
- headers=req.headers,
178
+ fp=BytesIO(rec.body),
179
+ headers=rec.headers,
147
180
  environ={
148
181
  'REQUEST_METHOD': 'POST'})
149
182
  assert fs.getvalue('metadata_version') == "2.1"
@@ -162,18 +195,16 @@ def test_upload_and_push_external_metadata21(mapp, reqmock, testapp):
162
195
  assert verdata['description_content_type'] == "text/plain"
163
196
  req = dict(name="pkg1", version="2.6", posturl="http://whatever.com/",
164
197
  username="user", password="password")
165
- rec = reqmock.mockresponse(url=None, code=200, method="POST", data="msg")
198
+ rep = reqmock.mockresponse(url=None, code=200, method="POST", data="msg")
166
199
  body = json.dumps(req).encode("utf-8")
167
200
  r = testapp.request(api.index, method="POST", body=body,
168
201
  expect_errors=True)
169
202
  assert r.status_code == 200
170
- assert len(rec.requests) == 2
171
- for i in range(2):
172
- assert rec.requests[i].url == req["posturl"]
173
- req = rec.requests[1]
203
+ (rec,) = rep.requests
204
+ assert rec.url == req["posturl"]
174
205
  fs = cgi_FieldStorage(
175
- fp=BytesIO(req.body),
176
- headers=req.headers,
206
+ fp=BytesIO(rec.body),
207
+ headers=rec.headers,
177
208
  environ={
178
209
  'REQUEST_METHOD': 'POST'})
179
210
  assert fs.getvalue('metadata_version') == "2.1"
@@ -221,7 +252,7 @@ def test_upload_and_push_warehouse(mapp, testapp, reqmock):
221
252
 
222
253
  # push OK
223
254
  req = dict(name="pkg1", version="2.6", posturl="http://whatever.com/",
224
- username="user", password="password")
255
+ username="user", password="password", register_project=True)
225
256
  body = json.dumps(req).encode("utf-8")
226
257
  r = testapp.request(api.index, method="POST", body=body,
227
258
  expect_errors=True)
@@ -250,9 +281,8 @@ def test_upload_and_push_egg(mapp, testapp, reqmock):
250
281
  # push
251
282
  req = dict(name="pkg2", version="1.0", posturl="http://whatever.com/",
252
283
  username="user", password="password")
253
- rec = reqmock.mockresponse(url=None, data=b"msg", code=200)
284
+ rep = reqmock.mockresponse(url=None, data=b"msg", code=200)
254
285
  r = testapp.push(api.index, json.dumps(req))
255
286
  assert r.status_code == 200
256
- assert len(rec.requests) == 2
257
- assert rec.requests[0].url == req["posturl"]
258
- assert rec.requests[1].url == req["posturl"]
287
+ (rec,) = rep.requests
288
+ assert rec.url == req["posturl"]
@@ -1 +0,0 @@
1
- __version__ = '6.13.0'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes