devpi-web 5.0.1__tar.gz → 5.0.2__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 (78) hide show
  1. devpi_web-5.0.2/.flake8 +3 -0
  2. {devpi_web-5.0.1 → devpi_web-5.0.2}/CHANGELOG +20 -19
  3. {devpi_web-5.0.1 → devpi_web-5.0.2}/CHANGELOG.short.rst +20 -22
  4. {devpi_web-5.0.1 → devpi_web-5.0.2}/PKG-INFO +22 -23
  5. devpi_web-5.0.2/devpi_web/__init__.py +1 -0
  6. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/indexing.py +37 -23
  7. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/main.py +10 -8
  8. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/null_index.py +1 -3
  9. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/views.py +34 -14
  10. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/whoosh_index.py +83 -43
  11. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web.egg-info/PKG-INFO +22 -23
  12. {devpi_web-5.0.1 → devpi_web-5.0.2}/mypy.ini +7 -12
  13. {devpi_web-5.0.1 → devpi_web-5.0.2}/pyproject.toml +22 -18
  14. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_indexing.py +2 -2
  15. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_views.py +1 -6
  16. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_views_misc.py +3 -12
  17. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_views_search.py +18 -11
  18. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_views_toxresults.py +7 -3
  19. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_whoosh_index.py +42 -27
  20. {devpi_web-5.0.1 → devpi_web-5.0.2}/tox.ini +5 -4
  21. devpi_web-5.0.1/.flake8 +0 -2
  22. devpi_web-5.0.1/devpi_web/__init__.py +0 -1
  23. {devpi_web-5.0.1 → devpi_web-5.0.2}/LICENSE +0 -0
  24. {devpi_web-5.0.1 → devpi_web-5.0.2}/MANIFEST.in +0 -0
  25. {devpi_web-5.0.1 → devpi_web-5.0.2}/README.rst +0 -0
  26. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/clear_index.py +0 -0
  27. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/compat.py +0 -0
  28. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/config.py +0 -0
  29. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/description.py +0 -0
  30. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/doczip.py +0 -0
  31. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/hookspecs.py +0 -0
  32. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/macroregistry.py +0 -0
  33. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/macros.py +0 -0
  34. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/static/common.js +0 -0
  35. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/static/docview.js +0 -0
  36. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/static/favicon.ico +0 -0
  37. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/static/jquery-3.6.0.min.js +0 -0
  38. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/static/search.js +0 -0
  39. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/static/style.css +0 -0
  40. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/doc.pt +0 -0
  41. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/error.pt +0 -0
  42. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/favicon.pt +0 -0
  43. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/footer.pt +0 -0
  44. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/footer_versions.pt +0 -0
  45. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/head.pt +0 -0
  46. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/header.pt +0 -0
  47. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/header_breadcrumbs.pt +0 -0
  48. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/header_search.pt +0 -0
  49. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/header_status.pt +0 -0
  50. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/html_head_css.pt +0 -0
  51. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/html_head_scripts.pt +0 -0
  52. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/index.pt +0 -0
  53. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/logo.pt +0 -0
  54. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/notfound.pt +0 -0
  55. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/project.pt +0 -0
  56. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/query_docs.pt +0 -0
  57. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/root.pt +0 -0
  58. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/root_above_user_index_list.pt +0 -0
  59. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/root_below_user_index_list.pt +0 -0
  60. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/search.pt +0 -0
  61. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/search_help.pt +0 -0
  62. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/status.pt +0 -0
  63. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/status_badge.pt +0 -0
  64. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/toxresult.pt +0 -0
  65. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/toxresults.pt +0 -0
  66. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/user.pt +0 -0
  67. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web/templates/version.pt +0 -0
  68. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web.egg-info/SOURCES.txt +0 -0
  69. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web.egg-info/dependency_links.txt +0 -0
  70. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web.egg-info/entry_points.txt +0 -0
  71. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web.egg-info/requires.txt +0 -0
  72. {devpi_web-5.0.1 → devpi_web-5.0.2}/devpi_web.egg-info/top_level.txt +0 -0
  73. {devpi_web-5.0.1 → devpi_web-5.0.2}/setup.cfg +0 -0
  74. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/conftest.py +0 -0
  75. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_macro_registry.py +0 -0
  76. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_main.py +0 -0
  77. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_theme.py +0 -0
  78. {devpi_web-5.0.1 → devpi_web-5.0.2}/tests/test_views_docs.py +0 -0
