devpi-web 4.2.2__tar.gz → 4.2.3__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 (62) hide show
  1. {devpi_web-4.2.2 → devpi-web-4.2.3}/CHANGELOG +16 -0
  2. {devpi_web-4.2.2 → devpi-web-4.2.3}/PKG-INFO +17 -32
  3. devpi-web-4.2.3/devpi_web/__init__.py +1 -0
  4. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/clear_index.py +1 -1
  5. devpi-web-4.2.3/devpi_web/compat.py +42 -0
  6. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/doczip.py +11 -6
  7. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/main.py +3 -1
  8. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/views.py +96 -47
  9. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/whoosh_index.py +19 -14
  10. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web.egg-info/PKG-INFO +17 -32
  11. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web.egg-info/SOURCES.txt +1 -1
  12. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web.egg-info/requires.txt +1 -0
  13. devpi-web-4.2.3/pyproject.toml +83 -0
  14. {devpi_web-4.2.2 → devpi-web-4.2.3}/setup.py +2 -1
  15. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/conftest.py +8 -0
  16. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_indexing.py +14 -10
  17. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_main.py +4 -3
  18. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_views.py +9 -2
  19. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_views_docs.py +3 -3
  20. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_views_misc.py +124 -42
  21. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_views_search.py +3 -2
  22. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_views_toxresults.py +2 -1
  23. {devpi_web-4.2.2 → devpi-web-4.2.3}/tox.ini +12 -2
  24. devpi_web-4.2.2/devpi_web/__init__.py +0 -1
  25. devpi_web-4.2.2/devpi_web/static/style-old.css +0 -847
  26. devpi_web-4.2.2/pyproject.toml +0 -26
  27. {devpi_web-4.2.2 → devpi-web-4.2.3}/.flake8 +0 -0
  28. {devpi_web-4.2.2 → devpi-web-4.2.3}/AUTHORS +0 -0
  29. {devpi_web-4.2.2 → devpi-web-4.2.3}/LICENSE +0 -0
  30. {devpi_web-4.2.2 → devpi-web-4.2.3}/MANIFEST.in +0 -0
  31. {devpi_web-4.2.2 → devpi-web-4.2.3}/README.rst +0 -0
  32. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/config.py +0 -0
  33. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/description.py +0 -0
  34. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/hookspecs.py +0 -0
  35. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/indexing.py +0 -0
  36. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/null_index.py +0 -0
  37. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/static/common.js +0 -0
  38. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/static/docview.js +0 -0
  39. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/static/favicon.ico +0 -0
  40. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/static/jquery-3.6.0.min.js +0 -0
  41. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/static/style.css +0 -0
  42. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/doc.pt +0 -0
  43. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/error.pt +0 -0
  44. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/index.pt +0 -0
  45. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/macros.pt +0 -0
  46. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/notfound.pt +0 -0
  47. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/project.pt +0 -0
  48. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/root.pt +0 -0
  49. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/search.pt +0 -0
  50. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/search_help.pt +0 -0
  51. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/status.pt +0 -0
  52. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/toxresult.pt +0 -0
  53. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/toxresults.pt +0 -0
  54. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/user.pt +0 -0
  55. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web/templates/version.pt +0 -0
  56. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web.egg-info/dependency_links.txt +0 -0
  57. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web.egg-info/entry_points.txt +0 -0
  58. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web.egg-info/not-zip-safe +0 -0
  59. {devpi_web-4.2.2 → devpi-web-4.2.3}/devpi_web.egg-info/top_level.txt +0 -0
  60. {devpi_web-4.2.2 → devpi-web-4.2.3}/setup.cfg +0 -0
  61. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_theme.py +0 -0
  62. {devpi_web-4.2.2 → devpi-web-4.2.3}/tests/test_whoosh_index.py +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  .. towncrier release notes start
4
4
 
5
+ 4.2.3 (2024-09-19)
6
+ ==================
7
+
8
+ Bug Fixes
9
+ ---------
10
+
11
+ - Fix deprecation warnings from devpi-server 6.13.0.
12
+
13
+ - Lazily evaluate file information. Especially with devpi-postgresql this safes many database accesses on most pages.
14
+
15
+ - Guard against missing doczip files, which can happen on replicas during replication.
16
+
17
+ - Increase threshold for index status from 60 s to 300 s for warnings and from 300 s to 3600 s for fatal.
18
+
19
+
20
+
5
21
  4.2.2 (2024-04-20)
