docput 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. docput-0.1.0/.forgejo/workflows/push.yml +115 -0
  2. docput-0.1.0/.gitignore +8 -0
  3. docput-0.1.0/CHANGELOG.md +7 -0
  4. docput-0.1.0/Justfile +30 -0
  5. docput-0.1.0/LICENSE.md +27 -0
  6. docput-0.1.0/PKG-INFO +45 -0
  7. docput-0.1.0/README.md +19 -0
  8. docput-0.1.0/docput/__init__.py +6 -0
  9. docput-0.1.0/docput/__main__.py +3 -0
  10. docput-0.1.0/docput/cli/__init__.py +5 -0
  11. docput-0.1.0/docput/cli/main.py +123 -0
  12. docput-0.1.0/docput/cli/manifest.py +58 -0
  13. docput-0.1.0/docput/cli/version.py +183 -0
  14. docput-0.1.0/docput/config.py +129 -0
  15. docput-0.1.0/docput/manifest.py +110 -0
  16. docput-0.1.0/docput/path.py +88 -0
  17. docput-0.1.0/docput/remote/__init__.py +43 -0
  18. docput-0.1.0/docput/remote/base.py +134 -0
  19. docput-0.1.0/docput/remote/filesystem.py +53 -0
  20. docput-0.1.0/docput/remote/git_pages.py +221 -0
  21. docput-0.1.0/docput/sphinx_ext/__init__.py +22 -0
  22. docput-0.1.0/docput/sphinx_ext/_static/docput.js +67 -0
  23. docput-0.1.0/docput/sphinx_ext/_templates/sidebar/version-switch.html +10 -0
  24. docput-0.1.0/docput/typing.py +13 -0
  25. docput-0.1.0/docput.egg-info/PKG-INFO +45 -0
  26. docput-0.1.0/docput.egg-info/SOURCES.txt +52 -0
  27. docput-0.1.0/docput.egg-info/dependency_links.txt +1 -0
  28. docput-0.1.0/docput.egg-info/entry_points.txt +2 -0
  29. docput-0.1.0/docput.egg-info/requires.txt +5 -0
  30. docput-0.1.0/docput.egg-info/scm_file_list.json +48 -0
  31. docput-0.1.0/docput.egg-info/scm_version.json +8 -0
  32. docput-0.1.0/docput.egg-info/top_level.txt +1 -0
  33. docput-0.1.0/docs/.gitignore +1 -0
  34. docput-0.1.0/docs/Justfile +25 -0
  35. docput-0.1.0/docs/_include/forgejo-hover.svg +41 -0
  36. docput-0.1.0/docs/_include/forgejo-monochrome.svg +35 -0
  37. docput-0.1.0/docs/_include/pypi-hover.svg +56 -0
  38. docput-0.1.0/docs/_include/pypi-monochrome.svg +13 -0
  39. docput-0.1.0/docs/_root/_headers +4 -0
  40. docput-0.1.0/docs/_root/_redirects +1 -0
  41. docput-0.1.0/docs/_static/css/custom.css +27 -0
  42. docput-0.1.0/docs/_templates/page.html +6 -0
  43. docput-0.1.0/docs/conf.py +85 -0
  44. docput-0.1.0/docs/getting_started.md +72 -0
  45. docput-0.1.0/docs/handbook.md +112 -0
  46. docput-0.1.0/docs/index.md +17 -0
  47. docput-0.1.0/docs/reference/commands/index.md +17 -0
  48. docput-0.1.0/docs/reference/commands/manifest.md +5 -0
  49. docput-0.1.0/docs/reference/commands/version.md +5 -0
  50. docput-0.1.0/docs/reference/index.md +9 -0
  51. docput-0.1.0/docs/versions.json +12 -0
  52. docput-0.1.0/pyproject.toml +76 -0
  53. docput-0.1.0/setup.cfg +4 -0
  54. docput-0.1.0/tests/test_remotes.py +95 -0