@@ -0,0 +1,3 @@
1
+ [flake8]
2
+ # E203 is ignored because we enforce ruff format which contradicts it
3
+ ignore = E203,E501,E704,E741,W503
@@ -2,6 +2,26 @@
2
2
 
3
3
  .. towncrier release notes start
4
4
 
5
+ 5.0.2 (2026-03-17)
6
+ ==================
7
+
8
+ Bug Fixes
9
+ ---------
10
+
11
+ - Prevent double indexing work on first start of freshly initialized/imported instance.
12
+
13
+ - Optimize preparation of mirror project data for indexing.
14
+
15
+ - Consistently use offline mode when indexing. This especially helps performance on startup.
16
+
17
+ - Skip redirect if an installer is detected which is configured without ``+simple`` in the URL.
18
+
19
+ Other Changes
20
+ -------------
21
+
22
+ - Support string timestamps for file history log entries from devpi-server 7.0.0.
23
+
24
+
5
25
  5.0.1 (2025-06-15)
6
26
  ==================
7
27
 
@@ -11,7 +31,6 @@ Bug Fixes
11
31
  - Fix traceback during unpacking of documentation in some cases.
12
32
 
13
33
 
14
-
15
34
  5.0.0 (2025-06-12)
16
35
  ==================
17
36
 
@@ -24,15 +43,11 @@ Deprecations and Removals
24
43
 
25
44
  - Future releases will more often require newer devpi-server than before.
26
45
 
27
-
28
-
29
46
  Bug Fixes
30
47
  ---------
31
48
 
32
49
  - Fix #930: remove remaining uses of unmaintained py library.
33
50
 
34
-
35
-
36
51
  Other Changes
37
52
  -------------
38
53
 
@@ -69,7 +84,6 @@ Other Changes
69
84
  - macros.pt: Move ``versions`` macro to separate ``footer_versions.pt`` template.
70
85
 
71
86
 
72
-
73
87
  4.3.0 (2024-10-16)
74
88
  ==================
75
89
 
@@ -79,7 +93,6 @@ Features
79
93
  - index.pt, project.pt, version.pt: Fix #1062: Added a link to download the documentation as zip-file to the index, project and version view.
80
94
 
81
95
 
82
-
83
96
  4.2.3 (2024-09-19)
84
97
  ==================
85
98
 
@@ -95,7 +108,6 @@ Bug Fixes
95
108
  - Increase threshold for index status from 60 s to 300 s for warnings and from 300 s to 3600 s for fatal.
96
109
 
97
110
 
98
-
99
111
  4.2.2 (2024-04-20)
100
112
  ==================
101
113
 
@@ -109,7 +121,6 @@ Bug Fixes
109
121
  - Fix #970: overwrite fixed html/body heights like ``100%`` in documentation iframe content.
110
122
 
111
123
 
112
-
113
124
  4.2.1 (2023-07-02)
114
125
  ==================
115
126
 
@@ -131,7 +142,6 @@ Features
131
142
 
132
143
  - Add ``--keep-docs-packed`` option.
133
144
 
134
-
135
145
  Bug Fixes
136
146
  ---------
137
147
 
@@ -151,7 +161,6 @@ Bug Fixes
151
161
  4.1.1 (2022-09-28)
152
162
  ==================
153
163
 
154
-
155
164
  Bug Fixes
156
165
  ---------
157
166
 
@@ -170,7 +179,6 @@ Features
170
179
 
171
180
  - project.pt: add navigation links to original project page for mirrors and to "Simple page" of projects.
172
181
 
173
-
174
182
  Bug Fixes
175
183
  ---------
176
184
 
@@ -216,7 +224,6 @@ Bug Fixes
216
224
 
217
225
  - Use a lock file to prevent concurrent unpacking of documentation.
218
226
 
219
-
220
227
  Other Changes
221
228
  -------------
222
229
 
