Merge pull request #21 from kiwix/feature/cd-per-brand-p3

Update tag/version number validation
This commit is contained in:
BPH 2024-01-13 19:57:54 +01:00 committed by GitHub
commit 44809e4631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 123 additions and 73 deletions

View File

@ -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.", "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_name": "DWDS",
"app_store_id": "id6473090365", "app_store_id": "id6473090365",
"build_version": 3, "build_number": 3,
"enforced_lang": "de", "enforced_lang": "de",
"settings_default_external_link_to": "alwaysLoad", "settings_default_external_link_to": "alwaysLoad",
"settings_show_external_link_option": false, "settings_show_external_link_option": false,

View File

@ -1,22 +1,21 @@
from pathlib import Path from pathlib import Path
import sys
INFO_JSON = 'info.json' INFO_JSON = 'info.json'
class Brand: class Brand:
def __init__(self, name): def __init__(self, name):
if Path(name).is_dir() == False: 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 self.info_file = Path(name)/INFO_JSON
if self.info_file.exists() == False: 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 self.name = name
@staticmethod @staticmethod
def all_info_files(): def all_info_files():
return list(Path().rglob(INFO_JSON)) return list(Path().rglob(INFO_JSON))
def _exit_with_error(self, msg):
print(f"Error: {msg}")
sys.exit(1)

View File

@ -7,8 +7,8 @@ INFO_JSON = 'info.json'
class CustomApps: class CustomApps:
def __init__(self, brands=["all"], build_version=None): def __init__(self, brands=["all"], build_number=None):
self.build_version = build_version self.build_number = build_number
if brands == ["all"]: if brands == ["all"]:
self.info_files = Brand.all_info_files() self.info_files = Brand.all_info_files()
else: else:
@ -28,7 +28,7 @@ class CustomApps:
dict = {"include": ["project.yml"]} dict = {"include": ["project.yml"]}
targets = {} targets = {}
for info in self.info_files: 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() targets = targets | parser.as_project_yml()
dict["targets"] = targets dict["targets"] = targets
@ -43,7 +43,7 @@ class CustomApps:
it should be a copy from the Kiwix target it should be a copy from the Kiwix target
""" """
for info in self.info_files: 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) parser.create_plist(based_on_plist_file=custom_plist)
def download_zim_files(self): def download_zim_files(self):
@ -60,7 +60,7 @@ class CustomApps:
array: commands that can be feeded into subprocess.call() array: commands that can be feeded into subprocess.call()
""" """
for info in self.info_files: 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() url = parser.zimurl()
file_path = parser.zim_file_path() file_path = parser.zim_file_path()
auth = parser.download_auth() auth = parser.download_auth()

View File

@ -19,17 +19,17 @@ def main():
) )
parser.add_argument( parser.add_argument(
"build_version", "build_number",
nargs='?', nargs='?',
default=None, 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 type=int
) )
args = parser.parse_args() args = parser.parse_args()
brand = args.brand_name 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 # create the plist files
custom_apps.create_plists(custom_plist=Path("Custom.plist")) custom_apps.create_plists(custom_plist=Path("Custom.plist"))

View File