@@ -0,0 +1,115 @@
1
+ name: build
2
+ on: [ push, pull_request ]
3
+
4
+ jobs:
5
+ build:
6
+ name: Test & Build
7
+ runs-on: ubuntu-24.04
8
+ services:
9
+ gitpages:
10
+ image: codeberg.org/git-pages/git-pages:0.9.1
11
+ env:
12
+ PAGES_INSECURE: 1
13
+ env:
14
+ DOCPUT_TEST_GIT_PAGES_URL: http://gitpages:3000
15
+
16
+ steps:
17
+ - name: Checkout
18
+ uses: https://code.forgejo.org/actions/checkout@v5
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - name: Set up Project
23
+ uses: https://git.offworld.city/actions/setup-python-project@87a134c2c5e6c03139c4215575f0f1ebdfcb41a2
24
+ with:
25
+ python-version-file: pyproject.toml
26
+ pip-install: -e .[sphinx] --group dev
27
+
28
+ - name: Lint
29
+ run: just lint
30
+
31
+ - name: Test
32
+ run: just test
33
+
34
+ - name: Build
35
+ run: just build
36
+
37
+ - name: Upload dist
38
+ uses: https://code.forgejo.org/forgejo/upload-artifact@v4
39
+ with:
40
+ name: python-dist
41
+ path: dist/
42
+ compression-level: 0 # already compressed
43
+
44
+
45
+ document:
46
+ name: Document
47
+ runs-on: ubuntu-24.04
48
+ container:
49
+ env:
50
+ DOCPUT_REMOTE: git-pages+https://drewcassidy.me/docput
51
+ GIT_PAGES_PASSWORD: ${{ secrets.DREWCASSIDY_ME_PASSWORD }}
52
+ steps:
53
+ - name: Checkout
54
+ uses: https://code.forgejo.org/actions/checkout@v5
55
+
56
+ - name: Set up Project
57
+ uses: https://git.offworld.city/actions/setup-python-project@87a134c2c5e6c03139c4215575f0f1ebdfcb41a2
58
+ with:
59
+ python-version-file: pyproject.toml
60
+ pip-install: -e .[sphinx] --group docs
61
+
62
+ - name: Build docs
63
+ run: just docs build
64
+
65
+ - name: Push docs
66
+ if: forge.event_name == 'push' #skip for PRs
67
+ run: just docs push ${{ startsWith(forge.ref, 'refs/tags') && '--latest --visible' || '' }}
68
+
69
+ - name: Upload root files
70
+ if: forge.ref == 'refs/heads/main' # follow main branch
71
+ run: just docs update-root
72
+
73
+
74
+ publish:
75
+ name: Publish
76
+ if: forge.event_name == 'push' && startsWith(forge.ref, 'refs/tags')
77
+ needs:
78
+ - build
79
+ - document
80
+ runs-on: ubuntu-24.04
81
+ steps:
82
+ - name: Checkout
83
+ uses: https://code.forgejo.org/actions/checkout@v5
84
+
85
+ - name: Download dist
86
+ uses: https://code.forgejo.org/forgejo/download-artifact@v4
87
+ with:
88
+ name: python-dist
89
+ path: dist
90
+
91
+ - name: Get Changelog Information
92
+ id: yaclog-show
93
+ uses: https://git.offworld.city/drewcassidy/yaclog@1.7.2
94
+
95
+ - name: Create release
96
+ uses: https://code.forgejo.org/actions/forgejo-release@v2.11.1
97
+ with:
98
+ direction: upload
99
+ token: ${{ secrets.FORGEJO_TOKEN }}
100
+ release-dir: dist
101
+ title: docput ${{ steps.yaclog-show.outputs.name }}
102
+ release-notes-file: ${{ steps.yaclog-show.outputs.body-file }}
103
+ override: true
104
+
105
+ - name: Upload to PyPI
106
+ run: pipx run twine upload --skip-existing dist/*
107
+ env:
108
+ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
109
+
110
+ - name: Upload to Forgejo
111
+ run: pipx run twine upload dist/*
112
+ env:
113
+ TWINE_USERNAME: ${{ forge.repository_owner }}
114
+ TWINE_PASSWORD: ${{ secrets.FORGEJO_DEPLOY_TOKEN }}
115
+ TWINE_REPOSITORY_URL: ${{ forge.server_url }}/api/packages/${{ forge.repository_owner }}/pypi
@@ -0,0 +1,8 @@
1
+ # ---> Python
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+
5
+ # Distribution / packaging
6
+ build/
7
+ dist/
8
+ *.egg-info/
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file
4
+
5
+ ## 0.1.0 - 2026-06-25
6
+
7
+ initial version
docput-0.1.0/Justfile ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env just --justfile
2
+
3
+ mod docs
4
+
5
+ # Lint python source code
6
+ lint:
7
+ python -m flake8 docput/
8
+ zuban check docput/
9
+
10
+ # Format python source code and other support files
11
+ format: && docs::format
12
+ python -m black docput/
13
+ python -m yaclog format
14
+ {{ just_executable() }} --fmt --unstable
15
+
16
+ # Test
17
+ test:
18
+ python -m pytest --doctest-modules
19
+
20
+ build:
21
+ python -m build
22
+
23
+ pre-commit := f"""
24
+ #!/bin/sh
25
+ PATH={{`dirname "$(which python)"`}}:$PATH just lint test"""
26
+
27
+ # Install recommended git hooks. Do this after activating your virtual environment
28
+ install-hooks:
29
+ echo '{{ pre-commit }}' > .git/hooks/pre-commit
30
+ chmod +x .git/hooks/pre-commit
@@ -0,0 +1,27 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © 2026 Andrew Cassidy
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice, this permission notice, and the
16
+ affirmation that TRANS RIGHTS ARE HUMAN RIGHTS shall be included
17
+ in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
20
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26
+ OTHER DEALINGS IN THE SOFTWARE.
27
+
docput-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: docput
3
+ Version: 0.1.0
4
+ Summary: Deploy multi-versioned docs to static hosting
5
+ Author-email: Andrew Cassidy <drewcassidy@me.com>
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://git.offworld.city/drewcassidy/docput
8
+ Keywords: sphinx,git-pages,documentation,ci
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Utilities
15
+ Classifier: Topic :: Documentation
16
+ Classifier: Topic :: Documentation :: Sphinx
17
+ Classifier: Framework :: Sphinx
18
+ Requires-Python: >=3.14
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.md
21
+ Requires-Dist: click>=8.4
22
+ Requires-Dist: requests>=2.33
23
+ Provides-Extra: sphinx
24
+ Requires-Dist: Sphinx>=9; extra == "sphinx"
25
+ Dynamic: license-file
26
+
27
+ docput
28
+ ======
29
+
30
+ [source](https://git.offworld.city/drewcassidy/docput) •
31
+ [documentation](https://drewcassidy.me/docput/latest/) •
32
+ [pypi](https://pypi.org/project/docput)
33
+
34
+ docput allows you to easily deploy versioned documentation
35
+ to [git-pages](https://git-pages.org) or the local filesystem. Each version of your
36
+ documentation is deployed to its own subfolder, and does not need to be rebuilt when
37
+ other versions are added.
38
+
39
+ ## Installation
40
+
41
+ Install and update with [pip](https://pip.pypa.io/en/stable/getting-started/):
42
+
43
+ ```shell
44
+ $ pip install docput
45
+ ```
docput-0.1.0/README.md ADDED
@@ -0,0 +1,19 @@
1
+ docput
2
+ ======
3
+
4
+ [source](https://git.offworld.city/drewcassidy/docput) •
5
+ [documentation](https://drewcassidy.me/docput/latest/) •
6
+ [pypi](https://pypi.org/project/docput)
7
+
8
+ docput allows you to easily deploy versioned documentation
9
+ to [git-pages](https://git-pages.org) or the local filesystem. Each version of your
10
+ documentation is deployed to its own subfolder, and does not need to be rebuilt when
11
+ other versions are added.
12
+
13
+ ## Installation
14
+
15
+ Install and update with [pip](https://pip.pypa.io/en/stable/getting-started/):
16
+
17
+ ```shell
18
+ $ pip install docput
19
+ ```
@@ -0,0 +1,6 @@
1
+ from pathlib import Path
2
+
3
+ from .sphinx_ext import setup # noqa: F401
4
+
5
+ sphinx_templates = str(Path(__file__).parent / "sphinx_ext/_templates")
6
+ sphinx_static = str(Path(__file__).parent / "sphinx_ext/_static")
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,5 @@
1
+ from .main import main_group as main
2
+ from .manifest import manifest_group as manifest
3
+ from .version import version_group as version
4
+
5
+ __all__ = [main, version, manifest]
@@ -0,0 +1,123 @@
1
+ import shutil
2
+ import sys
3
+ from io import Writer
4
+ from pathlib import Path
5
+ from typing import Any, Callable
6
+
7
+ import click
8
+
9
+ from docput.path import URLPath
10
+ from docput.remote import Remote, open_remote
11
+ from docput.typing import Reader
12
+
13
+
14
+ class RemoteParamType(click.ParamType[Remote]):
15
+ name = "URL"
16
+
17
+ def __init__(self, commit_on_close: bool = True):
18
+ self.commit_on_close = commit_on_close
19
+
20
+ def convert(
21
+ self, value: Any, param: click.Parameter | None, ctx: click.Context | None
22
+ ) -> Remote:
23
+ match value:
24
+ case Remote():
25
+ remote = value
26
+ case str(url):
27
+ remote = open_remote(url)
28
+ case _:
29
+ self.fail(f"Unknown value type {type(value)}")
30
+ if self.commit_on_close:
31
+ assert ctx is not None, "Cannot open remote without a context to bind it to"
32
+ ctx.with_resource(remote)
33
+ return remote
34
+
35
+
36
+ def remote_option[FC: Callable[..., Any] | click.Command](
37
+ commit_on_close=True,
38
+ help="Remote to interact with",
39
+ **kwargs,
40
+ ) -> Callable[[FC], FC]:
41
+ return click.option(
42
+ "--remote",
43
+ "-r",
44
+ required=True,
45
+ type=RemoteParamType(commit_on_close),
46
+ envvar="DOCPUT_REMOTE",
47
+ show_envvar=True,
48
+ help=help,
49
+ **kwargs,
50
+ )
51
+
52
+
53
+ @click.group(context_settings={"show_default": True, "max_content_width": 120})
54
+ @click.version_option()
55
+ def main_group(): ...
56
+
57
+
58
+ @main_group.command(short_help="Download a file.")
59
+ @remote_option(help="Remote to download from.")
60
+ @click.argument("source", type=click.Path(path_type=URLPath))
61
+ @click.argument("dest", type=click.File(mode="xb"))
62
+ def download(remote: Remote, source: URLPath, dest: Writer[bytes]):
63
+ """Download SOURCE from the remote and write it to DEST"""
64
+ buff = remote.open_file(source)
65
+ shutil.copyfileobj(buff, dest)
66
+
67
+
68
+ @main_group.command(short_help="Upload a file or directory.") # type: ignore[attr-defined]
69
+ @remote_option(help="Remote to upload to.")
70
+ @click.option(
71
+ "--overwrite/--merge",
72
+ "-o/-m",
73
+ default=False,
74
+ is_flag=True,
75
+ show_default=True,
76
+ help="Merge or overwrite uploaded directories.",
77
+ )
78
+ @click.argument("source", type=click.Path(exists=True, allow_dash=True, path_type=Path))
79
+ @click.argument("dest", type=click.Path(path_type=URLPath))
80
+ def upload(
81
+ remote: Remote,
82
+ overwrite: bool,
83
+ source: Path | Reader[bytes],
84
+ dest: URLPath,
85
+ ):
86
+ """
87
+ Upload a file or directory SOURCE to the remote at DEST.
88
+
89
+ SOURCE is a path to a file or directory to upload. If SOURCE is `-`, its contents is read from standard input.
90
+
91
+ DEST is a path on the destination to write to.
92
+
93
+ If SOURCE is a file, its contents will be placed at DEST on the remote. Unlike tools like `cp` or `rsync`,
94
+ there is no special handling if DEST refers to a directory
95
+ """
96
+ if isinstance(source, Path) and str(source) == "-":
97
+ source = sys.stdin.buffer
98
+ remote.write(source, dest, overwrite)
99
+
100
+
101
+ @main_group.command(short_help="Delete a file or directory.")
102
+ @remote_option(help="Remote to delete from.")
103
+ @click.argument("path", type=click.Path(path_type=URLPath))
104
+ def rm(remote: Remote, path: URLPath):
105
+ """Delete the file or directory at PATH on the remote"""
106
+ remote.delete(path)
107
+
108
+
109
+ @main_group.command(short_help="Create a symlink.")
110
+ @remote_option(help="Remote to create a symlink on.")
111
+ @click.argument("source", type=click.Path(path_type=URLPath))
112
+ @click.argument("dest", type=click.Path(path_type=URLPath))
113
+ def ln(remote: Remote, source: URLPath, dest: URLPath):
114
+ """Create a symlink on the remote at DEST pointing to SOURCE"""
115
+ remote.make_symlink(source, dest)
116
+
117
+
118
+ @main_group.command(short_help="Create a directory.")
119
+ @remote_option(help="Remote to create a directory on.")
120
+ @click.argument("path", type=click.Path(path_type=URLPath))
121
+ def mkdir(remote: Remote, path: URLPath):
122
+ """Create a directory on the remote at PATH. Any existing file or directory at PATH will be deleted"""
123
+ remote.make_dir(path)
@@ -0,0 +1,58 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+
7
+ import click
8
+ from click import ClickException
9
+
10
+ from docput.config import get_config
11
+ from .main import main_group, remote_option
12
+ from ..manifest import VersionManifest
13
+
14
+
15
+ @main_group.group(help="Edit the version manifest.")
16
+ def manifest_group(): ...
17
+
18
+
19
+ @manifest_group.command(short_help="Open the version manifest in a text editor")
20
+ @remote_option()
21
+ @click.option(
22
+ "--open",
23
+ "-O",
24
+ "editor",
25
+ is_flag=False,
26
+ flag_value=os.environ.get("EDITOR") or "nano",
27
+ help="Open the manifest file in a text editor and write it to the remote on exit.",
28
+ )
29
+ @click.option(
30
+ "--url",
31
+ "root_url",
32
+ metavar="URL",
33
+ help="""
34
+ Set the root URL to a new value. The root URL is the base for all hrefs in the version manifest. This must be absolute (either a path starting
35
+ with "/", or a full URL like "https://example.com/docs/"). If it is not set, it is assumed to be "/".
36
+ """,
37
+ )
38
+ def edit(remote, editor: str | None, root_url: str | None):
39
+ """Edit the version manifest."""
40
+ if editor is not None:
41
+ with TemporaryDirectory() as td:
42
+ tmp_path = Path(td) / get_config().get("manifest_url")
43
+ with open(tmp_path, "wb") as temp:
44
+ shutil.copyfileobj(remote.manifest.dump(), temp)
45
+ while True:
46
+ subprocess.run([editor, tmp_path], check=True)
47
+ try:
48
+ with open(tmp_path, "rb") as temp:
49
+ remote.manifest = VersionManifest(temp)
50
+ return
51
+ except Exception as e:
52
+ retry = click.confirm(f"Error parsing manifest file: {e}. Retry?")
53
+ if not retry:
54
+ raise ClickException(
55
+ f"Aborting after error parsing manifest file: {e}"
56
+ )
57
+ if root_url is not None:
58
+ remote.manifest.root_url = root_url
@@ -0,0 +1,183 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import Any, Callable
4
+
5
+ import click
6
+ from click import ClickException
7
+
8
+ from .main import main_group, remote_option
9
+ from ..config import get_config
10
+ from ..manifest import VersionRecord
11
+ from ..path import URLPath, reroot
12
+ from ..remote import Remote
13
+
14
+
15
+ def ref_option[FC: Callable[..., Any] | click.Command](**kwargs) -> Callable[[FC], FC]: # type: ignore[name-defined]
16
+ return click.option(
17
+ "--ref",
18
+ "-R",
19
+ envvar=["DOCPUT_VERSION_REF", "FORGEJO_REF", "GITHUB_REF"],
20
+ show_envvar=True,
21
+ required=True,
22
+ **kwargs,
23
+ )
24
+
25
+
26
+ @main_group.group(short_help="Create or edit versions.")
27
+ def version_group(): ...
28
+
29
+
30
+ @version_group.command(short_help="Push a documentation version to the remote.")
31
+ @click.argument("source", type=click.Path(exists=True, file_okay=False, path_type=Path))
32
+ @remote_option(help="Remote to push to.")
33
+ @ref_option(help="Version reference to push to.")
34
+ @click.option(
35
+ "--latest",
36
+ "-l",
37
+ is_flag=True,
38
+ default=False,
39
+ help="Mark this version as the latest release.",
40
+ )
41
+ @click.option(
42
+ "--visible/--hidden",
43
+ "-v/-h",
44
+ default=None,
45
+ help="Show this version in the version menu on the site.",
46
+ )
47
+ @click.option(
48
+ "--url",
49
+ "-u",
50
+ envvar=["DOCPUT_VERSION_URL"],
51
+ show_envvar=True,
52
+ help="""
53
+ Subdirectory on the site to write and link to. If not provided when pushing a new version, use the version ref with any text matching regex in the
54
+ config value `version.url_strip` removed. By default, this removes "refs/" from the start of the reference, so a version with the reference
55
+ `refs/tags/v1.0.0` would be published to `tags/v1.0.0`
56
+ """,
57
+ )
58
+ @click.option(
59
+ "--name",
60
+ "-n",
61
+ envvar=["DOCPUT_VERSION_NAME", "FORGEJO_REF_NAME", "GITHUB_REF_NAME"],
62
+ show_envvar=True,
63
+ help="The name to display for this version in the version menu.",
64
+ )
65
+ def push(
66
+ source: Path,
67
+ remote: Remote,
68
+ ref: str,
69
+ latest: bool,
70
+ visible: bool | None,
71
+ url: str | None,
72
+ name: str | None,
73
+ ):
74
+ """
75
+ Push a documentation version from SOURCE to the remote.
76
+ """
77
+ config = get_config()
78
+ manifest = remote.manifest
79
+ if (version := manifest.versions.get(ref)) is None:
80
+ # version does not exist in the manifest, so we need to make sure some values get filled in
81
+ if url is None:
82
+ url = re.sub(config.get("url_strip_regex"), "", ref)
83
+ version = VersionRecord(ref, {})
84
+ manifest.versions[ref] = version
85
+
86
+ # remove any old deployed versions. Old href may not equal the new href
87
+ if version.url is not None:
88
+ remote.delete(version.url)
89
+
90
+ # update the version properties
91
+ if name is not None:
92
+ version.name = name
93
+ if url is not None:
94
+ version.url = url
95
+ if visible is not None:
96
+ version.visible = visible
97
+
98
+ # root of the version we are deploying to
99
+ if version.url is None:
100
+ raise ClickException(f"Version {ref} has no href and none was provided")
101
+ version_url = URLPath(version.url)
102
+ assert not version_url.is_absolute(), "version URL must be a relative path"
103
+
104
+ # write local version and symlink in the manifest
105
+ remote.write(source, version_url, overwrite=True)
106
+ remote.make_symlink(
107
+ URLPath("/") / config.get("manifest_url"),
108
+ reroot(URLPath("/", version_url), config.get("manifest_url")),
109
+ )
110
+
111
+ # if this is the latest, make a symlink from /latest
112
+ if latest:
113
+ manifest.latest = version
114
+ latest_url = URLPath(config.get("latest_url"))
115
+ assert not latest_url.is_absolute(), "latest URL must be a relative path"
116
+ remote.make_symlink(version_url, latest_url)
117
+
118
+
119
+ @version_group.command()
120
+ @remote_option(help="Remote to delete the version from.")
121
+ @ref_option(help="Version reference to delete.")
122
+ @click.option(
123
+ "--keep",
124
+ is_flag=True,
125
+ default=False,
126
+ help="Keep the corresponding documentation, just delete the manifest record.",
127
+ )
128
+ def delete(remote: Remote, ref, keep):
129
+ """Delete a version from the remote."""
130
+ manifest = remote.manifest
131
+ if (latest := manifest.latest) is not None and latest.ref == ref:
132
+ manifest.latest = None
133
+
134
+ if (version := manifest.versions.pop(ref)) is None:
135
+ raise ClickException(f"Version {ref} does not exist in the manifest")
136
+
137
+ if not keep and version.url is not None:
138
+ remote.delete(version.url)
139
+
140
+
141
+ @version_group.command()
142
+ @remote_option(help="Remote to list versions on.", commit_on_close=False)
143
+ def list_command(remote: Remote):
144
+ """List all versions in the manifest."""
145
+ for version in remote.manifest.versions.values():
146
+ print(version)
147
+
148
+
149
+ @version_group.command()
150
+ @remote_option()
151
+ @ref_option()
152
+ @click.option(
153
+ "--visible/--hidden",
154
+ "-v/-h",
155
+ help="Show this version in the version menu on the site.",
156
+ )
157
+ @click.option(
158
+ "--url",
159
+ "-u",
160
+ envvar=["DOCPUT_VERSION_URL"],
161
+ show_envvar=True,
162
+ help="URL on the site to write and link to. Must be a relative-path reference.",
163
+ )
164
+ @click.option(
165
+ "--name",
166
+ "-n",
167
+ envvar=["DOCPUT_VERSION_NAME"],
168
+ show_envvar=True,
169
+ help="The name to display for this version in the version menu.",
170
+ )
171
+ def edit(
172
+ remote: Remote, ref: str, visible: bool | None, url: str | None, name: str | None
173
+ ):
174
+ """Edit a version in the manifest."""
175
+ if (version := remote.manifest.versions.get(ref)) is None:
176
+ raise ClickException(f"Version {ref} does not exist in the manifest")
177
+
178
+ if visible is not None:
179
+ version.visible = visible
180
+ if url is not None:
181
+ version.url = url
182
+ if name is not None:
183
+ version.name = name