@@ -277,7 +284,6 @@ Deprecations and Removals
277
284
 
278
285
  - Dropped support for Python 2.7.
279
286
 
280
-
281
287
  Features
282
288
  --------
283
289
 
@@ -317,7 +323,6 @@ Features
317
323
 
318
324
  - Indexer backend options can now be specified as a dict in yaml config files.
319
325
 
320
-
321
326
  Bug Fixes
322
327
  ---------
323
328
 
@@ -325,7 +330,6 @@ Bug Fixes
325
330
 
326
331
  - Pass query string on to documentation iframe, this fixes Sphinx search among other things.
327
332
 
328
-
329
333
  Other Changes
330
334
  -------------
331
335
 
@@ -364,7 +368,6 @@ Features
364
368
 
365
369
  - feature #193: adding support for using markdown in package descriptions.
366
370
 
367
-
368
371
  Bug Fixes
369
372
  ---------
370
373
 
@@ -404,7 +407,6 @@ Features
404
407
 
405
408
  - Use ``mirror_web_url_fmt`` in ``get_description`` for mirror indexes.
406
409
 
407
-
408
410
  Bug Fixes
409
411
  ---------
410
412
 
@@ -629,7 +631,6 @@ Bug Fixes
629
631
  - fix notfound-redirect when serving under an outside URL with a sub path
630
632
 
631
633
 
632
-
633
634
  2.2.0
634
635
  =====
635
636
 
@@ -9,6 +9,26 @@ Changelog
9
9
 
10
10
  .. towncrier release notes start
11
11
 
12
+ 5.0.2 (2026-03-17)
13
+ ==================
14
+
15
+ Bug Fixes
16
+ ---------
17
+
18
+ - Prevent double indexing work on first start of freshly initialized/imported instance.
19
+
20
+ - Optimize preparation of mirror project data for indexing.
21
+
22
+ - Consistently use offline mode when indexing. This especially helps performance on startup.
23
+
24
+ - Skip redirect if an installer is detected which is configured without ``+simple`` in the URL.
25
+
26
+ Other Changes
27
+ -------------
28
+
29
+ - Support string timestamps for file history log entries from devpi-server 7.0.0.
30
+
31
+
12
32
  5.0.1 (2025-06-15)
13
33
  ==================
14
34
 
@@ -18,7 +38,6 @@ Bug Fixes
18
38
  - Fix traceback during unpacking of documentation in some cases.
19
39
 
20
40
 
21
-
22
41
  5.0.0 (2025-06-12)
23
42
  ==================
24
43
 
@@ -31,15 +50,11 @@ Deprecations and Removals
31
50
 
32
51
  - Future releases will more often require newer devpi-server than before.
33
52
 
34
-
35
-
36
53
  Bug Fixes
37
54
  ---------
38
55
 
39
56
  - Fix #930: remove remaining uses of unmaintained py library.
40
57
 
41
-
42
-
43
58
  Other Changes
44
59
  -------------
45
60
 
@@ -76,7 +91,6 @@ Other Changes
76
91
  - macros.pt: Move ``versions`` macro to separate ``footer_versions.pt`` template.
77
92
 
78
93
 
79
-
80
94
  4.3.0 (2024-10-16)
81
95
  ==================
82
96
 
@@ -86,7 +100,6 @@ Features
86
100
  - index.pt, project.pt, version.pt: Fix #1062: Added a link to download the documentation as zip-file to the index, project and version view.
87
101
 
88
102
 
89
-
90
103
  4.2.3 (2024-09-19)
91
104
  ==================
92
105
 
@@ -101,18 +114,3 @@ Bug Fixes
101
114
 
102
115
  - Increase threshold for index status from 60 s to 300 s for warnings and from 300 s to 3600 s for fatal.
103
116
 
104
-
105
-
106
- 4.2.2 (2024-04-20)
107
- ==================
108
-
109
- Bug Fixes
110
- ---------
111
-
112
- - style.css: Always let content be full browser height. This also gives more height with some documentation themes when content is short.
113
-
114
- - style.css: set ``scrollbar-gutter: stable`` on ``body`` to prevent jumping content in documentation iframe.
115
-
116
- - Fix #970: overwrite fixed html/body heights like ``100%`` in documentation iframe content.
117
-
118
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devpi-web
3
- Version: 5.0.1
3
+ Version: 5.0.2
4
4
  Summary: devpi-web: a web view for devpi-server
