devpi-web 4.2.1__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 (63) hide show
  1. devpi-web-4.2.3/.flake8 +2 -0
  2. {devpi-web-4.2.1 → devpi-web-4.2.3}/CHANGELOG +30 -0
  3. {devpi-web-4.2.1 → devpi-web-4.2.3}/MANIFEST.in +1 -1
  4. {devpi-web-4.2.1 → devpi-web-4.2.3}/PKG-INFO +32 -31
  5. devpi-web-4.2.3/devpi_web/__init__.py +1 -0
  6. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/clear_index.py +2 -1
  7. devpi-web-4.2.3/devpi_web/compat.py +42 -0
  8. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/doczip.py +12 -7
  9. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/main.py +3 -1
  10. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/static/docview.js +5 -0
  11. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/static/style.css +22 -1
  12. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/views.py +97 -48
  13. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/whoosh_index.py +24 -15
  14. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web.egg-info/PKG-INFO +32 -31
  15. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web.egg-info/SOURCES.txt +2 -0
  16. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web.egg-info/requires.txt +1 -0
  17. devpi-web-4.2.3/pyproject.toml +83 -0
  18. {devpi-web-4.2.1 → devpi-web-4.2.3}/setup.py +3 -2
  19. devpi-web-4.2.3/tests/conftest.py +65 -0
  20. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_indexing.py +15 -10
  21. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_main.py +9 -7
  22. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_theme.py +3 -0
  23. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_views.py +10 -2
  24. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_views_docs.py +9 -4
  25. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_views_misc.py +127 -44
  26. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_views_search.py +7 -2
  27. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_views_toxresults.py +7 -3
  28. {devpi-web-4.2.1 → devpi-web-4.2.3}/tests/test_whoosh_index.py +6 -3
  29. devpi-web-4.2.3/tox.ini +59 -0
  30. devpi-web-4.2.1/devpi_web/__init__.py +0 -1
  31. devpi-web-4.2.1/pyproject.toml +0 -26
  32. devpi-web-4.2.1/tests/conftest.py +0 -51
  33. devpi-web-4.2.1/tox.ini +0 -36
  34. {devpi-web-4.2.1 → devpi-web-4.2.3}/AUTHORS +0 -0
  35. {devpi-web-4.2.1 → devpi-web-4.2.3}/LICENSE +0 -0
  36. {devpi-web-4.2.1 → devpi-web-4.2.3}/README.rst +0 -0
  37. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/config.py +0 -0
  38. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/description.py +0 -0
  39. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/hookspecs.py +0 -0
  40. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/indexing.py +0 -0
  41. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/null_index.py +0 -0
  42. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/static/common.js +0 -0
  43. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/static/favicon.ico +0 -0
  44. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/static/jquery-3.6.0.min.js +0 -0
  45. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/doc.pt +0 -0
  46. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/error.pt +0 -0
  47. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/index.pt +0 -0
  48. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/macros.pt +0 -0
  49. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/notfound.pt +0 -0
  50. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/project.pt +0 -0
  51. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/root.pt +0 -0
  52. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/search.pt +0 -0
  53. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/search_help.pt +0 -0
  54. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/status.pt +0 -0
  55. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/toxresult.pt +0 -0
  56. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/toxresults.pt +0 -0
  57. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/user.pt +0 -0
  58. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web/templates/version.pt +0 -0
  59. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web.egg-info/dependency_links.txt +0 -0
  60. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web.egg-info/entry_points.txt +0 -0
  61. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web.egg-info/not-zip-safe +0 -0
  62. {devpi-web-4.2.1 → devpi-web-4.2.3}/devpi_web.egg-info/top_level.txt +0 -0
  63. {devpi-web-4.2.1 → devpi-web-4.2.3}/setup.cfg +0 -0
@@ -0,0 +1,2 @@
1
+ [flake8]
2
+ ignore = E501,E741,W503
@@ -2,6 +2,36 @@
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
+
21
+ 4.2.2 (2024-04-20)
22
+ ==================
23
+
24
+ Bug Fixes
25
+ ---------
26
+
27
+ - style.css: Always let content be full browser height. This also gives more height with some documentation themes when content is short.
28
+
29
+ - style.css: set ``scrollbar-gutter: stable`` on ``body`` to prevent jumping content in documentation iframe.
30
+
31
+ - Fix #970: overwrite fixed html/body heights like ``100%`` in documentation iframe content.
32
+
33
+
34
+
5
35
  4.2.1 (2023-07-02)
6
36
  ==================
7
37
 
