devpi-server 6.18.0__tar.gz → 6.19.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 (135) hide show
  1. {devpi_server-6.18.0 → devpi_server-6.19.0}/CHANGELOG +29 -0
  2. {devpi_server-6.18.0 → devpi_server-6.19.0}/CHANGELOG.short.rst +29 -18
  3. {devpi_server-6.18.0 → devpi_server-6.19.0}/PKG-INFO +30 -19
  4. devpi_server-6.19.0/devpi_server/__init__.py +1 -0
  5. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/auth.py +23 -11
  6. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/config.py +58 -18
  7. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/exceptions.py +10 -4
  8. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/hookspecs.py +8 -0
  9. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/httpclient.py +2 -1
  10. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/model.py +9 -6
  11. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/replica.py +116 -30
  12. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/views.py +42 -5
  13. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server.egg-info/PKG-INFO +30 -19
  14. {devpi_server-6.18.0 → devpi_server-6.19.0}/pyproject.toml +13 -0
  15. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/functional.py +2 -0
  16. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/plugin.py +11 -11
  17. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_auth.py +49 -0
  18. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_config.py +21 -0
  19. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_replica.py +3 -3
  20. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_replica_functional.py +10 -2
  21. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_views.py +4 -0
  22. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_views_patch.py +55 -0
  23. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_views_status.py +6 -2
  24. devpi_server-6.18.0/devpi_server/__init__.py +0 -1
  25. {devpi_server-6.18.0 → devpi_server-6.19.0}/.flake8 +0 -0
  26. {devpi_server-6.18.0 → devpi_server-6.19.0}/LICENSE +0 -0
  27. {devpi_server-6.18.0 → devpi_server-6.19.0}/MANIFEST.in +0 -0
  28. {devpi_server-6.18.0 → devpi_server-6.19.0}/README.rst +0 -0
  29. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/__main__.py +0 -0
  30. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/auth_basic.py +0 -0
  31. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/auth_devpi.py +0 -0
  32. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/__init__.py +0 -0
  33. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/crontab.template +0 -0
  34. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/devpi.service.template +0 -0
  35. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/launchd-macos.txt.template +0 -0
  36. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/nginx-devpi-caching-http.conf.template +0 -0
  37. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/nginx-devpi-caching-location.conf.template +0 -0
  38. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/nginx-devpi-caching-proxy.conf.template +0 -0
  39. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/nginx-devpi-caching-server.conf.template +0 -0
  40. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/nginx-devpi.conf.template +0 -0
  41. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/supervisor-devpi.conf.template +0 -0
  42. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/supervisord.conf.template +0 -0
  43. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/cfg/windows-service.txt.template +0 -0
  44. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/compat.py +0 -0
  45. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/filestore.py +0 -0
  46. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/filestore_db.py +0 -0
  47. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/filestore_fs.py +0 -0
  48. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/filestore_fs_base.py +0 -0
  49. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/filestore_hash_hl.py +0 -0
  50. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/fileutil.py +0 -0
  51. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/fsck.py +0 -0
  52. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/genconfig.py +0 -0
  53. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/htmlpage.py +0 -0
  54. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/importexport.py +0 -0
  55. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/init.py +0 -0
  56. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/interfaces.py +0 -0
  57. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/keyfs.py +0 -0
  58. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/keyfs_sqlite.py +0 -0
  59. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/keyfs_sqlite_fs.py +0 -0
  60. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/keyfs_types.py +0 -0
  61. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/log.py +0 -0
  62. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/main.py +0 -0
  63. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/markers.py +0 -0
  64. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/middleware.py +0 -0
  65. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/mirror.py +0 -0
  66. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/mythread.py +0 -0
  67. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/normalized.py +0 -0
  68. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/passwd.py +0 -0
  69. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/py.typed +0 -0
  70. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/readonly.py +0 -0
  71. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/sizeof.py +0 -0
  72. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server/view_auth.py +0 -0
  73. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server.egg-info/SOURCES.txt +0 -0
  74. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server.egg-info/dependency_links.txt +0 -0
  75. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server.egg-info/entry_points.txt +0 -0
  76. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server.egg-info/requires.txt +0 -0
  77. {devpi_server-6.18.0 → devpi_server-6.19.0}/devpi_server.egg-info/top_level.txt +0 -0
  78. {devpi_server-6.18.0 → devpi_server-6.19.0}/mypy.ini +0 -0
  79. {devpi_server-6.18.0 → devpi_server-6.19.0}/pytest_devpi_server/__init__.py +0 -0
  80. {devpi_server-6.18.0 → devpi_server-6.19.0}/setup.cfg +0 -0
  81. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/__init__.py +0 -0
  82. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/conftest.py +0 -0
  83. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/example.py +0 -0
  84. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/badindexname/dataindex.json +0 -0
  85. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/badusername/dataindex.json +0 -0
  86. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/basescycle/dataindex.json +0 -0
  87. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/createdmodified/dataindex.json +0 -0
  88. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/deletedbase/dataindex.json +0 -0
  89. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/mirrordata/dataindex.json +0 -0
  90. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/mirrordata/root/pypi/dddttt/0.1.dev1/dddttt-0.1.dev1.tar.gz +0 -0
  91. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/modifiedpypi/dataindex.json +0 -0
  92. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/nocreatedmodified/dataindex.json +0 -0
  93. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/normalization/dataindex.json +0 -0
  94. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/normalization/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  95. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/normalization_merge/dataindex.json +0 -0
  96. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello-pkg/hello.pkg-1.1.tar.gz +0 -0
  97. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/normalization_merge/root/dev/hello.pkg/hello.pkg-1.0.tar.gz +0 -0
  98. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/norootpypi/dataindex.json +0 -0
  99. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/nouser/dataindex.json +0 -0
  100. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/removedindexplugin/dataindex.json +0 -0
  101. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/removedindexplugin/user/dev/pkg/pkg-1.0.tar.gz +0 -0
  102. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/dataindex.json +0 -0
  103. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/0.9/hello-0.9.tar.gz +0 -0
  104. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/toxresult_naming_scheme/root/dev/hello/sha256=ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73/hello-0.9.tar.gz.toxresult0 +0 -0
  105. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/importexportdata/toxresult_upload_default/dataindex.json +0 -0
  106. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/py.typed +0 -0
  107. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/reqmock.py +0 -0
  108. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/simpypi.py +0 -0
  109. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_authcheck.py +0 -0
  110. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_conftest.py +0 -0
  111. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_filestore.py +0 -0
  112. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_filestore_fs.py +0 -0
  113. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_fileutil.py +0 -0
  114. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_fsck.py +0 -0
  115. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_genconfig.py +0 -0
  116. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_importexport.py +0 -0
  117. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_keyfs.py +0 -0
  118. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_log.py +0 -0
  119. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_main.py +0 -0
  120. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_mirror.py +0 -0
  121. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_mirror_no_project_list.py +0 -0
  122. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_model.py +0 -0
  123. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_mythread.py +0 -0
  124. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_nginx.py +0 -0
  125. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_nginx_replica.py +0 -0
  126. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_permissions.py +0 -0
  127. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_readonly.py +0 -0
  128. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_stage_customizer.py +0 -0
  129. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_streaming.py +0 -0
  130. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_streaming_nginx.py +0 -0
  131. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_streaming_replica.py +0 -0
  132. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_streaming_replica_nginx.py +0 -0
  133. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_view_auth.py +0 -0
  134. {devpi_server-6.18.0 → devpi_server-6.19.0}/test_devpi_server/test_views_push_external.py +0 -0
  135. {devpi_server-6.18.0 → devpi_server-6.19.0}/tox.ini +0 -0