5
5
  Maintainer-email: Florian Schulze <mail@pyfidelity.com>
6
6
  License-Expression: MIT
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
23
24
  Classifier: Programming Language :: Python :: Implementation :: PyPy
24
25
  Classifier: Topic :: Internet :: WWW/HTTP
25
26
  Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
@@ -87,6 +88,26 @@ Changelog
87
88
 
88
89
  .. towncrier release notes start
89
90
 
91
+ 5.0.2 (2026-03-17)
92
+ ==================
93
+
94
+ Bug Fixes
95
+ ---------
96
+
97
+ - Prevent double indexing work on first start of freshly initialized/imported instance.
98
+
99
+ - Optimize preparation of mirror project data for indexing.
100
+
101
+ - Consistently use offline mode when indexing. This especially helps performance on startup.
102
+
103
+ - Skip redirect if an installer is detected which is configured without ``+simple`` in the URL.
104
+
105
+ Other Changes
106
+ -------------
107
+
108
+ - Support string timestamps for file history log entries from devpi-server 7.0.0.
109
+
110
+
90
111
  5.0.1 (2025-06-15)
91
112
  ==================
92
113
 
@@ -96,7 +117,6 @@ Bug Fixes
96
117
  - Fix traceback during unpacking of documentation in some cases.
97
118
 
98
119
 
99
-
100
120
  5.0.0 (2025-06-12)
101
121
  ==================
102
122
 
@@ -109,15 +129,11 @@ Deprecations and Removals
109
129
 
110
130
  - Future releases will more often require newer devpi-server than before.
111
131
 
112
-
113
-
114
132
  Bug Fixes
115
133
  ---------
116
134
 
117
135
  - Fix #930: remove remaining uses of unmaintained py library.
118
136
 
119
-
120
-
121
137
  Other Changes
122
138
  -------------
123
139
 
@@ -154,7 +170,6 @@ Other Changes
154
170
  - macros.pt: Move ``versions`` macro to separate ``footer_versions.pt`` template.
155
171
 
156
172
 
157
-
158
173
  4.3.0 (2024-10-16)
159
174
  ==================
160
175
 
@@ -164,7 +179,6 @@ Features
164
179
  - index.pt, project.pt, version.pt: Fix #1062: Added a link to download the documentation as zip-file to the index, project and version view.
165
180
 
166
181
 
167
-
168
182
  4.2.3 (2024-09-19)
169
183
  ==================
170
184
 
@@ -179,18 +193,3 @@ Bug Fixes
179
193
 
180
194
  - Increase threshold for index status from 60 s to 300 s for warnings and from 300 s to 3600 s for fatal.
181
195
 
182
-
183
-
184
- 4.2.2 (2024-04-20)
185
- ==================
186
-
187
- Bug Fixes
188
- ---------
189
-
190
- - style.css: Always let content be full browser height. This also gives more height with some documentation themes when content is short.
191
-
192
- - style.css: set ``scrollbar-gutter: stable`` on ``body`` to prevent jumping content in documentation iframe.
193
-
194
- - Fix #970: overwrite fixed html/body heights like ``100%`` in documentation iframe content.
195
-
196
-
@@ -0,0 +1 @@
1
+ __version__ = "5.0.2"
@@ -1,10 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from attrs import define
1
4
  from collections.abc import Mapping
2
- from devpi_common.types import ensure_unicode
3
5
  from devpi_common.metadata import get_sorted_versions
6
+ from devpi_common.types import ensure_unicode
4
7
  from devpi_common.validation import normalize_name
5
8
  from devpi_web.config import get_pluginmanager
6
9
  from devpi_web.doczip import Docs
7
- import attr
10
+ from typing import TYPE_CHECKING
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from typing import Any
8
15
 
9
16
 
10
17
  def is_project_cached(stage, project):
@@ -14,7 +21,7 @@ def is_project_cached(stage, project):
14
21
  return True
15
22
 