6
22
  ==================
7
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-web
3
- Version: 4.2.2
3
+ Version: 4.2.3
4
4
  Summary: devpi-web: a web view for devpi-server
5
5
  Home-page: https://devpi.net
6
6
  Maintainer: Florian Schulze
@@ -31,16 +31,6 @@ Classifier: Programming Language :: Python :: 3.12
31
31
  Requires-Python: >=3.4
32
32
  License-File: LICENSE
33
33
  License-File: AUTHORS
34
- Requires-Dist: Whoosh<3
35
- Requires-Dist: beautifulsoup4!=4.12.1,>=4.3.2
36
- Requires-Dist: defusedxml
37
- Requires-Dist: devpi-server>=5.2.0
38
- Requires-Dist: devpi-common>=3.2.0
39
- Requires-Dist: docutils>=0.11
40
- Requires-Dist: pygments>=1.6
41
- Requires-Dist: pyramid!=1.10a1
42
- Requires-Dist: pyramid-chameleon
43
- Requires-Dist: readme-renderer[md]>=23.0
44
34
 
45
35
  ================================================
46
36
  devpi-web: web interface plugin for devpi-server
@@ -87,6 +77,22 @@ Changelog
87
77
 
88
78
  .. towncrier release notes start
89
79
 
80
+ 4.2.3 (2024-09-19)
81
+ ==================
82
+
83
+ Bug Fixes
84
+ ---------
85
+
86
+ - Fix deprecation warnings from devpi-server 6.13.0.
87
+
88
+ - Lazily evaluate file information. Especially with devpi-postgresql this safes many database accesses on most pages.
89
+
90
+ - Guard against missing doczip files, which can happen on replicas during replication.
91
+
92
+ - Increase threshold for index status from 60 s to 300 s for warnings and from 300 s to 3600 s for fatal.
93
+
94
+
95
+
90
96
  4.2.2 (2024-04-20)
91
97
  ==================
92
98
 
@@ -148,24 +154,3 @@ Bug Fixes
148
154
 
149
155
  - Generalize GET redirect from URLs with trailing slash to ones without for consistency.
150
156
 
151
-
152
- 4.1.0 (2022-08-16)
153
- ==================
154
-
155
- Features
156
- --------
157
-
158
- - Support original project names for mirrors with devpi-server 6.6.0.
159
-
160
- - project.pt: Fix #601: add refresh button to project page.
161
-
162
- - project.pt: add navigation links to original project page for mirrors and to "Simple page" of projects.
163
-
164
-
165
- Bug Fixes
166
- ---------
167
-
168
- - Fix #880: explicitly register views for GET method, so there is a 404 on POSTs, like when using twine with wrong upload url.
169
-
170
- - Fix #867: AttributeError during ``devpi-import`` and maybe other commands.
171
-
@@ -0,0 +1 @@
1
+ __version__ = '4.2.3'
@@ -31,7 +31,7 @@ def clear_index(argv=None):
31
31
  configure_cli_logging(config.args)
32
32
  xom = xom_from_config(config)
33
33
  log = xom.log