@@ -1,4 +1,4 @@
1
- include CHANGELOG *.ini *.rst LICENSE
1
+ include .flake8 CHANGELOG *.ini *.rst LICENSE
2
2
  recursive-include devpi_web/static *
3
3
  recursive-include devpi_web/templates *
4
4
  recursive-include tests *.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devpi-web
3
- Version: 4.2.1
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
@@ -27,6 +27,7 @@ Classifier: Programming Language :: Python :: 3.8
27
27
  Classifier: Programming Language :: Python :: 3.9
28
28
  Classifier: Programming Language :: Python :: 3.10
29
29
  Classifier: Programming Language :: Python :: 3.11
30
+ Classifier: Programming Language :: Python :: 3.12
30
31
  Requires-Python: >=3.4
31
32
  License-File: LICENSE
32
33
  License-File: AUTHORS
@@ -76,80 +77,80 @@ Changelog
76
77
 
77
78
  .. towncrier release notes start
78
79
 
79
- 4.2.1 (2023-07-02)
80
+ 4.2.3 (2024-09-19)
80
81
  ==================
81
82
 
82
83
  Bug Fixes
83
84
  ---------
84
85
 
85
- - Fix #953: Exception when browsers send ETag for documentation pages.
86
+ - Fix deprecation warnings from devpi-server 6.13.0.
86
87
 
87
- - Fix #980: Remove long deprecated backward compatibility for old pluggy versions to fix error with pluggy 1.1.0.
88
+ - Lazily evaluate file information. Especially with devpi-postgresql this safes many database accesses on most pages.
88
89
 
90
+ - Guard against missing doczip files, which can happen on replicas during replication.
89
91
 
90
- 4.2.0 (2022-12-05)
91
- ==================
92
+ - Increase threshold for index status from 60 s to 300 s for warnings and from 300 s to 3600 s for fatal.
92
93
 
93
- Features
94
- --------
95
94
 
96
- - Set ETag header to the doczip hash and max-age to 60 seconds for all documentation files.
97
-
98
- - Add ``--keep-docs-packed`` option.
99
95
 
96
+ 4.2.2 (2024-04-20)
97
+ ==================
100
98
 
101
99
  Bug Fixes
102
100
  ---------
103
101
 
104
- - common.js, macros.pt: Fix #823: Remove moment.js and update jquery to 3.6.0.
102
+ - style.css: Always let content be full browser height. This also gives more height with some documentation themes when content is short.
105
103
 
106
- - common.js: Fix #764: Jumping to anchors with whitespace in documentation now works.
104
+ - style.css: set ``scrollbar-gutter: stable`` on ``body`` to prevent jumping content in documentation iframe.
107
105
 
108
- - doc.pt, docview.js, style.css: Fix #764: Rewrote setting height of documentation iframe which also fixes scrolling to anchors generated by JavaScript.
106
+ - Fix #970: overwrite fixed html/body heights like ``100%`` in documentation iframe content.
109
107
 
110
- - Fix project names from mirrors with devpi-server >= 6.8.0.
111
108
 
112
- - Fix exception in not found page of project URLs.
113
109
 
114
- - toxresults.pt, version.pt: Fix anchor generation for toxresults URLs.
115
-
116
-
117
- 4.1.1 (2022-09-28)
110
+ 4.2.1 (2023-07-02)
118
111
  ==================
119
112
 
120
-
121
113
  Bug Fixes
122
114
  ---------
123
115
 
124
- - Generalize GET redirect from URLs with trailing slash to ones without for consistency.
116
+ - Fix #953: Exception when browsers send ETag for documentation pages.
117
+
118
+ - Fix #980: Remove long deprecated backward compatibility for old pluggy versions to fix error with pluggy 1.1.0.
125
119
 
126
120
 
127
- 4.1.0 (2022-08-16)
121
+ 4.2.0 (2022-12-05)
128
122
  ==================
129
123
 
130
124
  Features
131
125
  --------
132
126
 
133
- - Support original project names for mirrors with devpi-server 6.6.0.
134
-
135
- - project.pt: Fix #601: add refresh button to project page.
127
+ - Set ETag header to the doczip hash and max-age to 60 seconds for all documentation files.
136
128
 
137
- - project.pt: add navigation links to original project page for mirrors and to "Simple page" of projects.
129
+ - Add ``--keep-docs-packed`` option.
138
130
 
139
131
 
140
132
  Bug Fixes
141
133
  ---------
142
134
 
143
- - Fix #880: explicitly register views for GET method, so there is a 404 on POSTs, like when using twine with wrong upload url.
135
+ - common.js, macros.pt: Fix #823: Remove moment.js and update jquery to 3.6.0.
144
136
 