@@ -2,6 +2,35 @@
2
2
 
3
3
  .. towncrier release notes start
4
4
 
5
+ 6.19.0 (2026-02-06)
6
+ ===================
7
+
8
+ Features
9
+ --------
10
+
11
+ - Add ``--autocreate-users`` server option.
12
+ Automatically creates users that don't exist in devpi, but have successfully authenticated via an authentication plugin.
13
+ A typical example of when to enable this would be when authenticating via an LDAP directory.
14
+ Automatically created users do not have passwords, and have no password hash to prevent local authentication.
15
+
16
+ - 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.
17
+
18
+ - 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.
19
+
20
+ - Add new ``devpiserver_user_created`` hook which can be used to create default indexes or other setup for newly created users.
21
+
22
+ Bug Fixes
23
+ ---------
24
+
25
+ - Fix ``+status`` json encoding errors by making sure the ``FatalResponse.url`` attribute is a string.
26
+
27
+ - Ignore existing unknown index options from uninstalled plugins when patching other options with ``+=`` and ``-=``.
28
+
29
+ - Fix removal with ``-=`` of index options with default values from ``devpiserver_indexconfig_defaults`` hooks.
30
+
31
+ - Fix #1110: a list for the ``listen`` option in a config file stopped working in 6.18.0.
32
+
33
+
5
34
  6.18.0 (2026-01-27)