34
- log.warn( # noqa: PGH002,G010 - this is actually TagLogger from devpi-server, but devpi-web still needs to support older devpi-server versions
34
+ log.warn( # noqa: G010 - this is actually TagLogger from devpi-server, but devpi-web still needs to support older devpi-server versions
35
35
  "You should stop devpi-server before running this command.")
36
36
  ix = get_indexer(xom)
37
37
  ix.delete_index()
@@ -0,0 +1,42 @@
1
+ try:
2
+ from devpi_server.filestore import get_hashes
3
+
4
+ def get_default_hash_spec(content_or_file):
5
+ return get_hashes(content_or_file).get_default_spec()
6
+ except ImportError:
7
+ from devpi_server.filestore import get_default_hash_spec # noqa: F401
8
+
9
+
10
+ try:
11
+ from devpi_server.main import Fatal
12
+
13
+ def fatal(msg, *, exc=None):
14
+ raise Fatal(msg) from exc
15
+ except ImportError:
16
+ from devpi_server.main import fatal # noqa: F401
17
+
18
+
19
+ def get_entry_hash_spec(entry):
20
+ return (
21
+ entry.best_available_hash_spec
22
+ if hasattr(entry, 'best_available_hash_spec') else
23
+ entry.hash_spec)
24
+
25
+
26
+ def get_entry_hash_value(entry):
27
+ return (
28
+ entry.best_available_hash_value
29
+ if hasattr(entry, 'best_available_hash_value') else
30
+ entry.hash_value)
31
+
32
+
33
+ def read_transaction(keyfs):
34
+ if hasattr(keyfs, 'read_transaction'):
35
+ return keyfs.read_transaction()
36
+ return keyfs.transaction(write=False)
37
+
38
+
39
+ def write_transaction(keyfs):
40
+ if hasattr(keyfs, 'write_transaction'):
41
+ return keyfs.write_transaction()
42
+ return keyfs.transaction(write=True)
@@ -1,22 +1,25 @@
1
- from collections.abc import MutableMapping as DictMixin
2
1
  from bs4 import BeautifulSoup
2
+ from collections.abc import MutableMapping as DictMixin
3
3
  from contextlib import contextmanager
4
4
  from devpi_common.archive import Archive
5
5
  from devpi_common.types import cached_property
6
6
  from devpi_common.validation import normalize_name
7
7
  from devpi_server.log import threadlog
8
+ from devpi_web.compat import get_entry_hash_spec
9
+ import json
10
+ import py
11
+
12
+
8
13
  try:
9
14
  import fcntl
10
15
  except ImportError:
11
16
  fcntl = None # type: ignore[assignment]
12
- import json
13
- import py
14
17
 
15
18
 
16
19
  def get_unpack_path(stage, name, version):
17
20
  path = stage.xom.config.args.documentation_path
18
21
  if path is None:
19
- path = stage.keyfs.basedir
22
+ path = py.path.local(stage.keyfs.base_path) if hasattr(stage.keyfs, "base_path") else stage.keyfs.basedir
20
23
  else:
21
24
  path = py.path.local(path)
22
25
  return path.join(
@@ -90,7 +93,7 @@ def unpack_docs(stage, name, version, entry):
90
93
  # we are not losing the original zip file anyway
91
94
  with locked_unpack_path(stage, name, version) as (hash_file, unpack_path):
92
95
  hash_file.seek(0)
93
- if hash_file.read().strip() == entry.hash_spec:
96
+ if hash_file.read().strip() == get_entry_hash_spec(entry):
94
97
  return unpack_path
95
98
  if unpack_path.exists():
96
99
  try:
@@ -98,11 +101,13 @@ def unpack_docs(stage, name, version, entry):
98
101
  except py.error.ENOENT:
99
102
  # there is a rare possibility of a race condition here
100
103
  pass
104
+ if not entry.file_exists():
105
+ return unpack_path
101
106
  with entry.file_open_read() as f:
102
107
  with Archive(f) as archive:
103
108
  archive.extract(unpack_path)
104
109
  hash_file.seek(0)
105
- hash_file.write(entry.hash_spec)
110
+ hash_file.write(get_entry_hash_spec(entry))
106
111
  hash_file.truncate()
107
112
  threadlog.debug("%s: unpacked %s-%s docs to %s",
108
113
  stage.name, name, version, unpack_path)
@@ -1,14 +1,15 @@
1
1
  from chameleon.config import AUTO_RELOAD
2
+ from chameleon.config import DEBUG_MODE
2
3
  from collections.abc import Mapping
3
4
  from devpi_common.metadata import get_latest_version
4
5
  from devpi_common.validation import normalize_name
6
+ from devpi_web.compat import fatal
5
7
  from devpi_web.config import add_indexer_backend_option
6
8
  from devpi_web.config import get_pluginmanager
7
9
  from devpi_web.doczip import remove_docs
8
10
  from devpi_web.indexing import ProjectIndexingInfo
9
11
  from devpi_web.indexing import is_project_cached
10
12
  from devpi_server.log import threadlog
11
- from devpi_server.main import fatal
12
13
  from pluggy import HookimplMarker
13
14
  from pyramid.renderers import get_renderer
14
15
  from pyramid_chameleon.renderer import ChameleonRendererLookup
@@ -121,6 +122,7 @@ def query_docs_html(request):
121
122
 
122
123
  class ThemeChameleonRendererLookup(ChameleonRendererLookup):
123
124
  auto_reload = AUTO_RELOAD
125
+ debug = DEBUG_MODE
124
126
 
125
127
  def __call__(self, info):
126
128
  # if the template exists in the theme, we will use it instead of the
@@ -2,12 +2,15 @@ from defusedxml.xmlrpc import DefusedExpatParser
2
2
  from devpi_common.metadata import Version
3
3
  from devpi_common.metadata import get_pyversion_filetype
4
4
  from devpi_common.metadata import get_sorted_versions
5
+ from devpi_common.types import cached_property
5
6
  from devpi_common.validation import normalize_name
6
7
  from devpi_common.viewhelp import iter_toxresults
7
8
  from devpi_server.log import threadlog as log
8
9
  from devpi_server.readonly import SeqViewReadonly
9
10
  from devpi_server.views import StatusView, url_for_entrypath
10
11
  from devpi_server.views import PyPIView
12
+ from devpi_web.compat import get_entry_hash_spec
13
+ from devpi_web.compat import get_entry_hash_value
11
14
  from devpi_web.description import get_description
12
15
  from devpi_web.doczip import Docs
13
16
  from devpi_web.doczip import docs_exist
@@ -151,7 +154,7 @@ def get_doc_info(context, request, version=None, check_content=True):
151
154
  if not relpath_exists:
152
155
  raise HTTPNotFound("File %s not found in documentation." % relpath)
153
156
  result = dict(
154
- etag=entry.hash_value,
157
+ etag=get_entry_hash_value(entry),
155
158
  relpath=relpath,
156
159
  doc_version=doc_version,
157
160
  version_mismatch=doc_version != navigation_version(context))
@@ -331,47 +334,88 @@ def make_history_view_item(request, log_item):
331
334
  return result
332
335
 
333
336
 
334
- def get_files_info(request, linkstore, show_toxresults=False):
335
- files = []
336
- filedata = linkstore.get_links(rel='releasefile')
337
- if not filedata:
338
- log.warn("project %r version %r has no files",
339
- linkstore.project, linkstore.version)
340
- for link in sorted(filedata, key=attrgetter('basename')):
341
- url = url_for_entrypath(request, link.entrypath)
342
- entry = link.entry
343
- if entry.hash_spec:
344
- url += "#" + entry.hash_spec
345
- py_version, file_type = get_pyversion_filetype(link.basename)
346
- if py_version == 'source':
347
- py_version = ''
348
- size = ''
349
- if entry.file_exists():
350
- size = "%.0f %s" % sizeof_fmt(entry.file_size())
337
+ class FileInfo:
338
+ def __init__(self, request, link, linkstore, show_toxresults):
339
+ self.entry = link.entry
340
+ self.link = link
341
+ self.linkstore = linkstore
342
+ self.request = request
343
+ self.show_toxresults = show_toxresults
344
+
345
+ @cached_property
346
+ def basename(self):
347
+ return self.entry.basename
348
+
349
+ @cached_property
350
+ def dist_type(self):
351
+ (py_version, file_type) = self.pyversion_filetype
352
+ return dist_file_types.get(file_type, '')
353
+
354
+ def get(self, key, default=None):
355
+ return getattr(self, key, default)
356
+
357
+ @cached_property
358
+ def hash_spec(self):
359
+ return get_entry_hash_spec(self.entry)
360
+
361
+ @cached_property
362
+ def history(self):
351
363
  try:
352
- history = [
353
- make_history_view_item(request, x)
354
- for x in link.get_logs()]
364
+ return [
365
+ make_history_view_item(self.request, x)
366
+ for x in self.link.get_logs()]
355
367
  except AttributeError:
356
- history = []
357
- last_modified = format_timetuple(parsedate(entry.last_modified))
358
- fileinfo = dict(
359
- title=link.basename,
360
- url=url,
361
- basename=link.basename,
362
- hash_spec=entry.hash_spec,
363
- dist_type=dist_file_types.get(file_type, ''),
364
- py_version=py_version,
365
- last_modified=last_modified,
366
- history=history,
367
- size=size)
368
- if show_toxresults:
369
- toxresults = get_toxresults_info(linkstore, link)
370
- if toxresults:
371
- fileinfo['toxresults'] = toxresults
372
- fileinfo['toxresults_state'] = get_toxresults_state(toxresults)
373
- files.append(fileinfo)
374
- return files
368
+ return []
369
+
370
+ @cached_property
371
+ def last_modified(self):
372
+ return format_timetuple(parsedate(self.entry.last_modified))
373
+
374
+ @cached_property
375
+ def py_version(self):
376
+ (py_version, file_type) = self.pyversion_filetype
377
+ return '' if py_version == 'source' else py_version
378
+
379
+ @cached_property
380
+ def pyversion_filetype(self):
381
+ return get_pyversion_filetype(self.basename)
382
+
383
+ @cached_property
384
+ def size(self):
385
+ if self.entry.file_exists():
386
+ (value, unit) = sizeof_fmt(self.entry.file_size())
387
+ return f"{value:.0f} {unit}"
388
+ return ''
389
+
390
+ @cached_property
391
+ def title(self):
392
+ return self.basename
393
+
394
+ @cached_property
395
+ def toxresults(self):
396
+ if not self.show_toxresults:
397
+ return []
398
+ return get_toxresults_info(self.linkstore, self.link)
399
+
400
+ @cached_property
401
+ def toxresults_state(self):
402
+ return get_toxresults_state(self.toxresults)
403
+
404
+ @cached_property
405
+ def url(self):
406
+ url = url_for_entrypath(self.request, self.entry.relpath)
407
+ if self.hash_spec:
408
+ url = f"{url}#{self.hash_spec}"
409
+ return url
410
+
411
+
412
+ def get_files_info(request, linkstore, *, show_toxresults=False):
413
+ filedata = sorted(
414
+ linkstore.get_links(rel='releasefile'),
415
+ key=attrgetter('basename'))
416
+ return [
417
+ FileInfo(request, link, linkstore, show_toxresults)
418
+ for link in filedata]
375
419
 
376
420
 
377
421
  def load_toxresult(link):
@@ -559,7 +603,7 @@ def index_get(context, request):
559
603
  request.route_url, "toxresults",
560
604
  user=stage.user.name, index=stage.index,
561
605
  project=name, version=ver),
562
- files=get_files_info(request, linkstore, show_toxresults),
606
+ files=get_files_info(request, linkstore, show_toxresults=show_toxresults),
563
607
  docs=get_docs_info(request, stage, linkstore),
564
608
  _version_data=verdata))