145
- - Fix #867: AttributeError during ``devpi-import`` and maybe other commands.
137
+ - common.js: Fix #764: Jumping to anchors with whitespace in documentation now works.
146
138
 
139
+ - doc.pt, docview.js, style.css: Fix #764: Rewrote setting height of documentation iframe which also fixes scrolling to anchors generated by JavaScript.
147
140
 
148
- 4.0.8 (2021-08-12)
141
+ - Fix project names from mirrors with devpi-server >= 6.8.0.
142
+
143
+ - Fix exception in not found page of project URLs.
144
+
145
+ - toxresults.pt, version.pt: Fix anchor generation for toxresults URLs.
146
+
147
+
148
+ 4.1.1 (2022-09-28)
149
149
  ==================
150
150
 
151
+
151
152
  Bug Fixes
152
153
  ---------
153
154
 
154
- - On startup loop over mirror indexes last to avoid filling up the indexing queue, which processes non mirror indexes first.
155
+ - Generalize GET redirect from URLs with trailing slash to ones without for consistency.
155
156
 
@@ -0,0 +1 @@
1
+ __version__ = '4.2.3'
@@ -31,7 +31,8 @@ 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("You should stop devpi-server before running this command.")
34
+ log.warn( # noqa: G010 - this is actually TagLogger from devpi-server, but devpi-web still needs to support older devpi-server versions
35
+ "You should stop devpi-server before running this command.")
35
36
  ix = get_indexer(xom)
36
37
  ix.delete_index()
37
38
  log.info("Index deleted, start devpi-server again to let the index rebuild automatically.")
@@ -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
- fcntl = None # type: ignore
12
- import json
13
- import py
16
+ fcntl = None # type: ignore[assignment]
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
@@ -12,6 +12,7 @@ function onIFrameLoad(event) {
12
12
  $iframe = $(iframe),
13
13
  $doc = $(iframe.contentWindow.document),
14
14
  $docHtml = $doc.find('html'),
15
+ $docBody = $docHtml.find('body'),
15
16
  scrolled_to_anchor = false,
16
17
  iframeResizeObserver = new ResizeObserver((entries) => {
17
18
  for (const entry of entries) {
@@ -28,6 +29,10 @@ function onIFrameLoad(event) {
28
29
  }
29
30
  });
30
31
 
32
+ $docHtml.css('height', 'auto');
33
+ $docHtml.css('overflow-y', 'hidden');
34
+ $docBody.css('height', 'auto');
35
+ $docBody.css('overflow-y', 'hidden');
31
36
  // make keyboard actions affect the actual documentation
32
37
  // in the iframe by default
33
38
  iframe.contentWindow.focus();
@@ -7,6 +7,17 @@ body, html {
7
7
  padding: 0;
8
8
  }
9
9
 
10
+ html {
11
+ height: 100%;
12
+ }
13
+
14
+ body {
15
+ min-height: 100%;
16
+ display: flex;
17
+ flex-direction: column;
18
+ scrollbar-gutter: stable;
19
+ }
20
+
10
21
  pre, table {
11
22
  font-size: inherit;
12
23
  }
@@ -23,6 +34,10 @@ a:hover {
23
34
  text-decoration: underline;
24
35
  }
25
36
 
37
+ #content {
38
+ flex: auto;
39
+ }
40
+
26
41
  form, #content, #navigation, .searchresults {
27
42
  position: relative;
28
43
  margin: 0 1em;
@@ -545,10 +560,16 @@ table.projectinfos .closed .value:after,
545
560
  }
546
561
 
547
562
  div.iframe {
563
+ flex: auto;
548
564
  margin: 0;
565
+ display: flex;
566
+ }
567
+
568
+ div.iframe iframe {
569
+ flex: auto;
549
570
  }
550
571
 
551
- div.iframe, iframe {
572
+ div.iframe, div.iframe iframe {
552
573
  width: 100%;
553
574
  border: 0;
554
575
  }
@@ -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))
@@ -215,7 +218,7 @@ def doc_show(context, request):
215
218
  project=name, version='latest', relpath="index.html")))
216
219
  try:
217
220
  stable_doc_info = get_doc_info(context, request, version='stable', check_content=False)
218
- if stable_doc_info['doc_version'] != doc_info['doc_version'] and stable_doc_info['doc_version'] != latest_doc_info['doc_version']:
221
+ if stable_doc_info['doc_version'] not in (doc_info['doc_version'], latest_doc_info['doc_version']):
219
222
  version_links.append(dict(
220
223
  title="Stable documentation",
221
224
  url=request.route_url(
@@ -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')),