6
35
  ===================
7
36
 
@@ -9,6 +9,35 @@ Changelog
9
9
 
10
10
  .. towncrier release notes start
11
11
 
12
+ 6.19.0 (2026-02-06)
13
+ ===================
14
+
15
+ Features
16
+ --------
17
+
18
+ - Add ``--autocreate-users`` server option.
19
+ Automatically creates users that don't exist in devpi, but have successfully authenticated via an authentication plugin.
20
+ A typical example of when to enable this would be when authenticating via an LDAP directory.
21
+ Automatically created users do not have passwords, and have no password hash to prevent local authentication.
22
+
23
+ - 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.
24
+
25
+ - 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.
26
+
27
+ - Add new ``devpiserver_user_created`` hook which can be used to create default indexes or other setup for newly created users.
28
+
29
+ Bug Fixes
30
+ ---------
31
+
32
+ - Fix ``+status`` json encoding errors by making sure the ``FatalResponse.url`` attribute is a string.
33
+
34
+ - Ignore existing unknown index options from uninstalled plugins when patching other options with ``+=`` and ``-=``.
35
+
36
+ - Fix removal with ``-=`` of index options with default values from ``devpiserver_indexconfig_defaults`` hooks.
37
+
38
+ - Fix #1110: a list for the ``listen`` option in a config file stopped working in 6.18.0.
39
+
40
+
12
41
  6.18.0 (2026-01-27)
13
42
  ===================
14
43
 
@@ -117,21 +146,3 @@ Features
117
146
 
118
147
  - Add ``--connection-limit`` option to devpi-server passed on to waitress.
119
148
 
120
-
121
- 6.14.0 (2024-10-16)
122
- ===================
123
-
124
- Features
125
- --------
126
-
127
- - Allow pushing of versions which only have documentation and no releases.
128
-
129
- - Allow pushing of release files only with no documentation. Requires devpi-client 7.2.0.
130
-
131
- - Allow pushing of documentation only with no release files. Requires devpi-client 7.2.0.
132
-
133
- Bug Fixes
134
- ---------
135
-
136
- - 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.
137
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devpi-server
3
- Version: 6.18.0
3
+ Version: 6.19.0
4
4
  Summary: devpi-server: reliable, private, and pypi.org caching server
5
5
  Maintainer-email: Florian Schulze <mail@pyfidelity.com>
6
6
  License-Expression: MIT
@@ -122,6 +122,35 @@ Changelog
122
122
 
123
123
  .. towncrier release notes start
124
124
 
125
+ 6.19.0 (2026-02-06)
126
+ ===================
127
+
128
+ Features
129
+ --------
130
+
131
+ - Add ``--autocreate-users`` server option.
132
+ Automatically creates users that don't exist in devpi, but have successfully authenticated via an authentication plugin.
133
+ A typical example of when to enable this would be when authenticating via an LDAP directory.
134
+ Automatically created users do not have passwords, and have no password hash to prevent local authentication.
135
+
136
+ - 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.
137
+
138
+ - 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.
139
+
140
+ - Add new ``devpiserver_user_created`` hook which can be used to create default indexes or other setup for newly created users.
141
+
142
+ Bug Fixes
143
+ ---------
144
+
145
+ - Fix ``+status`` json encoding errors by making sure the ``FatalResponse.url`` attribute is a string.
146
+
147
+ - Ignore existing unknown index options from uninstalled plugins when patching other options with ``+=`` and ``-=``.
148
+
149
+ - Fix removal with ``-=`` of index options with default values from ``devpiserver_indexconfig_defaults`` hooks.
150
+
151
+ - Fix #1110: a list for the ``listen`` option in a config file stopped working in 6.18.0.
152
+
153
+
125
154
  6.18.0 (2026-01-27)