565
609
  packages.sort(key=lambda x: x["info"]["title"])
@@ -728,7 +772,7 @@ def version_get(context, request):
728
772
  infos.append((escape(key), value))
729
773
  show_toxresults = (stage.ixconfig['type'] != 'mirror')
730
774
  linkstore = stage.get_linkstore_perstage(name, version)
731
- files = get_files_info(request, linkstore, show_toxresults)
775
+ files = get_files_info(request, linkstore, show_toxresults=show_toxresults)
732
776
  docs = get_docs_info(request, stage, linkstore)
733
777
  home_page = verdata.get("home_page")
734
778
  nav_links = []
@@ -856,23 +900,28 @@ def statusview(request):
856
900
  in_request=replica.get('in-request', 'unknown'),
857
901
  last_request=format_timestamp(
858
902
  replica.get('last-request', 'unknown'))))
903
+ primary_url = status.get('primary-url', status.get('master-url'))
904
+ primary_uuid = status.get('primary-uuid', status.get('master-uuid'))
905
+ primary_serial = status.get('primary-serial', status.get('master-serial'))
906
+ primary_serial_ts = status.get('primary-serial-timestamp', status.get('master-serial-timestamp'))
907
+ update_from_primary_at = status.get('update-from-primary-at', status.get('update-from-master-at'))
859
908
  return dict(
860
909
  msgs=request.status_info['msgs'],
861
910
  info=dict(
862
911
  uuid=status.get('uuid', 'unknown'),
863
912
  role=status.get('role', 'unknown'),
864
913
  outside_url=status.get('outside-url', 'unknown'),
865
- master_url=status.get('master-url'),
866
- master_uuid=status.get('master-uuid'),
867
- master_serial=status.get('master-serial'),
914
+ master_url=primary_url,
915
+ master_uuid=primary_uuid,
916
+ master_serial=primary_serial,
868
917
  master_serial_timestamp=format_timestamp(
869
- status.get('master-serial-timestamp'), unset_value="never"),
918
+ primary_serial_ts, unset_value="never"),
870
919
  replica_started_at=format_timestamp(
871
920
  status.get('replica-started-at')),
872
921
  replica_in_sync_at=format_timestamp(
873
922
  status.get('replica-in-sync-at'), unset_value="never"),
874
923
  update_from_master_at=format_timestamp(
875
- status.get('update-from-master-at'), unset_value="never"),
924
+ update_from_primary_at, unset_value="never"),
876
925
  serial=status.get('serial', 'unknown'),