16
23
 
17
- def preprocess_project(project):
24
+ def preprocess_project(project: ProjectIndexingInfo) -> dict | None:
18
25
  stage = project.stage
19
26
  pm = get_pluginmanager(stage.xom.config)
20
27
  name = normalize_name(project.name)
@@ -25,15 +32,19 @@ def preprocess_project(project):
25
32
  user, index = stage.name.split('/')
26
33
  user = ensure_unicode(user)
27
34
  index = ensure_unicode(index)
28
- if not is_project_cached(stage, name):
29
- result = dict(name=project.name, user=user, index=index)
30
- pm.hook.devpiweb_modify_preprocess_project_result(
31
- project=project, result=result)
32
- return result
33
- stage.offline = True
34
- if not stage.has_project_perstage(name):
35
+ result: dict[str, object]
36
+ if stage.ixconfig["type"] == "mirror":
37
+ stage.offline = True
38
+ if not stage.is_project_cached(name):
39
+ # only index basic info for projects with no downloads
40
+ result = dict(name=project.name, user=user, index=index)
41
+ pm.hook.devpiweb_modify_preprocess_project_result(
42
+ project=project, result=result
43
+ )
44
+ return result
45
+ elif not stage.has_project_perstage(name):
35
46
  # project doesn't exist anymore
36
- return
47
+ return None
37
48
  # metadata_keys is only available on private indexes
38
49
  setuptools_metadata = frozenset(getattr(stage, 'metadata_keys', ()))
39
50
  versions = get_sorted_versions(stage.list_versions_perstage(name))
@@ -49,8 +60,7 @@ def preprocess_project(project):
49
60
  result['doc_version'] = version
50
61
  result['+doczip'] = docs
51
62
  break
52
- else:
53
- assert '+doczip' not in result
63
+ assert "+doczip" not in result
54
64
 
55
65
  result[u'user'] = user
56
66
  result[u'index'] = index
@@ -64,17 +74,18 @@ def preprocess_project(project):
64
74
  return result
65
75
 
66
76
 
67
- @attr.s(slots=True)
68
- class ProjectIndexingInfo(object):
69
- stage = attr.ib()
70
- name = attr.ib(type=str)
77
+ @define
78
+ class ProjectIndexingInfo:
79
+ stage: Any
80
+ name: str
81
+ num_names: int
71
82
 
72
83
  @property
73
- def indexname(self):
84
+ def indexname(self) -> str:
74
85
  return self.stage.name
75
86
 
76
87
  @property
77
- def is_from_mirror(self):
88
+ def is_from_mirror(self) -> bool:
78
89
  return self.stage.ixconfig['type'] == 'mirror'
79
90
 
80
91
 
@@ -92,18 +103,21 @@ def iter_indexes(xom):
92
103
  yield from mirrors
93
104
 
94
105
 
95
- def iter_projects(xom):
106
+ def iter_projects(xom, *, offline=True):
96
107
  for username, index in iter_indexes(xom):
97
108
  stage = xom.model.getstage(username, index)
98
109
  if stage is None: # this is async, so the stage may be gone
99
110
  continue
100
111
  names = stage.list_projects_perstage()
112
+ # only go offline after we got the projects list
113
+ if stage.ixconfig["type"] == "mirror":
114
+ stage.offline = offline
101
115
  if isinstance(names, Mapping):
102
116
  # since devpi-server 6.6.0 mirrors return a mapping where
103
117
  # the un-normalized names are in the values
104
118
  names = names.values()
119
+ num_names = len(names)
105
120
  for name in names:
106
- name = ensure_unicode(name)
107
121
  yield ProjectIndexingInfo(
108
- stage=stage,
109
- name=name)
122
+ stage=stage, name=ensure_unicode(name), num_names=num_names
123
+ )
@@ -101,9 +101,8 @@ def status_info(request):
101
101
  msgs = []
102
102
  pm = request.registry['devpiweb-pluginmanager']
103
103
  for result in pm.hook.devpiweb_get_status_info(request=request):
104
- for msg in result:
105
- msgs.append(msg)
106
- states = set(x['status'] for x in msgs)
104
+ msgs.extend(result)
105
+ states = {x["status"] for x in msgs}
107
106
  if 'fatal' in states:
108
107
  status = 'fatal'
109
108
  short_msg = 'fatal'
@@ -284,9 +283,11 @@ def devpiserver_mirror_initialnames(stage, projectnames):
284
283
  # since devpi-server 6.6.0 mirrors return a mapping where
285
284
  # the un-normalized names are in the values
286
285
  projectnames = projectnames.values()
286
+ num_names = len(projectnames)
287
287
  ix.update_projects(
288
- ProjectIndexingInfo(stage=stage, name=name)
289
- for name in projectnames)
288
+ ProjectIndexingInfo(stage=stage, name=name, num_names=num_names)
289
+ for name in projectnames
290
+ )
290
291
  threadlog.info("finished mirror indexing operation")
291
292
 
292
293
 
@@ -315,14 +316,14 @@ def delete_project(stage, name):
315
316
  if stage is None:
316
317
  return
317
318
  ix = get_indexer(stage.xom)
318
- ix.delete_projects([ProjectIndexingInfo(stage=stage, name=name)])
319
+ ix.delete_projects([ProjectIndexingInfo(stage=stage, name=name, num_names=1)])
319
320
 
320
321
 
321
322
  def index_project(stage, name):
322
323
  if stage is None:
323
324
  return
324
325
  ix = get_indexer(stage.xom)
325
- ix.update_projects([ProjectIndexingInfo(stage=stage, name=name)])
326
+ ix.update_projects([ProjectIndexingInfo(stage=stage, name=name, num_names=1)])
326
327
 
327
328
 
328
329
  @devpiserver_hookimpl
@@ -338,9 +339,10 @@ def devpiserver_on_upload(stage, project, version, link):
338
339
  @devpiserver_hookimpl
339
340
  def devpiserver_on_changed_versiondata(stage, project, version, metadata):
340
341
  if stage is None:
341
- # TODO we don't have enough info to delete the project
342
342
  return
343
343
  if not metadata:
344
+ if stage.ixconfig["type"] == "mirror":
345
+ stage.offline = True
344
346
  if is_project_cached(stage, project) and not stage.has_project_perstage(project):
345
347
  delete_project(stage, project)
346
348
  return
@@ -24,9 +24,7 @@ class Index:
24
24
  return dict(items=[])
25
25
 
26
26
  def _matcher(self, searchinfo):
27
- items = list(
28
- re.escape(x)
29
- for x in searchinfo['fields']['name'])
27
+ items = [re.escape(x) for x in searchinfo["fields"]["name"]]
30
28
  return re.compile('|'.join(items)).search
31
29
 
32
30
  def query_packages(self, searchinfo, sro):
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from attrs import define
2
4
  from defusedxml.xmlrpc import DefusedExpatParser
3
5
  from devpi_common.metadata import Version
@@ -8,8 +10,10 @@ from devpi_common.validation import normalize_name
8
10
  from devpi_common.viewhelp import iter_toxresults
9
11
  from devpi_server.log import threadlog as log
10
12
  from devpi_server.readonly import SeqViewReadonly
11
- from devpi_server.views import StatusView, url_for_entrypath
12
13
  from devpi_server.views import PyPIView
14
+ from devpi_server.views import StatusView
15
+ from devpi_server.views import is_simple_json_or_requested_by_installer
16
+ from devpi_server.views import url_for_entrypath
13
17
  from devpi_web.description import get_description
14
18
  from devpi_web.doczip import Docs
15
19
  from devpi_web.doczip import docs_exist
@@ -19,24 +23,32 @@ from devpi_web.doczip import docs_file_path
19
23
  from devpi_web.indexing import is_project_cached
20
24
  from devpi_web.main import navigation_version
21
25
  from email.utils import parsedate
22
- from io import TextIOWrapper
23
26
  from html import escape
24
- from operator import attrgetter, itemgetter
27
+ from io import TextIOWrapper
28
+ from operator import attrgetter
29
+ from operator import itemgetter
25
30
  from pyramid.decorator import reify
