emcd-projects 1.23a0__tar.gz → 1.24__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 (28) hide show
  1. {emcd_projects-1.23a0 → emcd_projects-1.24}/.gitignore +3 -0
  2. {emcd_projects-1.23a0 → emcd_projects-1.24}/PKG-INFO +1 -1
  3. emcd_projects-1.24/data/templates/website.html.jinja +103 -0
  4. {emcd_projects-1.23a0 → emcd_projects-1.24}/pyproject.toml +0 -2
  5. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__/imports.py +2 -3
  6. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__init__.py +1 -1
  7. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/cli.py +0 -2
  8. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/website.py +251 -28
  9. emcd_projects-1.23a0/data/templates/website.html.jinja +0 -58
  10. {emcd_projects-1.23a0 → emcd_projects-1.24}/LICENSE.txt +0 -0
  11. {emcd_projects-1.23a0 → emcd_projects-1.24}/data/.gitignore +0 -0
  12. {emcd_projects-1.23a0 → emcd_projects-1.24}/data/copier/answers-default.yaml +0 -0
  13. {emcd_projects-1.23a0 → emcd_projects-1.24}/data/copier/answers-maximum.yaml +0 -0
  14. {emcd_projects-1.23a0 → emcd_projects-1.24}/data/templates/coverage.svg.jinja +0 -0
  15. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/README.rst +0 -0
  16. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__/__init__.py +0 -0
  17. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__/application.py +0 -0
  18. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__/distribution.py +0 -0
  19. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__/nomina.py +0 -0
  20. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__/preparation.py +0 -0
  21. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__/state.py +0 -0
  22. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/__main__.py +0 -0
  23. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/_typedecls/__builtins__.pyi +0 -0
  24. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/exceptions.py +0 -0
  25. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/filesystem.py +0 -0
  26. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/interfaces.py +0 -0
  27. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/py.typed +0 -0
  28. {emcd_projects-1.23a0 → emcd_projects-1.24}/sources/emcdproj/template.py +0 -0
@@ -1,9 +1,12 @@
1
1
  .env
2
+ .gemini
3
+ .mcp.json
2
4
  *.so
3
5
  .*.swp
4
6
  AGENTS.md
5
7
  CLAUDE.md
6
8
  CONVENTIONS.md
9
+ GEMINI.md
7
10
  __pycache__/
8
11
  bugs/
9
12
  dist/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emcd-projects
3
- Version: 1.23a0
3
+ Version: 1.24
4
4
  Summary: Project management utilities.
5
5
  Project-URL: Homepage, https://github.com/emcd/python-project-common
6
6
  Project-URL: Documentation, https://emcd.github.io/python-project-common