@ -1,8 +1,8 @@
from urllib.parse import urlparse from urllib.parse import urlparse
import json import json
from pathlib import Path from pathlib import Path
from version import Version
import os import os
import re
import shutil import shutil
import plistlib import plistlib
@ -23,20 +23,22 @@ JSON_TO_PLIST_MAPPING = {
class InfoParser: 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 """Parse a specific info.json file for a brand
Args: Args:
json_path (Path): of the branded info.json file 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.brand_name = self._brandname_from(json_path)
self.build_version = build_version
content = json_path.read_text() content = json_path.read_text()
self.data = json.loads(content) self.data = json.loads(content)
assert (JSON_KEY_ZIM_URL in self.data) assert (JSON_KEY_ZIM_URL in self.data)
self.zim_file_name = self._filename_from( self.zim_file_name = self._filename_from(
self.data[JSON_KEY_ZIM_URL]) 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): def create_plist(self, based_on_plist_file):
with based_on_plist_file.open(mode="rb") as file: with based_on_plist_file.open(mode="rb") as file:
@ -54,7 +56,7 @@ class InfoParser:
dict = { dict = {
"templates": ["ApplicationTemplate"], "templates": ["ApplicationTemplate"],
"settings": {"base": { "settings": {"base": {
"MARKETING_VERSION": self._app_version(), "MARKETING_VERSION": self.version.semantic,
"PRODUCT_BUNDLE_IDENTIFIER": f"org.kiwix.custom.{self.brand_name}", "PRODUCT_BUNDLE_IDENTIFIER": f"org.kiwix.custom.{self.brand_name}",
"INFOPLIST_FILE": f"custom/{self._info_plist_path()}", "INFOPLIST_FILE": f"custom/{self._info_plist_path()}",
"INFOPLIST_KEY_CFBundleDisplayName": self._app_name(), "INFOPLIST_KEY_CFBundleDisplayName": self._app_name(),
@ -104,10 +106,6 @@ class InfoParser:
value = self.data[json_key] value = self.data[json_key]
yield {plistKey: value} 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): def _app_name(self):
return self.data[JSON_KEY_APP_NAME] return self.data[JSON_KEY_APP_NAME]
@ -130,18 +128,6 @@ class InfoParser:
def _filename_from(self, url): def _filename_from(self, url):
return Path(urlparse(url).path).stem return Path(urlparse(url).path).stem
def _app_version_from(self, file_name):
p = re.compile('(?P<year>\d{4})-(?P<month>\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): def _excluded_languages(self):
enforced = self._enforced_language() enforced = self._enforced_language()
if enforced == None: if enforced == None:

View File

@ -4,20 +4,42 @@ import argparse
import re import re
import sys import sys
from brand import Brand from brand import Brand
from version import Version
from info_parser import InfoParser
def _is_valid(tag): def _is_valid(tag):
# Regex verify the tag format # Regex verify the tag format: folder_YYYY.MM.buildNr(_extra)
pattern = re.compile( pattern = re.compile(
r'^(?P<brand_folder>\w+)_(?P<build_nr>\d+)(?:_(?P<extra_tag>\w+))?$') r'^(?P<brand_name>\w+)_(?P<year>\d{4})\.(?P<month>\d{1,2})\.(?P<build_number>\d+)(?:_(?P<extra_tag>\w+))?$'
)
match = pattern.match(tag) match = pattern.match(tag)
if match: if match:
groups = match.groupdict() groups = match.groupdict()
brand_name = groups.get('brand_folder') brand_name = groups.get('brand_name')
build_nr = int(groups.get('build_nr')) year = int(groups.get('year'))
brand = Brand(brand_name) month = int(groups.get('month'))
print(f"{brand.name} {build_nr}") 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: else:
_exit_with_error(f"Invalid tag: {tag}") _exit_with_error(f"Invalid tag: {tag}")
return False return False

31
src/version.py Normal file
View File

@ -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<year>\d{4})-(?P<month>\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

View File

@ -8,7 +8,7 @@ import os
class InfoParserTest(unittest.TestCase): class InfoParserTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.parser = InfoParser(Path()/"tests"/"test.json") self.parser = InfoParser(Path("tests")/"test.json")
def test_json_to_project_yml(self): def test_json_to_project_yml(self):
project = self.parser.as_project_yml() project = self.parser.as_project_yml()
@ -17,7 +17,7 @@ class InfoParserTest(unittest.TestCase):
def test_info_plist_path(self): def test_info_plist_path(self):
custom_info = self.parser._info_plist_path() 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): def test_file_name_from_url(self):
url = "https://www.dwds.de/kiwix/f/dwds_de_dictionary_nopic_2023-11-20.zim" 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) brand_name = self.parser._brandname_from(filepath)
self.assertEqual(brand_name, "dwds") 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): def test_app_name(self):
app_name = self.parser._app_name() app_name = self.parser._app_name()
self.assertEqual(app_name, "DWDS") self.assertEqual(app_name, "DWDS")
@ -62,15 +45,18 @@ class InfoParserTest(unittest.TestCase):
excluded = self.parser._excluded_languages() excluded = self.parser._excluded_languages()
self.assertIn("**/*.lproj", excluded) self.assertIn("**/*.lproj", excluded)
def test_app_version(self): def test_app_version_with_default_json_build_number(self):
self.assertEqual(self.parser._app_version(), "1023.12.3") 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): def test_app_version_using_a_specific_build_number(self):
parser = InfoParser(Path()/"tests"/"test.json", build_version=15) parser = InfoParser(Path("tests")/"test.json", build_number=15)
self.assertEqual(parser._app_version(), "1023.12.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) parser = InfoParser(Path("tests")/"test.json", build_number=33)
self.assertEqual(parser._app_version(), "1023.12.33") self.assertEqual(parser.version.semantic, "2023.12.33")
self.assertEqual(parser.version.semantic_downgraded, "1023.12.33")
def test_as_plist(self): def test_as_plist(self):
self.parser.create_plist( self.parser.create_plist(

View File

@ -9,5 +9,5 @@
"settings_show_external_link_option": false, "settings_show_external_link_option": false,
"zim_auth": "DWDS_HTTP_BASIC_ACCESS_AUTHENTICATION", "zim_auth": "DWDS_HTTP_BASIC_ACCESS_AUTHENTICATION",
"zim_url": "https://www.dwds.de/kiwix/f/dwds_de_dictionary_nopic_2023-12-15.zim", "zim_url": "https://www.dwds.de/kiwix/f/dwds_de_dictionary_nopic_2023-12-15.zim",
"build_version": 3 "build_number": 3
} }

26
tests/version_test.py Normal file
View File

@ -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")