26
- from pyramid.httpexceptions import HTTPBadGateway, HTTPError
27
- from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31
+ from pyramid.httpexceptions import HTTPBadGateway
32
+ from pyramid.httpexceptions import HTTPError
33
+ from pyramid.httpexceptions import HTTPFound
34
+ from pyramid.httpexceptions import HTTPNotFound
28
35
  from pyramid.httpexceptions import HTTPNotModified
29
36
  from pyramid.httpexceptions import default_exceptionresponse_view
30
37
  from pyramid.interfaces import IRoutesMapper
31
- from pyramid.response import FileResponse, Response
32
- from pyramid.view import notfound_view_config, view_config
38
+ from pyramid.response import FileResponse
39
+ from pyramid.response import Response
40
+ from pyramid.view import notfound_view_config
41
+ from pyramid.view import view_config
33
42
  from time import gmtime
34
43
  from typing import Any
35
- from xmlrpc.client import Fault, Unmarshaller, dumps
44
+ from xmlrpc.client import Fault
45
+ from xmlrpc.client import Unmarshaller
46
+ from xmlrpc.client import dumps
36
47
  import functools
37
48
  import json
38
49
  import mimetypes
39
50
 
51
+
40
52
  seq_types = (list, tuple, SeqViewReadonly)
41
53
 
42
54
 
@@ -106,6 +118,8 @@ def ServerViews(context, request):
106
118
 
107
119
  def tween_trailing_slash_redirect(handler, registry):
108
120
  def request_trailing_slash_redirect_handler(request):
121
+ if is_simple_json_or_requested_by_installer(request):
122
+ return handler(request)
109
123
  if request.path == "/":
110
124
  return handler(request)
111
125
  if request.method != "GET":
@@ -298,7 +312,8 @@ def sizeof_fmt(num):
298
312
 
299
313
  def format_timetuple(tt):
300
314
  if tt is not None:
301
- return "{0}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}".format(*tt)
315
+ return "{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(*tt)
316
+ return None
302
317
 
303
318
 
304
319
  def format_timestamp(ts, unset_value=None):
@@ -312,6 +327,12 @@ def format_timestamp(ts, unset_value=None):
312
327
  return ts
313
328
 
314
329
 
330
+ def format_when(when):
331
+ if isinstance(when, str):
332
+ return when.replace("T", " ").rstrip("Z")
333
+ return format_timetuple(when)
334
+
335
+
315
336
  _what_map = dict(
316
337
  overwrite="Replaced",
317
338
  push="Pushed",
@@ -323,7 +344,7 @@ def make_history_view_item(request, log_item):
323
344
  result['what'] = _what_map.get(log_item['what'], log_item['what'])
324
345
  result['who'] = log_item['who']
325
346
  if log_item['what'] != 'overwrite':
326
- result['when'] = format_timetuple(log_item['when'])
347
+ result["when"] = format_when(log_item["when"])
327
348
  for key in ('dst', 'src'):
328
349
  if key in log_item:
329
350
  result[key] = dict(
@@ -972,8 +993,7 @@ def batch_list(num, current, left=3, right=3):
972
993
  raise ValueError("Current position (%s) can't be greater than total (%s)." % (current, num))
973
994
  result.append(0)
974
995
  first = current - left
975
- if first < 1:
976
- first = 1
996
+ first = max(first, 1)
977
997
  if first > 1:
978
998
  result.append(None)
979
999
  last = current + right + 1
@@ -1093,7 +1113,7 @@ class SearchView:
1093
1113
  highlight = None
1094
1114
  if text_type == 'project':
1095
1115
  continue
1096
- elif text_type in ('title', 'page'):
1116
+ if text_type in ("title", "page"):
1097
1117
  docs = self.get_docs(stage, data)
1098
1118
  entry = docs[sub_data['text_path']]
1099
1119
  text = entry['text']
@@ -1199,7 +1219,7 @@ class SearchView:
1199
1219
  else:
1200
1220
  fields = data
1201
1221
  operator = "and"
1202
- log.debug("xmlrpc_search {0}".format((fields, operator)))
1222
+ log.debug("xmlrpc_search %s", (fields, operator))
1203
1223
  return dict(fields=fields, operator=operator)
1204
1224
 
1205
1225
  @view_config(