126
155
  ===================
127
156
 
@@ -230,21 +259,3 @@ Features
230
259
 
231
260
  - Add ``--connection-limit`` option to devpi-server passed on to waitress.
232
261
 
233
-
234
- 6.14.0 (2024-10-16)
235
- ===================
236
-
237
- Features
238
- --------
239
-
240
- - Allow pushing of versions which only have documentation and no releases.
241
-
242
- - Allow pushing of release files only with no documentation. Requires devpi-client 7.2.0.
243
-
244
- - Allow pushing of documentation only with no release files. Requires devpi-client 7.2.0.
245
-
246
- Bug Fixes
247
- ---------
248
-
249
- - 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.
250
-
@@ -0,0 +1 @@
1
+ __version__ = "6.19.0"
@@ -24,6 +24,7 @@ class Auth:
24
24
  self.serializer = itsdangerous.TimedSerializer(secret)
25
25
  self.hook = xom.config.hook.devpiserver_auth_request
26
26
  self.legacy_hook = xom.config.hook.devpiserver_auth_user
27
+ self.autocreate_users = xom.config.autocreate_users
27
28
 
28
29
  @property
29
30
  def model(self):
@@ -55,18 +56,27 @@ class Auth:
55
56
  return dict(status="ok", groups=sorted(
56
57
  set(itertools.chain.from_iterable(groups))))
57
58
 
