Skip to content

Commit 405066d

Browse files
committed
Actions fully unit tested
1 parent 904170e commit 405066d

19 files changed

+605
-314
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ mypy = "^1.8.0"
4040
[tool.poetry.plugins."mkdocs_deploy.plugins"]
4141
"local" = "mkdocs_deploy.plugins.local_filesystem:enable_plugin"
4242
"s3" = "mkdocs_deploy.plugins.aws_s3:enable_plugin"
43+
"html-redirect" = "mkdocs_deploy.plugins.html_redirect:enable_plugin"
4344

4445
[tool.poetry.group.dev.dependencies]
4546
build = "^0.10.0"

source/mkdocs_deploy/abstract.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class RedirectMechanismNotFound(Exception):
1717
class _DefaultVersionType(Enum):
1818
DEFAULT_VERSION = 0
1919

20+
def __repr__(self) -> str:
21+
return self.name
22+
2023

2124
DEFAULT_VERSION = _DefaultVersionType.DEFAULT_VERSION
2225

@@ -268,4 +271,28 @@ def target_for_url(target_url: str) -> Target:
268271
:return:
269272
"""
270273
handler = _TARGETS[urllib.parse.urlparse(target_url).scheme]
271-
return handler(target_url)
274+
return handler(target_url)
275+
276+
277+
_SHARED_REDIRECT_MECHANISMS: dict[str, RedirectMechanism] = {}
278+
279+
280+
def get_redirect_mechanisms(session: TargetSession) -> dict[str, RedirectMechanism]:
281+
"""Get all available redirect mechanisms for a target session
282+
283+
Unlike the property returned by the target session itself, this will also include shared redirect mechanisms.
284+
"""
285+
result = _SHARED_REDIRECT_MECHANISMS.copy()
286+
result.update(session.available_redirect_mechanisms)
287+
return result
288+
289+
290+
def register_shared_redirect_mechanism(mechanism_key: str, mechanism: RedirectMechanism) -> None:
291+
"""Register a redirect mechanism which can work with any target type from any plugin.
292+
293+
DO NOT use this to simply add a mechanism to your own plugin. The target session should return mechanisms that
294+
only work on that target.
295+
296+
There are believed to be very few of these: html is the only inbuilt mechanism. This can work with any target
297+
because it only generates html files and all target types support uploading html files."""
298+
_SHARED_REDIRECT_MECHANISMS[mechanism_key] = mechanism

source/mkdocs_deploy/actions.py

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
"""
99
import importlib.metadata
1010
import logging
11-
from typing import Iterable, Optional
11+
from typing import Collection
1212

13-
from .abstract import DEFAULT_VERSION, Source, TargetSession, Version, VersionNotFound
13+
from .abstract import DEFAULT_VERSION, Source, TargetSession, Version, VersionNotFound, get_redirect_mechanisms
1414
from .versions import DeploymentAlias
1515