877
926
  last_commit_timestamp=format_timestamp(
878
927
  status.get('last-commit-timestamp', 'unknown')),
@@ -2,14 +2,16 @@ from collections import defaultdict
2
2
  from devpi_common.validation import normalize_name
3
3
  from devpi_server.log import threadlog as log
4
4
  from devpi_server.log import thread_push_log
5
- from devpi_server.main import fatal
6
5
  from devpi_server.readonly import get_mutable_deepcopy
7
6
  from devpi_server import mythread
7
+ from devpi_web.compat import fatal
8
+ from devpi_web.compat import read_transaction
8
9
  from devpi_web.indexing import iter_projects
9
10
  from devpi_web.indexing import ProjectIndexingInfo
10
11
  from devpi_web.indexing import is_project_cached
11
12
  from devpi_web.indexing import preprocess_project
12
13
  from devpi_web.main import get_indexer
14
+ from pathlib import Path
13
15
  from pluggy import HookimplMarker
14
16
  from whoosh import fields
15
17
  from whoosh.analysis import Filter, LowercaseFilter, RegexTokenizer
@@ -24,7 +26,6 @@ from whoosh.searching import ResultsPage
24
26
  from whoosh.util.text import rcompile
25
27
  import itertools
26
28
  import os
27
- import py
28
29
  import shutil