@@ -0,0 +1,103 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Available Releases</title>
7
+ <style>
8
+ table {
9
+ width: 100%;
10
+ border-collapse: collapse;
11
+ }
12
+ th, td {
13
+ padding: 8px;
14
+ text-align: left;
15
+ border-bottom: 1px solid #ddd;
16
+ }
17
+ th {
18
+ background-color: #f2f2f2;
19
+ }
20
+ tr:hover {
21
+ background-color: #f5f5f5;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <h1>Available Releases</h1>
27
+
28
+ {% if stable_dev_versions %}
29
+ <h2>Current Releases</h2>
30
+ <table>
31
+ <thead>
32
+ <tr>
33
+ <th>Version</th>
34
+ <th>Documentation</th>
35
+ <th>Coverage</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ {% for version_label, attributes in stable_dev_versions.items() %}
40
+ <tr>
41
+ <td>{{ version_label }}</td>
42
+ <td>
43
+ {% if 'sphinx-html' in attributes %}
44
+ {% if version_label.startswith('stable') %}
45
+ <a href="stable/sphinx-html/index.html">Docs</a>
46
+ {% elif version_label.startswith('development') %}
47
+ <a href="development/sphinx-html/index.html">Docs</a>
48
+ {% endif %}
49
+ {% else %}
50
+ N/A
51
+ {% endif %}
52
+ </td>
53
+ <td>
54
+ {% if 'coverage-pytest' in attributes %}
55
+ {% if version_label.startswith('stable') %}
56
+ <a href="stable/coverage-pytest/index.html">Coverage</a>
57
+ {% elif version_label.startswith('development') %}
58
+ <a href="development/coverage-pytest/index.html">Coverage</a>
59
+ {% endif %}
60
+ {% else %}
61
+ N/A
62
+ {% endif %}
63
+ </td>
64
+ </tr>
65
+ {% endfor %}
66
+ </tbody>
67
+ </table>
68
+
69
+ <h2>All Releases</h2>
70
+ {% endif %}
71
+
72
+ <table>
73
+ <thead>
74
+ <tr>
75
+ <th>Version</th>
76
+ <th>Documentation</th>
77
+ <th>Coverage</th>
78
+ </tr>
79
+ </thead>
80
+ <tbody>
81
+ {% for version, attributes in versions.items() %}
82
+ <tr>
83
+ <td>{{ version }}</td>
84
+ <td>
85
+ {% if 'sphinx-html' in attributes %}
86
+ <a href="{{ version }}/sphinx-html/index.html">Docs</a>
87
+ {% else %}
88
+ N/A
89
+ {% endif %}
90
+ </td>
91
+ <td>
92
+ {% if 'coverage-pytest' in attributes %}
93
+ <a href="{{ version }}/coverage-pytest/index.html">Coverage</a>
94
+ {% else %}
95
+ N/A
96
+ {% endif %}
97
+ </td>
98
+ </tr>
99
+ {% endfor %}
100
+ </tbody>
101
+ </table>
102
+ </body>
103
+ </html>
@@ -111,9 +111,7 @@ description = ''' Development environment. '''
111
111
  dependencies = [
112
112
  'Jinja2',
113
113
  'coverage[toml]',
114
- 'emcd-projects',
115
114
  'furo',
116
- 'icecream-truck',
117
115
  'packaging',
118
116
  'pre-commit',
119
117
  'pyfakefs',
@@ -18,13 +18,11 @@
18
18
  #============================================================================#
19
19
 
20
20
 
21
- ''' Common imports and type aliases used throughout the package. '''
21
+ ''' Common imports used throughout the package. '''
22
22
 
23
23
  # ruff: noqa: F401
24
24
 
25
25
 
26
- from __future__ import annotations
27
-
28
26
  import abc
29
27
  import collections.abc as cabc
30
28
  import contextlib as ctxl
@@ -33,6 +31,7 @@ import io
33
31
  import json
34
32
  import math
35
33
  import os
34
+ import subprocess
36
35
  import shutil
37
36
  import sys
38
37
  import tempfile
@@ -27,7 +27,7 @@ from . import exceptions
27
27
  # --- END: Injected by Copier ---
28
28
 
29
29
 
30
- __version__ = '1.23a0'
30
+ __version__ = '1.24'
31
31
 
32
32
 
33
33
  def main( ):
@@ -21,8 +21,6 @@
21
21
  ''' Command-line interface. '''
22
22
 
23
23
 
24
- from __future__ import annotations
25
-
26
24
  from . import __
27
25
  from . import interfaces as _interfaces
28
26
  from . import template as _template
@@ -19,8 +19,6 @@
19
19
 
20
20
 
21
21
  ''' Static website maintenance utilities for projects. '''
22
- # TODO: Support separate section for current documentation: stable, latest.
23
- # TODO? Separate coverage SVG files for each release.
24
22
 
25
23
 
26
24
  from __future__ import annotations
@@ -60,11 +58,15 @@ class SurveyCommand(
60
58
  ):
61
59
  ''' Surveys release versions published in static website. '''
62
60
 
61
+ use_extant: __.typx.Annotated[
62
+ bool,
63
+ __.typx.Doc( ''' Fetch publication branch and use tarball. ''' ),
64
+ ] = False
65
+
63
66
  async def __call__(
64
67
  self, auxdata: __.Globals, display: _interfaces.ConsoleDisplay
65
68
  ) -> None:
66
- # TODO: Implement.
67
- pass
69
+ survey( auxdata, use_extant = self.use_extant )
68
70
 
69
71
 
70
72
  class UpdateCommand(
@@ -78,10 +80,24 @@ class UpdateCommand(
78
80
  __.tyro.conf.Positional,
79
81
  ]
80
82
 
83
+ use_extant: __.typx.Annotated[
84
+ bool,
85
+ __.typx.Doc( ''' Fetch publication branch and use tarball. ''' ),
86
+ ] = False
87
+
88
+ production: __.typx.Annotated[
89
+ bool,
90
+ __.typx.Doc( ''' Update publication branch with new tarball.
91
+ Implies --use-extant to prevent data loss. ''' ),
92
+ ] = False
93
+
81
94
  async def __call__(
82
95
  self, auxdata: __.Globals, display: _interfaces.ConsoleDisplay
83
96
  ) -> None:
84
- update( auxdata, self.version )
97
+ update(
98
+ auxdata, self.version,
99
+ use_extant = self.use_extant,
100
+ production = self.production )
85
101
 
86
102
 
87
103
  class Locations( metaclass = __.ImmutableDataclass ):
@@ -129,11 +145,57 @@ class Locations( metaclass = __.ImmutableDataclass ):
129
145
  templates = templates )
130
146
 
131
147
 
148
+ def survey(
149
+ auxdata: __.Globals, *,
150
+ project_anchor: __.Absential[ __.Path ] = __.absent,
151
+ use_extant: bool = False
152
+ ) -> None:
153
+ ''' Surveys release versions published in static website.
154
+
155
+ Lists all versions from the versions manifest, showing their
156
+ available documentation types and highlighting the latest version.
157
+ '''
158
+ locations = Locations.from_project_anchor( auxdata, project_anchor )
159
+
160
+ # Handle --use-extant flag: fetch publication branch and checkout tarball
161
+ if use_extant:
162
+ _fetch_publication_branch_and_tarball( locations )
163
+ # Extract the fetched tarball to view published versions
164
+ if locations.archive.is_file( ):
165
+ from tarfile import open as tarfile_open
166
+ if locations.website.is_dir( ):
167
+ __.shutil.rmtree( locations.website )
168
+ locations.website.mkdir( exist_ok = True, parents = True )
169
+ with tarfile_open( locations.archive, 'r:xz' ) as archive:
170
+ archive.extractall( path = locations.website ) # noqa: S202
171
+
172
+ if not locations.versions.is_file( ):
173
+ context = "published" if use_extant else "local"
174
+ print( f"No versions manifest found for {context} website. "
175
+ f"Run 'website update' first." )
176
+ return
177
+ with locations.versions.open( 'r' ) as file:
178
+ data = __.json.load( file )
179
+ versions = data.get( 'versions', { } )
180
+ latest = data.get( 'latest_version' )
181
+ if not versions:
182
+ context = "published" if use_extant else "local"
183
+ print( f"No versions found in {context} manifest." )
184
+ return
185
+ context = "Published" if use_extant else "Local"
186
+ print( f"{context} versions:" )
187
+ for version, species in versions.items( ):
188
+ marker = " (latest)" if version == latest else ""
189
+ species_list = ', '.join( species ) if species else "none"
190
+ print( f" {version}{marker}: {species_list}" )
191
+
132
192
 
133
193
  def update(
134
194
  auxdata: __.Globals,
135
195
  version: str, *,
136
- project_anchor: __.Absential[ __.Path ] = __.absent
196
+ project_anchor: __.Absential[ __.Path ] = __.absent,
197
+ use_extant: bool = False,
198
+ production: bool = False
137
199
  ) -> None:
138
200
  ''' Updates project website with latest documentation and coverage.
139
201
 
@@ -145,6 +207,9 @@ def update(
145
207
  from tarfile import open as tarfile_open
146
208
  locations = Locations.from_project_anchor( auxdata, project_anchor )
147
209
  locations.publications.mkdir( exist_ok = True, parents = True )
210
+ # --production implies --use-extant to prevent clobbering existing versions
211
+ if use_extant or production:
212
+ _fetch_publication_branch_and_tarball( locations )
148
213
  if locations.website.is_dir( ): __.shutil.rmtree( locations.website )
149
214
  locations.website.mkdir( exist_ok = True, parents = True )
150
215
  if locations.archive.is_file( ):
@@ -155,14 +220,86 @@ def update(
155
220
  loader = _jinja2.FileSystemLoader( locations.templates ),
156
221
  autoescape = True )
157
222
  index_data = _update_versions_json( locations, version, available_species )
223
+ _enhance_index_data_with_stable_dev( index_data )
224
+ _create_stable_dev_directories( locations, index_data )
158
225
  _update_index_html( locations, j2context, index_data )
159
226
  if ( locations.artifacts / 'coverage-pytest' ).is_dir( ):
160
227
  _update_coverage_badge( locations, j2context )
228
+ _update_version_coverage_badge( locations, j2context, version )
161
229
  ( locations.website / '.nojekyll' ).touch( )
162
230
  from .filesystem import chdir
163
231
  with chdir( locations.website ): # noqa: SIM117
164
232
  with tarfile_open( locations.archive, 'w:xz' ) as archive:
165
233
  archive.add( '.' )
234
+ if production: _update_publication_branch( locations, version )
235
+
236
+
237
+ def _create_stable_dev_directories(
238
+ locations: Locations, data: dict[ __.typx.Any, __.typx.Any ]
239
+ ) -> None:
240
+ ''' Creates stable/ and development/ directories with current releases.
241
+
242
+ Copies the content from the identified stable and development versions
243
+ to stable/ and development/ directories to provide persistent URLs
244
+ that don't change when new versions are released.
245
+ '''
246
+ stable_version = data.get( 'stable_version' )
247
+ development_version = data.get( 'development_version' )
248
+ if stable_version:
249
+ stable_source = locations.website / stable_version
250
+ stable_dest = locations.website / 'stable'
251
+ if stable_dest.is_dir( ):
252
+ __.shutil.rmtree( stable_dest )
253
+ if stable_source.is_dir( ):
254
+ __.shutil.copytree( stable_source, stable_dest )
255
+ if development_version:
256
+ dev_source = locations.website / development_version
257
+ dev_dest = locations.website / 'development'
258
+ if dev_dest.is_dir( ):
259
+ __.shutil.rmtree( dev_dest )
260
+ if dev_source.is_dir( ):
261
+ __.shutil.copytree( dev_source, dev_dest )
262
+
263
+
264
+ def _enhance_index_data_with_stable_dev(
265
+ data: dict[ __.typx.Any, __.typx.Any ]
266
+ ) -> None:
267
+ ''' Enhances index data with stable/development version information.
268
+
269
+ Identifies the latest stable release and latest development version
270
+ from the versions data and adds them as separate entries for the
271
+ stable/development table.
272
+ '''
273
+ from packaging.version import Version
274
+ versions = data.get( 'versions', { } )
275
+ if not versions:
276
+ data[ 'stable_dev_versions' ] = { }
277
+ return
278
+ stable_version = None
279
+ development_version = None
280
+ # Sort versions by packaging.version.Version for proper comparison
281
+ sorted_versions = sorted(
282
+ versions.items( ),
283
+ key = lambda entry: Version( entry[ 0 ] ),
284
+ reverse = True )
285
+ # Find latest stable (non-prerelease) and development (prerelease) versions
286
+ for version_string, species in sorted_versions:
287
+ version_obj = Version( version_string )
288
+ if not version_obj.is_prerelease and stable_version is None:
289
+ stable_version = ( version_string, species )
290
+ if version_obj.is_prerelease and development_version is None:
291
+ development_version = ( version_string, species )
292
+ if stable_version and development_version:
293
+ break
294
+ stable_dev_versions: dict[ str, tuple[ str, ... ] ] = { }
295
+ if stable_version:
296
+ stable_dev_versions[ 'stable (current)' ] = stable_version[ 1 ]
297
+ data[ 'stable_version' ] = stable_version[ 0 ]
298
+ if development_version:
299
+ stable_dev_versions[ 'development (current)' ] = (
300
+ development_version[ 1 ] )
301
+ data[ 'development_version' ] = development_version[ 0 ]
302
+ data[ 'stable_dev_versions' ] = stable_dev_versions
166
303
 
167
304
 
168
305
  def _extract_coverage( locations: Locations ) -> int:
@@ -184,6 +321,58 @@ def _extract_coverage( locations: Locations ) -> int:
184
321
  return __.math.floor( float( line_rate ) * 100 )
185
322
 
186
323
 
324
+ def _fetch_publication_branch_and_tarball( locations: Locations ) -> None:
325
+ ''' Fetches publication branch and checks out existing tarball.
326
+
327
+ Attempts to fetch the publication branch from origin and checkout
328
+ the website tarball. Ignores failures if branch or tarball don't exist.
329
+ '''
330
+ with __.ctxl.suppress( Exception ):
331
+ __.subprocess.run(
332
+ [ 'git', 'fetch', 'origin', 'publication:publication' ],
333
+ cwd = locations.project,
334
+ check = False,
335
+ capture_output = True )
336
+ with __.ctxl.suppress( Exception ):
337
+ __.subprocess.run(
338
+ [ 'git', 'checkout', 'publication', '--',
339
+ str( locations.archive ) ],
340
+ cwd = locations.project,
341
+ check = False,
342
+ capture_output = True )
343
+
344
+
345
+ def _generate_coverage_badge_svg(
346
+ locations: Locations, j2context: _jinja2.Environment
347
+ ) -> str:
348
+ ''' Generates coverage badge SVG content.
349
+
350
+ Returns the rendered SVG content for a coverage badge based on the
351
+ current coverage percentage. Colors indicate coverage quality:
352
+ - red: < 50%
353
+ - yellow: 50-79%
354
+ - green: >= 80%
355
+ '''
356
+ coverage = _extract_coverage( locations )
357
+ color = (
358
+ 'red' if coverage < 50 else ( # noqa: PLR2004
359
+ 'yellow' if coverage < 80 else 'green' ) ) # noqa: PLR2004
360
+ label_text = 'coverage'
361
+ value_text = f"{coverage}%"
362
+ label_width = len( label_text ) * 6 + 10
363
+ value_width = len( value_text ) * 6 + 15
364
+ total_width = label_width + value_width
365
+ template = j2context.get_template( 'coverage.svg.jinja' )
366
+ # TODO: Add error handling for template rendering failures.
367
+ return template.render(
368
+ color = color,
369
+ total_width = total_width,
370
+ label_text = label_text,
371
+ value_text = value_text,
372
+ label_width = label_width,
373
+ value_width = value_width )
374
+
375
+
187
376
  def _update_available_species(
188
377
  locations: Locations, version: str
189
378
  ) -> tuple[ str, ... ]:
@@ -204,30 +393,49 @@ def _update_coverage_badge(
204
393
  ''' Updates coverage badge SVG.
205
394
 
206
395
  Generates a color-coded coverage badge based on the current coverage
207
- percentage. Colors indicate coverage quality:
208
- - red: < 50%
209
- - yellow: 50-79%
210
- - green: >= 80%
396
+ percentage and writes it to the main coverage.svg location.
211
397
  '''
212
- coverage = _extract_coverage( locations )
213
- color = (
214
- 'red' if coverage < 50 else ( # noqa: PLR2004
215
- 'yellow' if coverage < 80 else 'green' ) ) # noqa: PLR2004
216
- label_text = 'coverage'
217
- value_text = f"{coverage}%"
218
- label_width = len( label_text ) * 6 + 10
219
- value_width = len( value_text ) * 6 + 15
220
- total_width = label_width + value_width
221
- template = j2context.get_template( 'coverage.svg.jinja' )
222
- # TODO: Add error handling for template rendering failures.
398
+ svg_content = _generate_coverage_badge_svg( locations, j2context )
223
399
  with locations.coverage.open( 'w' ) as file:
224
- file.write( template.render(
225
- color = color,
226
- total_width = total_width,
227
- label_text = label_text,
228
- value_text = value_text,
229
- label_width = label_width,
230
- value_width = value_width ) )
400
+ file.write( svg_content )
401
+
402
+
403
+ def _update_publication_branch( locations: Locations, version: str ) -> None:
404
+ ''' Updates publication branch with new tarball.
405
+
406
+ Adds the tarball to git, commits to the publication branch, and pushes
407
+ to origin. Uses the same approach as the GitHub workflow.
408
+ '''
409
+ __.subprocess.run(
410
+ [ 'git', 'add', str( locations.archive ) ],
411
+ cwd = locations.project,
412
+ check = True )
413
+ # Commit to publication branch without checkout
414
+ # Get current tree hash
415
+ tree_result = __.subprocess.run(
416
+ [ 'git', 'write-tree' ],
417
+ cwd = locations.project,
418
+ check = True,
419
+ capture_output = True,
420
+ text = True )
421
+ tree_hash = tree_result.stdout.strip( )
422
+ # Create commit with publication branch as parent
423
+ commit_result = __.subprocess.run(
424
+ [ 'git', 'commit-tree', tree_hash, '-p', 'publication',
425
+ '-m', f"Update documents for publication. ({version})" ],
426
+ cwd = locations.project,
427
+ check = True,
428
+ capture_output = True,
429
+ text = True )
430
+ commit_hash = commit_result.stdout.strip( )
431
+ __.subprocess.run(
432
+ [ 'git', 'branch', '--force', 'publication', commit_hash ],
433
+ cwd = locations.project,
434
+ check = True )
435
+ __.subprocess.run(
436
+ [ 'git', 'push', 'origin', 'publication:publication' ],
437
+ cwd = locations.project,
438
+ check = True )
231
439
 
232
440
 
233
441
  def _update_index_html(
@@ -246,6 +454,21 @@ def _update_index_html(
246
454
  file.write( template.render( **data ) )
247
455
 
248
456
 
457
+ def _update_version_coverage_badge(
458
+ locations: Locations, j2context: _jinja2.Environment, version: str
459
+ ) -> None:
460
+ ''' Updates version-specific coverage badge SVG.
461
+
462
+ Generates a coverage badge for the specific version and places it
463
+ in the version's subtree. This allows each version to have its own
464
+ coverage badge accessible at version/coverage.svg.
465
+ '''
466
+ svg_content = _generate_coverage_badge_svg( locations, j2context )
467
+ version_coverage_path = locations.website / version / 'coverage.svg'
468
+ with version_coverage_path.open( 'w' ) as file:
469
+ file.write( svg_content )
470
+
471
+
249
472
  def _update_versions_json(
250
473
  locations: Locations,
251
474
  version: str,
@@ -1,58 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Available Releases</title>
7
- <style>
8
- table {
9
- width: 100%;
10
- border-collapse: collapse;
11
- }
12
- th, td {
13
- padding: 8px;
14
- text-align: left;
15
- border-bottom: 1px solid #ddd;
16
- }
17
- th {
18
- background-color: #f2f2f2;
19
- }
20
- tr:hover {
21
- background-color: #f5f5f5;
22
- }
23
- </style>
24
- </head>
25
- <body>
26
- <h1>Available Releases</h1>
27
- <table>
28
- <thead>
29
- <tr>
30
- <th>Version</th>
31
- <th>Documentation</th>
32
- <th>Coverage</th>
33
- </tr>
34
- </thead>
35
- <tbody>
36
- {% for version, attributes in versions.items() %}
37
- <tr>
38
- <td>{{ version }}</td>
39
- <td>
40
- {% if 'sphinx-html' in attributes %}
41
- <a href="{{ version }}/sphinx-html/index.html">Docs</a>
42
- {% else %}
43
- N/A
44
- {% endif %}
45
- </td>
46
- <td>
47
- {% if 'coverage-pytest' in attributes %}
48
- <a href="{{ version }}/coverage-pytest/index.html">Coverage</a>
49
- {% else %}
50
- N/A
51
- {% endif %}
52
- </td>
53
- </tr>
54
- {% endfor %}
55
- </tbody>
56
- </table>
57
- </body>
58
- </html>
File without changes