From 66413c610a5df94edda7f7ae5db7f26a4ab2ebbe Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 13 Jan 2024 19:57:09 +0100 Subject: [PATCH] Update tag/version number validation --- dwds/info.json | 2 +- src/brand.py | 17 ++++++++-------- src/custom_apps.py | 10 +++++----- src/generate_and_download.py | 8 ++++---- src/info_parser.py | 28 +++++++------------------- src/tag_validator.py | 34 ++++++++++++++++++++++++++------ src/version.py | 31 +++++++++++++++++++++++++++++ tests/info_parser_test.py | 38 ++++++++++++------------------------ tests/test.json | 2 +- tests/version_test.py | 26 ++++++++++++++++++++++++ 10 files changed, 123 insertions(+), 73 deletions(-) create mode 100644 src/version.py create mode 100644 tests/version_test.py diff --git a/dwds/info.json b/dwds/info.json index a3efca3..23298fb 100644 --- a/dwds/info.json +++ b/dwds/info.json @@ -3,7 +3,7 @@ "about_text": "Für Schreibende, Lernende, Lehrende und Sprachinteressierte: Das Digitale Wörterbuch der deutschen Sprache (DWDS) ist das große Bedeutungswörterbuch des Deutschen der Gegenwart. Es bietet umfassende und wissenschaftlich verlässliche lexikalische Informationen, kostenlos und werbefrei.", "app_name": "DWDS", "app_store_id": "id6473090365", - "build_version": 3, + "build_number": 3, "enforced_lang": "de", "settings_default_external_link_to": "alwaysLoad", "settings_show_external_link_option": false, diff --git a/src/brand.py b/src/brand.py index e424219..26b4a58 100644 --- a/src/brand.py +++ b/src/brand.py @@ -1,22 +1,21 @@ from pathlib import Path -import sys INFO_JSON = 'info.json' + class Brand: - + def __init__(self, name): if Path(name).is_dir() == False: - self._exit_with_error(f"The directory of the brand: '{name}' does not exist") + raise FileExistsError( + f"The directory for brand: '{name}' does not exist") self.info_file = Path(name)/INFO_JSON if self.info_file.exists() == False: - self._exit_with_error(f"There is no {INFO_JSON} file for brand {name}") + raise FileExistsError( + f"There is no {INFO_JSON} file for brand '{name}'") + self.name = name - + @staticmethod def all_info_files(): return list(Path().rglob(INFO_JSON)) - - def _exit_with_error(self, msg): - print(f"Error: {msg}") - sys.exit(1) diff --git a/src/custom_apps.py b/src/custom_apps.py index 25afe5e..471917c 100644 --- a/src/custom_apps.py +++ b/src/custom_apps.py @@ -7,8 +7,8 @@ INFO_JSON = 'info.json' class CustomApps: - def __init__(self, brands=["all"], build_version=None): - self.build_version = build_version + def __init__(self, brands=["all"], build_number=None): + self.build_number = build_number if brands == ["all"]: self.info_files = Brand.all_info_files() else: @@ -28,7 +28,7 @@ class CustomApps: dict = {"include": ["project.yml"]} targets = {} for info in self.info_files: - parser = InfoParser(info, build_version=self.build_version) + parser = InfoParser(info, build_number=self.build_number) targets = targets | parser.as_project_yml() dict["targets"] = targets @@ -43,7 +43,7 @@ class CustomApps: it should be a copy from the Kiwix target """ for info in self.info_files: - parser = InfoParser(info, build_version=self.build_version) + parser = InfoParser(info, build_number=self.build_number) parser.create_plist(based_on_plist_file=custom_plist) def download_zim_files(self): @@ -60,7 +60,7 @@ class CustomApps: array: commands that can be feeded into subprocess.call() """ for info in self.info_files: - parser = InfoParser(info, build_version=self.build_version) + parser = InfoParser(info, build_number=self.build_number) url = parser.zimurl() file_path = parser.zim_file_path() auth = parser.download_auth() diff --git a/src/generate_and_download.py b/src/generate_and_download.py index 468e9d1..f43ae56 100644 --- a/src/generate_and_download.py +++ b/src/generate_and_download.py @@ -19,17 +19,17 @@ def main(): ) parser.add_argument( - "build_version", + "build_number", nargs='?', default=None, - help="The optional build version to use, if not provided will fall back to the build_version defined in the info.json value", + help="The optional build version to use, if not provided will fall back to the build_number defined in the info.json value", type=int ) args = parser.parse_args() brand = args.brand_name - build_version = args.build_version + build_number = args.build_number - custom_apps = CustomApps(brands=[brand], build_version=build_version) + custom_apps = CustomApps(brands=[brand], build_number=build_number) # create the plist files custom_apps.create_plists(custom_plist=Path("Custom.plist")) diff --git a/src/info_parser.py b/src/info_parser.py index 7205593..2bb1a76 100644 --- a/src/info_parser.py +++ b/src/info_parser.py @@ -1,8 +1,8 @@ from urllib.parse import urlparse import json from pathlib import Path +from version import Version import os -import re import shutil import plistlib @@ -23,20 +23,22 @@ JSON_TO_PLIST_MAPPING = { class InfoParser: - def __init__(self, json_path, build_version=None): + def __init__(self, json_path, build_number=None): """Parse a specific info.json file for a brand Args: json_path (Path): of the branded info.json file - build_number (int, optional): If defined it will be used instead of the info.json[build_version]. Defaults to None. + build_number (int, optional): If defined it will be used instead of the info.json[build_number]. Defaults to None. """ self.brand_name = self._brandname_from(json_path) - self.build_version = build_version content = json_path.read_text() self.data = json.loads(content) assert (JSON_KEY_ZIM_URL in self.data) self.zim_file_name = self._filename_from( self.data[JSON_KEY_ZIM_URL]) + build_number = build_number or self.data["build_number"] + self.version = Version.from_file_name(file_name=self.zim_file_name, + build_number=build_number) def create_plist(self, based_on_plist_file): with based_on_plist_file.open(mode="rb") as file: @@ -54,7 +56,7 @@ class InfoParser: dict = { "templates": ["ApplicationTemplate"], "settings": {"base": { - "MARKETING_VERSION": self._app_version(), + "MARKETING_VERSION": self.version.semantic, "PRODUCT_BUNDLE_IDENTIFIER": f"org.kiwix.custom.{self.brand_name}", "INFOPLIST_FILE": f"custom/{self._info_plist_path()}", "INFOPLIST_KEY_CFBundleDisplayName": self._app_name(), @@ -104,10 +106,6 @@ class InfoParser: value = self.data[json_key] yield {plistKey: value} - def _app_version(self): - build_version = self.build_version or self.data["build_version"] - return f"{self._app_version_from(self.zim_file_name)}.{build_version}" - def _app_name(self): return self.data[JSON_KEY_APP_NAME] @@ -130,18 +128,6 @@ class InfoParser: def _filename_from(self, url): return Path(urlparse(url).path).stem - def _app_version_from(self, file_name): - p = re.compile('(?P\d{4})-(?P\d{1,2})') - m = p.search(file_name) - year = int(m.group('year')) - month = int(m.group('month')) - assert (year > 2000) - assert (month > 0) - assert (month <= 12) - # downgrade the version by 1000 for testing the release - year -= 1000 - return ".".join([str(year), str(month)]) - def _excluded_languages(self): enforced = self._enforced_language() if enforced == None: diff --git a/src/tag_validator.py b/src/tag_validator.py index c5fae37..d225bc0 100644 --- a/src/tag_validator.py +++ b/src/tag_validator.py @@ -4,20 +4,42 @@ import argparse import re import sys from brand import Brand +from version import Version +from info_parser import InfoParser def _is_valid(tag): - # Regex verify the tag format + # Regex verify the tag format: folder_YYYY.MM.buildNr(_extra) pattern = re.compile( - r'^(?P\w+)_(?P\d+)(?:_(?P\w+))?$') + r'^(?P\w+)_(?P\d{4})\.(?P\d{1,2})\.(?P\d+)(?:_(?P\w+))?$' + ) match = pattern.match(tag) if match: groups = match.groupdict() - brand_name = groups.get('brand_folder') - build_nr = int(groups.get('build_nr')) - brand = Brand(brand_name) - print(f"{brand.name} {build_nr}") + brand_name = groups.get('brand_name') + year = int(groups.get('year')) + month = int(groups.get('month')) + build_number = int(groups.get('build_number')) + + try: + brand = Brand(brand_name) + except FileExistsError as e: + _exit_with_error(f"Invalid tag {tag}: {e}") + + try: + version = Version( + year=year, month=month, build_number=build_number + ) + except ValueError as e: + _exit_with_error(f"Invalid tag {tag}: {e}") + + parser = InfoParser(json_path=brand.info_file, build_number=version.build_number) + if parser.version != version: + _exit_with_error(f"Ivalid date in tag: {tag}, does not match year.month of ZIM file in {brand.info_file}, it should be: {parser.version.semantic}") + + print(f"{brand.name} {version.semantic_downgraded}") + else: _exit_with_error(f"Invalid tag: {tag}") return False diff --git a/src/version.py b/src/version.py new file mode 100644 index 0000000..b188208 --- /dev/null +++ b/src/version.py @@ -0,0 +1,31 @@ +import re +from datetime import datetime + + +class Version: + + def __init__(self, year: int, month: int, build_number: int): + if (1 <= month <= 12) == False: + raise ValueError(f"invalid month: {month}") + if (0 <= build_number) == False: + raise ValueError(f"invalid build number: {build_number}") + max_year = datetime.now().year + 5 + if (2000 < year < max_year) == False: + raise ValueError(f"invalid year: {year}") + + self.semantic = f"{year}.{month}.{build_number}" + self.semantic_downgraded = f"{year-1000}.{month}.{build_number}" + self.build_number = build_number + + @classmethod + def from_file_name(self, file_name: str, build_number: int): + p = re.compile('(?P\d{4})-(?P\d{1,2})') + m = p.search(file_name) + return Version(year=int(m.group('year')), + month=int(m.group('month')), + build_number=build_number) + + def __eq__(self, other) -> bool: + if isinstance(other, Version): + return self.semantic == other.semantic + return False diff --git a/tests/info_parser_test.py b/tests/info_parser_test.py index 94330ca..02c0ff7 100644 --- a/tests/info_parser_test.py +++ b/tests/info_parser_test.py @@ -8,7 +8,7 @@ import os class InfoParserTest(unittest.TestCase): def setUp(self): - self.parser = InfoParser(Path()/"tests"/"test.json") + self.parser = InfoParser(Path("tests")/"test.json") def test_json_to_project_yml(self): project = self.parser.as_project_yml() @@ -17,7 +17,7 @@ class InfoParserTest(unittest.TestCase): def test_info_plist_path(self): custom_info = self.parser._info_plist_path() - self.assertEqual(custom_info, Path()/"tests"/"tests.plist") + self.assertEqual(custom_info, Path("tests")/"tests.plist") def test_file_name_from_url(self): url = "https://www.dwds.de/kiwix/f/dwds_de_dictionary_nopic_2023-11-20.zim" @@ -33,23 +33,6 @@ class InfoParserTest(unittest.TestCase): brand_name = self.parser._brandname_from(filepath) self.assertEqual(brand_name, "dwds") - def test_version_from_filename(self): - version = self.parser._app_version_from( - "dwds_de_dictionary_nopic_2023-11-20") - self.assertEqual(version, "1023.11") - - version = self.parser._app_version_from( - "dwds_de_dictionary_nopic_2023-09-20") - self.assertEqual(version, "1023.9") - - version = self.parser._app_version_from( - "dwds_de_dictionary_nopic_2023-01") - self.assertEqual(version, "1023.1") - - version = self.parser._app_version_from( - "dwds_de_dictionary_nopic_2023-12") - self.assertEqual(version, "1023.12") - def test_app_name(self): app_name = self.parser._app_name() self.assertEqual(app_name, "DWDS") @@ -62,15 +45,18 @@ class InfoParserTest(unittest.TestCase): excluded = self.parser._excluded_languages() self.assertIn("**/*.lproj", excluded) - def test_app_version(self): - self.assertEqual(self.parser._app_version(), "1023.12.3") + def test_app_version_with_default_json_build_number(self): + self.assertEqual(self.parser.version.semantic, "2023.12.3") + self.assertEqual(self.parser.version.semantic_downgraded, "1023.12.3") - def test_app_version_using_a_tag(self): - parser = InfoParser(Path()/"tests"/"test.json", build_version=15) - self.assertEqual(parser._app_version(), "1023.12.15") + def test_app_version_using_a_specific_build_number(self): + parser = InfoParser(Path("tests")/"test.json", build_number=15) + self.assertEqual(parser.version.semantic, "2023.12.15") + self.assertEqual(parser.version.semantic_downgraded, "1023.12.15") - parser = InfoParser(Path()/"tests"/"test.json", build_version=33) - self.assertEqual(parser._app_version(), "1023.12.33") + parser = InfoParser(Path("tests")/"test.json", build_number=33) + self.assertEqual(parser.version.semantic, "2023.12.33") + self.assertEqual(parser.version.semantic_downgraded, "1023.12.33") def test_as_plist(self): self.parser.create_plist( diff --git a/tests/test.json b/tests/test.json index 95fe19c..7990e6e 100644 --- a/tests/test.json +++ b/tests/test.json @@ -9,5 +9,5 @@ "settings_show_external_link_option": false, "zim_auth": "DWDS_HTTP_BASIC_ACCESS_AUTHENTICATION", "zim_url": "https://www.dwds.de/kiwix/f/dwds_de_dictionary_nopic_2023-12-15.zim", - "build_version": 3 + "build_number": 3 } diff --git a/tests/version_test.py b/tests/version_test.py new file mode 100644 index 0000000..95a1119 --- /dev/null +++ b/tests/version_test.py @@ -0,0 +1,26 @@ +import unittest +from src.version import Version + + +class VersionTest(unittest.TestCase): + + def test_version_from_filename(self): + version = Version.from_file_name( + "dwds_de_dictionary_nopic_2023-11-20", build_number=10) + self.assertEqual(version.semantic, "2023.11.10") + self.assertEqual(version.semantic_downgraded, "1023.11.10") + + version = Version.from_file_name( + "dwds_de_dictionary_nopic_2023-09-20", build_number=0) + self.assertEqual(version.semantic, "2023.9.0") + self.assertEqual(version.semantic_downgraded, "1023.9.0") + + version = Version.from_file_name( + "dwds_de_dictionary_nopic_2023-01", build_number=7) + self.assertEqual(version.semantic, "2023.1.7") + self.assertEqual(version.semantic_downgraded, "1023.1.7") + + version = Version.from_file_name( + "dwds_de_dictionary_nopic_2023-12", build_number=129) + self.assertEqual(version.semantic, "2023.12.129") + self.assertEqual(version.semantic_downgraded, "1023.12.129")