1616
_logger = logging.getLogger(__name__)
@@ -71,6 +71,8 @@ def delete_version(target: TargetSession, version_id: str) -> None:
7171
:param version_id:
7272
"""
7373
deployment_spec = target.deployment_spec
74+
if deployment_spec.default_version is not None and deployment_spec.default_version.version_id == version_id:
75+
delete_alias(target, DEFAULT_VERSION)
7476
for alias_id in deployment_spec.aliases_for_version(version_id):
7577
delete_alias(target, alias_id)
7678
_logger.info("Deleting version %s", version_id)
@@ -82,7 +84,7 @@ def delete_version(target: TargetSession, version_id: str) -> None:
8284

8385

8486
def create_alias(
85-
target: TargetSession, alias_id: Version, version: str, mechanisms: Iterable[str] | None = None
87+
target: TargetSession, alias_id: Version, version: str, mechanisms: Collection[str] | None = None
8688
) -> None:
8789
"""
8890
Create a new alias for a version.
@@ -93,66 +95,68 @@ def create_alias(
9395
:param target: The target session to create the alias on
9496
:param alias_id: The new alias id
9597
:param version: The version_id to point to
96-
:param mechanisms: The named mechanisms to use. If None then 'html' target will choose the mechanism.
98+
:param mechanisms: The named mechanisms to use. If None then all available mechanisms will be used.
9799
"""
98100
# Check if the given mechanisms can be implemented by this target
99-
available_redirect_mechanisms = target.available_redirect_mechanisms
101+
available_redirect_mechanisms = get_redirect_mechanisms(target)
100102
if mechanisms is not None:
101103
for mechanism in mechanisms:
102104
if mechanism not in available_redirect_mechanisms:
103-
raise ValueError(f"LocalFileTreeTarget does not support redirect mechanism: {mechanism}")
105+
raise ValueError(f"{type(TargetSession).__name__} does not support redirect mechanism: {mechanism}")
104106

105107
# Check if the alias already exists ...
106108
# If mechanisms wasn't specified use whatever is on the existing one.
107109
deployment_spec = target.deployment_spec
108110
if alias_id in deployment_spec.versions:
109111
raise ValueError(f"Cannot create an alias with the same name as an existing version. "
110112
f"Delete the version first! Alias name: {alias_id}")
111-
if alias_id is ... and deployment_spec.default_version is not None:
113+
if alias_id is DEFAULT_VERSION and deployment_spec.default_version is not None:
112114
# This is the "default" alias
113115
alias = deployment_spec.default_version
114-
if mechanisms is None:
115-
mechanisms = alias.redirect_mechanisms
116-
if alias_id in deployment_spec.aliases:
116+
elif alias_id in deployment_spec.aliases:
117117
alias = deployment_spec.aliases[alias_id]
118-
if mechanisms is None:
119-
mechanisms = alias.redirect_mechanisms
120118
else:
121119
# No existing alias was found. Make a new one.
122120
alias = DeploymentAlias(version_id=version, redirect_mechanisms=set())
123121
target.set_alias(alias_id, alias)
124-
# Must set the alias first or creating the mechanism will fail.
125-
if mechanisms is None:
126-
mechanisms = ["html"]
122+
123+
if mechanisms is None:
124+
mechanisms = available_redirect_mechanisms.keys()
127125

128126
_logger.info("Creating %s alias redirect %s to %s", ", ".join(mechanisms), alias_id, version)
129127
# Remove any redirect mechanisms to a different version that we are not going to replace
130-
if alias.version_id != version:
131-
for mechanism in alias.redirect_mechanisms.copy():
132-
if mechanism not in mechanisms:
133-
try:
134-
available_redirect_mechanisms[mechanism].delete_redirect(target, alias_id)
135-
except KeyError:
136-
raise ValueError(f"LocalFileTreeTarget does not support redirect mechanism: {mechanism}. "
137-
f"Unable to remove redirect for {alias_id}-->{alias.version_id}")
138-
alias.redirect_mechanisms.discard(mechanism)
139-
alias.version_id = version
128+
for mechanism in alias.redirect_mechanisms.copy():
129+
if mechanism not in mechanisms:
130+
try:
131+
_logger.warning(
132+
"Implicitly deleting redirect %s mechanism %s to %s", alias_id, mechanism, alias.version_id
133+
)
134+
available_redirect_mechanisms[mechanism].delete_redirect(target, alias_id)
135+
except KeyError:
136+
raise ValueError(
137+
f"{type(TargetSession).__name__} does not support redirect mechanism: {mechanism}. "
138+
f"Unable to remove redirect for {alias_id}-->{alias.version_id}"
139+
)
140+
alias.redirect_mechanisms.discard(mechanism)
140141

141142
# Create the redirects or refresh them to their new location.
142143
for mechanism in mechanisms:
143144
if mechanism in alias.redirect_mechanisms:
144145
if alias.version_id != version:
146+
_logger.debug("Modifying %s mechanism %s from %s to %s", alias_id, mechanism, alias.version_id, version)
145147
available_redirect_mechanisms[mechanism].refresh_redirect(target, alias_id, version)
146148
else:
147149
_logger.debug("mechanism %s already in place, skipping", mechanism)
148150
else:
151+
_logger.debug("Creating %s mechanism %s to %s", alias_id, mechanism, version)
149152
available_redirect_mechanisms[mechanism].create_redirect(target, alias_id, version)
150153
alias.redirect_mechanisms.add(mechanism)
151154

155+
alias.version_id = version
152156
target.set_alias(alias_id, alias)
153157

154158

155-
def delete_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable[str] | None = None) -> None:
159+
def delete_alias(target: TargetSession, alias_id: Version, mechanisms: Collection[str] | None = None) -> None:
156160
"""
157161
Delete an alias.
158162
@@ -167,20 +171,20 @@ def delete_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable[
167171
if alias_id is DEFAULT_VERSION:
168172
alias = target.deployment_spec.default_version
169173
if alias is None:
170-
_logger.debug("Default alias not set")
174+
_logger.warning("Cannot delete default alias as it is not set")
171175
return
172176
else:
173177
try:
174178
alias = target.deployment_spec.aliases[alias_id]
175179
except KeyError:
176-
_logger.debug("Alias %s not set, skipping", alias_id)
180+
_logger.warning("Cannot delete alias %s not set, it has not been set", alias_id)
177181
return
178182

179183
if mechanisms is not None:
180-
to_delete = [mechanism for mechanism in mechanisms if mechanism in alias.redirect_mechanisms]
184+
to_delete: list | set = [mechanism for mechanism in mechanisms if mechanism in alias.redirect_mechanisms]
181185
else:
182186
to_delete = alias.redirect_mechanisms.copy()
183-
available_mechanisms = target.available_redirect_mechanisms
187+
available_mechanisms = get_redirect_mechanisms(target)
184188
for mechanism in to_delete:
185189
try:
186190
available_mechanisms[mechanism].delete_redirect(
@@ -196,7 +200,7 @@ def delete_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable[
196200
target.set_alias(alias_id, None)
197201

198202

199-
def refresh_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable[str] | None = None) -> None:
203+
def refresh_alias(target: TargetSession, alias_id: Version, mechanisms: Collection[str] | None = None) -> None:
200204
"""
201205
Refresh redirects.
202206
@@ -218,7 +222,7 @@ def refresh_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable
218222
to_refresh = {mechanism for mechanism in mechanisms if mechanism in alias.redirect_mechanisms}
219223
else:
220224
to_refresh = alias.redirect_mechanisms
221-
available_mechanisms = target.available_redirect_mechanisms
225+
available_mechanisms = get_redirect_mechanisms(target)
222226
for mechanism in to_refresh:
223227
available_mechanisms[mechanism].refresh_redirect(
224228
session=target,

source/mkdocs_deploy/plugins/aws_s3.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@
1818
logging.getLogger("s3transfer").setLevel("INFO")
1919

2020

21-
AWS_S3_REDIRECT_MECHANISMS = shared_implementations.SHARED_REDIRECT_MECHANISMS.copy()
22-
# TODO add S3 redirect
23-
24-
2521
def enable_plugin() -> None:
2622
"""
2723
Enables the plugin.
@@ -179,7 +175,9 @@ def set_alias(self, alias_id: Union[str, type(...)], alias: Optional[versions.De
179175

180176
@property
181177
def available_redirect_mechanisms(self) -> dict[str, abstract.RedirectMechanism]:
182-
return AWS_S3_REDIRECT_MECHANISMS.copy()
178+
# TODO add S3 redirect
179+
# TODO cloud front redirect rules
180+
return {}
183181

184182
@property
185183
def deployment_spec(self) -> versions.DeploymentSpec:
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from io import BytesIO
2+
3+
from ..abstract import DEFAULT_VERSION, RedirectMechanism, TargetSession, Version, register_shared_redirect_mechanism
4+
5+
6+
def enable_plugin() -> None:
7+
"""Enables the plugin.
8+
9+
Registers HTML redirect mechanism"""
10+
register_shared_redirect_mechanism("html", HtmlRedirect())
11+
12+
13+
class HtmlRedirect(RedirectMechanism):
14+
15+
def create_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None:
16+
if alias is DEFAULT_VERSION:
17+
session.upload_file(
18+
version_id=DEFAULT_VERSION,
19+
filename="index.html",
20+
file_obj=BytesIO(_HTML_REDIRECT_PATTERN.format(url=version_id+"/").encode("utf-8"))
21+
)
22+
else:
23+
files_created = set()
24+
for filename in session.iter_files(version_id):
25+
if filename.endswith(".html") or filename.endswith(".htm"):
26+
if filename == "404.html" or filename.endswith("/404.htm"):
27+
session.upload_file(
28+
version_id=alias,
29+
filename=filename,
30+
file_obj=session.download_file(version_id=version_id, filename=filename)
31+
)
32+
else:
33+
parts = filename.split("/")
34+
depth = len(parts)
35+
url = ("../" * depth + version_id + "/" + "/".join(parts[:-1]))
36+
session.upload_file(
37+
version_id=alias, # Yes that's correct!
38+
filename=filename,
39+
file_obj=BytesIO(_HTML_REDIRECT_PATTERN.format(url=url).encode("utf-8"))
40+
)
41+
files_created.add(filename)
42+
for filename in session.iter_files(alias):
43+
if filename not in files_created and (filename.endswith("html") or filename.endswith("htm")):
44+
session.delete_file(alias, filename)
45+
46+
def refresh_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None:
47+
# create_redirect already cleans up so no need to explicitly delete the old one
48+
self.create_redirect(session, alias, version_id)
49+
50+
def delete_redirect(self, session: TargetSession, alias: Version) -> None:
51+
if alias is ...:
52+
session.delete_file(version_id=..., filename="index.html")
53+
else:
54+
for filename in session.iter_files(alias):
55+
if filename.endswith("html") or filename.endswith("htm"):
56+
session.delete_file(version_id=alias, filename=filename)
57+
58+
_HTML_REDIRECT_PATTERN="""<!DOCTYPE html>
59+
<html>
60+
<head>
61+
<meta charset="utf-8">
62+
<title>Redirecting</title>
63+
<noscript>
64+
<meta http-equiv="refresh" content="1; url={url}" />
65+
</noscript>
66+
<script>
67+
window.location.replace("{url}" + window.location.hash);
68+
</script>
69+
</head>
70+
<body>
71+
Redirecting to <a href="{url}">{url}</a>...
72+
</body>
73+
</html>
74+
"""

source/mkdocs_deploy/plugins/local_filesystem.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
from .. import abstract, shared_implementations
1111
from ..versions import DeploymentAlias, DeploymentSpec, DeploymentVersion
1212

13-
LOCAL_FILE_REDIRECT_MECHANISMS = shared_implementations.SHARED_REDIRECT_MECHANISMS.copy()
14-
1513
_logger = logging.getLogger(__name__)
1614

1715

@@ -66,6 +64,7 @@ def open_file_for_read(self, filename: str) -> IO[bytes]:
6664
result = self._tar_file.extractfile(self._prefix + filename)
6765
if result is None:
6866
raise RuntimeError(f"Requested file is not a regular file: {filename} in {self._file_path}")
67+
return result
6968

7069
def close(self):
7170
self._tar_file.close()
@@ -233,8 +232,7 @@ def set_alias(self, alias_id: Union[str, type(...)], alias: Optional[DeploymentA
233232

234233
@property
235234
def available_redirect_mechanisms(self) -> dict[str, abstract.RedirectMechanism]:
236-
return LOCAL_FILE_REDIRECT_MECHANISMS.copy()
237-
235+
return {}
238236

239237
@property
240238
def deployment_spec(self) -> DeploymentSpec:

0 commit comments

Comments
 (0)