58
- def _validate(self, authuser, authpassword, request=None):
59
- """ Validates user credentials.
60
-
61
- If authentication plugins are installed, they will be queried.
59
+ def _autocreate_user(self, authuser):
60
+ if self.model.get_user(authuser) is None:
61
+ # Autocreated users will be authenticated via plugin, and so no
62
+ # valid password should be stored.
63
+ self.model.create_user(authuser, password=None)
64
+ threadlog.debug("automatically created user %r", authuser)
62
65
 
63
- If no user can be found, returns None.
64
- If the credentials are wrong, returns False.
65
- On success a list of group names the user is member of will be
66
- returned. If the plugins don't return any groups, then the list
67
- will be empty.
68
- The 'root' user is always authenticated with the devpi-server
69
- credentials, never by plugins.
66
+ def _validate(self, authuser, authpassword, request=None):
67
+ """Validates user credentials.
68
+
69
+ If authentication plugins are installed, they will be queried.
70
+
71
+ If no user can be found, returns None.
72
+ If the credentials are wrong, returns False.
73
+ On success a list of group names the user is member of will be
74
+ returned. If the plugins don't return any groups, then the list
75
+ will be empty.
76
+ If autocreate-users is enabled, the new user is created, if it
77
+ doesn't already exist.
78
+ The 'root' user is always authenticated with the devpi-server
79
+ credentials, never by plugins.
70
80
  """
71
81
  user = self.model.get_user(authuser)
72
82
  is_root = authuser == 'root'
@@ -77,6 +87,8 @@ class Auth:
77
87
  if result is not None:
78
88
  if result["status"] != "ok":
79
89
  return dict(status="reject")
90
+ if self.autocreate_users:
91
+ self._autocreate_user(authuser)
80
92
  # plugins may never return from_user_object
81
93
  result.pop('from_user_object', None)
82
94
  return result
@@ -6,7 +6,6 @@ from .interfaces import IIOFileFactory
6
6
  from .log import threadlog
7
7
  from devpi_common.types import cached_property
8
8
  from devpi_common.url import URL
9
- from functools import partial
10
9
  from operator import itemgetter
11
10
  from pathlib import Path
12
11
  from pluggy import HookimplMarker
@@ -29,6 +28,8 @@ import warnings
29
28
  if TYPE_CHECKING:
30
29
  from .interfaces import IStorageConnection4
31
30
  from collections.abc import Callable
31
+ from collections.abc import MutableMapping
32
+ from typing import Any
32
33
 
33
34
 
34
35
  log = threadlog
@@ -403,6 +404,20 @@ def add_permission_options(parser, pluginmanager):
403
404
  "explicitly if wanted.")
404
405
 
405
406
 
407
+ def add_autocreate_users_options(
408
+ parser,
409
+ pluginmanager, # noqa: ARG001 - call convention
410
+ ):
411
+ parser.addoption(
412
+ "--autocreate-users",
413
+ action="store_true",
414
+ default=False,
415
+ help="when using an authentication plugin, automatically create new "
416
+ "users that authenticate, but don't already exist. This is "
417
+ "helpful for deployments making use of LDAP authentication.",
418
+ )
419
+
420
+
406
421
  def addoptions(parser, pluginmanager):
407
422
  add_help_option(parser, pluginmanager)
408
423
  add_configfile_option(parser, pluginmanager)
@@ -441,6 +456,9 @@ def addoptions(parser, pluginmanager):
441
456
  add_permission_options(
442
457
  parser.addgroup("permission options"),
443
458
  pluginmanager)
459
+ add_autocreate_users_options(
460
+ parser.addgroup("autocreate users options"), pluginmanager
461
+ )
444
462
 
445
463
 
446
464
  def try_argcomplete(parser):
@@ -514,21 +532,31 @@ def load_config_file(config_file):
514
532
  return config.data
515
533
 
516
534
 
517
- def default_getter(name, config_options, environ):
518
- if name is None:
519
- return
520
- if name == "serverdir":
521
- if "DEVPI_SERVERDIR" in environ:
535
+ class ArgumentDefaultGetter:
536
+ used_config_options: set[str]
537
+
538
+ def __init__(
539
+ self, *, config_options: dict[str, Any], environ: MutableMapping[str, Any]
540
+ ):
541
+ self.config_options = config_options
542
+ self.environ = environ
543
+ self.used_config_options = set()
544
+
545
+ def __call__(self, name: str | None) -> Any | None:
546
+ if name is None:
547
+ return None
548
+ if name == "serverdir" and "DEVPI_SERVERDIR" in self.environ:
522
549
  log.warning(
523
550
  "Using deprecated DEVPI_SERVERDIR environment variable. "
524
551
  "You should switch to use DEVPISERVER_SERVERDIR.")
525
- return environ["DEVPI_SERVERDIR"]
526
- envname = "DEVPISERVER_%s" % name.replace('-', '_').upper()
527
- if envname in environ:
528
- value = environ[envname]
529
- if value:
530
- return value
531
- return config_options[name]
552
+ return self.environ["DEVPI_SERVERDIR"]
553
+ envname = f"DEVPISERVER_{name.replace('-', '_').upper()}"
554
+ if envname in self.environ:
555
+ value = self.environ[envname]
556
+ if value:
557
+ return value
558
+ self.used_config_options.add(name)
559
+ return self.config_options[name]
532
560
 
533
561
 
534
562
  def parseoptions(pluginmanager, argv, parser=None):
@@ -553,10 +581,9 @@ def parseoptions(pluginmanager, argv, parser=None):
553
581
  "Error in config file '%s':\n %s", config_file, e
554
582
  )
555
583
  sys.exit(4)
556
- defaultget = partial(
557
- default_getter,
558
- config_options=config_options,
559
- environ=os.environ)
584
+ defaultget = ArgumentDefaultGetter(
585
+ config_options=config_options, environ=os.environ
586
+ )
560
587
  parser.post_process_actions(defaultget=defaultget)
561
588
  # the getattr is a workaround for a problem with 3.14t-dev
562
589
  # weirdly enough it does not happen with 3.14-dev
@@ -564,6 +591,11 @@ def parseoptions(pluginmanager, argv, parser=None):
564
591
  parser.print_help()
565
592
  parser.exit()
566
593
  args = parser.parse_args(argv[1:])
594
+ if unknown_keys := set(config_options).difference(defaultget.used_config_options):
595
+ exec_name = Path(argv[0]).name or "unknown"
596
+ for unknown_key in unknown_keys:
597
+ msg = f"Unknown {exec_name} option {unknown_key!r} in {config_file}"
598
+ log.warning(msg)
567
599
  config = Config(args, pluginmanager=pluginmanager)
568
600
  return config
569
601
 
@@ -609,7 +641,11 @@ class MyArgumentParser(argparse.ArgumentParser):
609
641
  except KeyError:
610
642
  pass
611
643
  else:
612
- if isinstance(action, argparse._StoreTrueAction):
644
+ if isinstance(action, argparse._AppendAction) and isinstance(
645
+ default, list
646
+ ):
647
+ pass
648
+ elif isinstance(action, argparse._StoreTrueAction):
613
649
  default = bool(strtobool(default))
614
650
  elif isinstance(action, argparse._StoreFalseAction):
615
651
  default = not bool(strtobool(default))
@@ -987,6 +1023,10 @@ class Config:
987
1023
  rm = frozenset(x.strip() for x in rm.split(','))
988
1024
  return rm
989
1025
 
1026
+ @property
1027
+ def autocreate_users(self):
1028
+ return getattr(self.args, "autocreate_users", False)
1029
+
990
1030
  @property
991
1031
  def wait_for_events(self):
992
1032
  return getattr(self.args, 'wait_for_events', False)
@@ -8,8 +8,7 @@ class LazyExceptionFormatter:
8
8
  self.e = e
9
9
 
10
10
  def __str__(self):
11
- return ''.join(traceback.format_exception(
12
- self.e.__class__, self.e, self.e.__traceback__)).strip()
11
+ return format_exception(self.e)
13
12
 
14
13
 
15
14
  class LazyExceptionOnlyFormatter:
@@ -19,8 +18,15 @@ class LazyExceptionOnlyFormatter:
19
18
  self.e = e
20
19
 
21
20
  def __str__(self):
22
- return ''.join(traceback.format_exception_only(
23
- self.e.__class__, self.e)).strip()
21
+ return format_exception_only(self.e)
22
+
23
+
24
+ def format_exception(e):
25
+ return "".join(traceback.format_exception(e.__class__, e, e.__traceback__)).strip()
26
+
27
+
28
+ def format_exception_only(e):
29
+ return "".join(traceback.format_exception_only(e.__class__, e)).strip()
24
30
 
25
31
 
26
32
  def lazy_format_exception(e):
@@ -98,6 +98,14 @@ def devpiserver_mirror_initialnames(stage, projectnames):
98
98
  """
