diff --git a/README.md b/README.md index fe02273..f95ebba 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,14 @@ The following formatting placeholders are supported: * `{name}`: Human readable name of the resource e.g. `Python`. * `{full_name}`: Name with optional version for the resource e.g. `Python 3.12`. * `{slug}`: Devdocs slug for the resource e.g. `python~3.12`. +* `{clean_slug}`: Slug with non alphanumeric/period characters replaced with `-` e.g. `python-3.12`. * `{slug_without_version}`: Devdocs slug for the resource without the version e.g. `python`. * `{version}`: Shortened version displayed in devdocs, if any e.g. `3.12`. * `{release}`: Specific release of the software the documentation is for, if any e.g. `3.12.1`. * `{attribution}`: License and attribution information about the resource. * `{home_link}`: Link to the project's home page, if any: e.g. `https://python.org`. * `{code_link}`: Link to the project's source, if any: e.g. `https://github.com/python/cpython`. +* `{period}`: The current date in `YYYY-MM` format e.g. `2024-02`. ## Developing diff --git a/src/devdocs2zim/client.py b/src/devdocs2zim/client.py index d7924c5..b5eb6ec 100644 --- a/src/devdocs2zim/client.py +++ b/src/devdocs2zim/client.py @@ -1,5 +1,7 @@ +import datetime import re from collections import defaultdict +from collections.abc import Callable from enum import Enum from functools import cached_property @@ -50,8 +52,14 @@ class DevdocsMetadata(BaseModel): def slug_without_version(self): return self.slug.split("~")[0] - def placeholders(self) -> dict[str, str]: - """Gets placeholders for filenames.""" + def placeholders( + self, clock: Callable[[], datetime.date] = datetime.date.today + ) -> dict[str, str]: + """Gets placeholders for filenames. + + Arguments: + clock: Override the default clock to use for producing the "period". + """ home_link = "" code_link = "" if self.links is not None: @@ -68,12 +76,14 @@ def placeholders(self) -> dict[str, str]: "name": self.name, "full_name": full_name, "slug": self.slug, + "clean_slug": re.sub(r"[^.a-zA-Z0-9]", "-", self.slug), "version": self.version, "release": self.release, "attribution": self.attribution, "home_link": home_link, "code_link": code_link, "slug_without_version": self.slug_without_version, + "period": clock().strftime("%Y-%m"), } diff --git a/src/devdocs2zim/entrypoint.py b/src/devdocs2zim/entrypoint.py index 0725326..7815015 100644 --- a/src/devdocs2zim/entrypoint.py +++ b/src/devdocs2zim/entrypoint.py @@ -15,7 +15,8 @@ def zim_defaults() -> ZimConfig: """Returns the default configuration for ZIM generation.""" return ZimConfig( - name_format="devdocs_{slug_without_version}_{version}", + file_name_format="devdocs.io_en_{clean_slug}_{period}", + name_format="devdocs.io_en_{clean_slug}", creator="DevDocs", publisher="openZIM", title_format="{full_name} Docs", diff --git a/src/devdocs2zim/generator.py b/src/devdocs2zim/generator.py index fb6d67c..9b8e76f 100644 --- a/src/devdocs2zim/generator.py +++ b/src/devdocs2zim/generator.py @@ -43,7 +43,9 @@ class InvalidFormatError(Exception): class ZimConfig(BaseModel): """Common configuration for building ZIM files.""" - # File name/name for the ZIM. + # File name for the ZIM. + file_name_format: str + # Name for the ZIM. name_format: str # Human readable title for the ZIM. title_format: str @@ -73,6 +75,14 @@ def add_flags(parser: argparse.ArgumentParser, defaults: "ZimConfig"): default=defaults.publisher, ) + parser.add_argument( + "--file-name-format", + help="Custom file name format for individual ZIMs. " + f"Default: {defaults.file_name_format!r}", + default=defaults.file_name_format, + metavar="FORMAT", + ) + parser.add_argument( "--name-format", help="Custom name format for individual ZIMs. " @@ -149,6 +159,7 @@ def check_length(string: str, field_name: str, length: int) -> str: return string return ZimConfig( + file_name_format=fmt(self.file_name_format), name_format=fmt(self.name_format), title_format=check_length( fmt(self.title_format), @@ -383,7 +394,7 @@ def generate_zim( logger.info(f"Generating ZIM for {doc_metadata.slug}") formatted_config = self.zim_config.format(doc_metadata.placeholders()) - zim_path = Path(self.output_folder, f"{formatted_config.name_format}.zim") + zim_path = Path(self.output_folder, f"{formatted_config.file_name_format}.zim") # Don't clobber existing files so a user can resume a failed run. if zim_path.exists(): diff --git a/tests/test_client.py b/tests/test_client.py index 857b5ae..c334040 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,3 +1,4 @@ +import datetime from unittest import TestCase from unittest.mock import ANY, create_autospec, patch @@ -111,19 +112,21 @@ def test_slug_without_version_version(self): def test_placeholders_minimal(self): metadata = DevdocsMetadata(name="test", slug="test~1.23") - placeholders = metadata.placeholders() + placeholders = metadata.placeholders(clock=lambda: datetime.date(2001, 2, 3)) self.assertEqual( { "name": "test", "full_name": "test", "slug": "test~1.23", + "clean_slug": "test-1.23", "version": "", "release": "", "attribution": "", "home_link": "", "code_link": "", "slug_without_version": "test", + "period": "2001-02", }, placeholders, ) @@ -141,19 +144,21 @@ def test_placeholders_full(self): attribution="© 2022 The Kubernetes Authors", ) - placeholders = metadata.placeholders() + placeholders = metadata.placeholders(clock=lambda: datetime.date(2001, 2, 3)) self.assertEqual( { "name": "Kubernetes", "full_name": "Kubernetes 1.28.1", "slug": "kubernetes~1.28", + "clean_slug": "kubernetes-1.28", "version": "1.28.1", "release": "1.28", "attribution": "© 2022 The Kubernetes Authors", "home_link": "https://kubernetes.io/", "code_link": "https://github.com/kubernetes/kubernetes", "slug_without_version": "kubernetes", + "period": "2001-02", }, placeholders, ) diff --git a/tests/test_generator.py b/tests/test_generator.py index e23f5e1..d05cfbe 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -23,6 +23,7 @@ class TestZimConfig(TestCase): def defaults(self) -> ZimConfig: return ZimConfig( + file_name_format="default_file_name_format", name_format="default_name_format", title_format="default_title_format", publisher="default_publisher", @@ -54,6 +55,8 @@ def test_flag_parsing_overrides(self): "publisher", "--name-format", "name-format", + "--file-name-format", + "file-name-format", "--title-format", "title-format", "--description-format", @@ -71,6 +74,7 @@ def test_flag_parsing_overrides(self): creator="creator", publisher="publisher", name_format="name-format", + file_name_format="file-name-format", title_format="title-format", description_format="description-format", long_description_format="long-description-format", @@ -88,6 +92,7 @@ def test_format_none_needed(self): def test_format_only_allowed(self): to_format = ZimConfig( + file_name_format="{replace_me}", name_format="{replace_me}", title_format="{replace_me}", publisher="{replace_me}", @@ -101,6 +106,7 @@ def test_format_only_allowed(self): self.assertEqual( ZimConfig( + file_name_format="replaced", name_format="replaced", title_format="replaced", publisher="{replace_me}",