29
30
  import sys
30
31
  import time
@@ -400,7 +401,7 @@ class IndexerThread(object):
400
401
  writer = project_ix.writer()
401
402
  searcher = project_ix.searcher()
402
403
  try:
403
- with self.xom.keyfs.transaction(write=False) as tx:
404
+ with read_transaction(self.xom.keyfs) as tx:
404
405
  stage = self.xom.model.getstage(indexname)
405
406
  if stage is not None:
406
407
  for name in names:
@@ -435,6 +436,7 @@ class IndexerThread(object):
435
436
  writer.commit()
436
437
 
437
438
  def tick(self):
439
+ self.thread.exit_if_shutdown()
438
440
  self.shared_data.process_next(self.handler)
439
441
 
440
442
  def thread_run(self):
@@ -470,7 +472,7 @@ class InitialQueueThread(object):
470
472
 
471
473
  def thread_run(self):
472
474
  thread_push_log("[IDXQ]")
473
- with self.xom.keyfs.transaction(write=False) as tx:
475
+ with read_transaction(self.xom.keyfs) as tx:
474
476
  indexer = get_indexer(self.xom)
475
477
  searcher = indexer.get_project_ix().searcher()
476
478
  self.shared_data.queue_projects(
@@ -489,20 +491,23 @@ def setup_thread(xom):
489
491
  return indexer_thread
490
492
 
491
493
 
492
- class Index(object):
494
+ class Index:
493
495
  SearchUnavailableException = SearchUnavailableException
494
496
 
495
497
  def __init__(self, config, settings):
496
498
  if 'path' not in settings:
497
- index_path = config.serverdir.join('.indices')
499
+ if hasattr(config, "server_path"):
500
+ index_path = config.server_path / '.indices'
501
+ else:
502
+ index_path = Path(str(config.serverdir.join('.indices')))
498
503
  else:
499
504
  index_path = settings['path']
500
505
  if not os.path.isabs(index_path):
501
506
  fatal("The path for Whoosh index files must be absolute.")
502
- index_path = py.path.local(index_path)
503
- index_path.ensure_dir()
504
- log.info("Using %s for Whoosh index files." % index_path)
505
- self.index_path = index_path.strpath
507
+ index_path = Path(index_path)
508
+ index_path.mkdir(parents=True, exist_ok=True)
509
+ log.info("Using %s for Whoosh index files.", index_path)
510
+ self.index_path = str(index_path)
506
511
  self.indexer_thread = None
507
512
  self.shared_data = None
508
513
  self.xom = None
@@ -915,10 +920,10 @@ def devpiweb_get_status_info(request):
915
920
  last_activity_seconds = (now - shared_data.last_added)
916
921
  elif shared_data.last_processed:
917
922
  last_activity_seconds = (now - shared_data.last_processed)
918
- if last_activity_seconds > 300:
919
- msgs.append(dict(status="fatal", msg="Nothing indexed for more than 5 minutes"))
920
- elif last_activity_seconds > 60:
921
- msgs.append(dict(status="warn", msg="Nothing indexed for more than 1 minute"))
923
+ if last_activity_seconds > 3600:
924
+ msgs.append(dict(status="fatal", msg="Nothing indexed for more than 60 minutes"))
925
+ elif last_activity_seconds > 300:
926
+ msgs.append(dict(status="warn", msg="Nothing indexed for more than 5 minutes"))
922
927
  if qsize > 10:
923
928
  msgs.append(dict(status="warn", msg="%s items in index queue" % qsize))
924
929
  error_qsize = shared_data.error_queue.qsize()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-web
3
- Version: 4.2.2
3
+ Version: 4.2.3
4
4
  Summary: devpi-web: a web view for devpi-server
5
5
  Home-page: https://devpi.net
6
6
  Maintainer: Florian Schulze
@@ -31,16 +31,6 @@ Classifier: Programming Language :: Python :: 3.12
31
31
  Requires-Python: >=3.4
32
32
  License-File: LICENSE
33
33
  License-File: AUTHORS
34
- Requires-Dist: Whoosh<3
35
- Requires-Dist: beautifulsoup4!=4.12.1,>=4.3.2
36
- Requires-Dist: defusedxml
37
- Requires-Dist: devpi-server>=5.2.0
38
- Requires-Dist: devpi-common>=3.2.0
39
- Requires-Dist: docutils>=0.11
40
- Requires-Dist: pygments>=1.6
41
- Requires-Dist: pyramid!=1.10a1
42
- Requires-Dist: pyramid-chameleon
43
- Requires-Dist: readme-renderer[md]>=23.0
44
34
 
45
35
  ================================================
46
36
  devpi-web: web interface plugin for devpi-server
@@ -87,6 +77,22 @@ Changelog
87
77
 
88
78
  .. towncrier release notes start
89
79
 
80
+ 4.2.3 (2024-09-19)
81
+ ==================
82
+
83
+ Bug Fixes
84
+ ---------
85
+
86
+ - Fix deprecation warnings from devpi-server 6.13.0.
87
+
88
+ - Lazily evaluate file information. Especially with devpi-postgresql this safes many database accesses on most pages.
89
+
90
+ - Guard against missing doczip files, which can happen on replicas during replication.
91
+
92
+ - Increase threshold for index status from 60 s to 300 s for warnings and from 300 s to 3600 s for fatal.
93
+
94
+
95
+
90
96
  4.2.2 (2024-04-20)
91
97
  ==================
92
98
 
@@ -148,24 +154,3 @@ Bug Fixes
148
154
 
149
155
  - Generalize GET redirect from URLs with trailing slash to ones without for consistency.
150
156
 
151
-
152
- 4.1.0 (2022-08-16)
153
- ==================
154
-
155
- Features
156
- --------
157
-
158
- - Support original project names for mirrors with devpi-server 6.6.0.
159
-
160
- - project.pt: Fix #601: add refresh button to project page.
161
-
162
- - project.pt: add navigation links to original project page for mirrors and to "Simple page" of projects.
163
-
164
-
165
- Bug Fixes
166
- ---------
167
-
168
- - Fix #880: explicitly register views for GET method, so there is a 404 on POSTs, like when using twine with wrong upload url.
169
-
170
- - Fix #867: AttributeError during ``devpi-import`` and maybe other commands.
171
-
@@ -10,6 +10,7 @@ setup.py
10
10
  tox.ini
11
11
  devpi_web/__init__.py
12
12
  devpi_web/clear_index.py
13
+ devpi_web/compat.py
13
14
  devpi_web/config.py
14
15
  devpi_web/description.py
15
16
  devpi_web/doczip.py
@@ -30,7 +31,6 @@ devpi_web/static/common.js
30
31
  devpi_web/static/docview.js
31
32
  devpi_web/static/favicon.ico
32
33
  devpi_web/static/jquery-3.6.0.min.js
33
- devpi_web/static/style-old.css
34
34
  devpi_web/static/style.css
35
35
  devpi_web/templates/doc.pt
36
36
  devpi_web/templates/error.pt
@@ -4,6 +4,7 @@ defusedxml
4
4
  devpi-server>=5.2.0
5
5
  devpi-common>=3.2.0
6
6
  docutils>=0.11
7
+ py
7
8
  pygments>=1.6
8
9
  pyramid!=1.10a1
9
10
  pyramid-chameleon