99
99
 
100
100
 
101
+ @hookspec
102
+ def devpiserver_user_created(user):
103
+ """called when a user was successfully created.
104
+
105
+ - user is an instance of model.User
106
+ """
107
+
108
+
101
109
  @hookspec
102
110
  def devpiserver_stage_created(stage):
103
111
  """ called when a stage was successfully created. (both for replica and a primary)
@@ -41,9 +41,10 @@ def get_caller_location(stacklevel: int = 2) -> str:
41
41
 
42
42
  class FatalResponse:
43
43
  status_code = -1
44
+ url: str
44
45
 
45
46
  def __init__(self, url: URL | str, reason: str):
46
- self.url = url
47
+ self.url = str(url)
47
48
  self.reason_phrase = reason
48
49
  self.status = self.status_code
49
50
 
@@ -187,6 +187,8 @@ class RootModel:
187
187
  threadlog.info("created user %r with email %r" % (username, kwargs["email"]))
188
188
  else:
189
189
  threadlog.info("created user %r" % username)
190
+ # Call any user created hooks, passing along the newly created user object.
191
+ self.xom.config.hook.devpiserver_user_created(user=user)
190
192
  return user
191
193
 
192
194
  def create_stage(self, user, index, type="stage", **kwargs):
@@ -786,12 +788,13 @@ class BaseStage:
786
788
  % (index_type, ", ".join(sorted(conflicting))))
787
789
  for key, value in defaults.items():
788
790
  new_value = kwargs.pop(key, value)
789
- if isinstance(value, bool):
790
- new_value = ensure_boolean(new_value)
791
- elif isinstance(value, ACLList):
792
- new_value = ensure_acl_list(new_value)
793
- elif isinstance(value, (list, tuple, set)):
794
- new_value = ensure_list(new_value)
791
+ if new_value is not RemoveValue:
792
+ if isinstance(value, bool):
793
+ new_value = ensure_boolean(new_value)
794
+ elif isinstance(value, ACLList):
795
+ new_value = ensure_acl_list(new_value)
796
+ elif isinstance(value, (list, tuple, set)):
797
+ new_value = ensure_list(new_value)
795
798
  ixconfig.setdefault(key, new_value)
796
799
  # XXX backward compatibility for old exports where these could appear
797
800
  # on mirror indexes