From 7e40c96af8418b9f14549792f78b10536ec6d293 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 17 Feb 2023 12:13:03 +0100 Subject: [PATCH 01/11] More care at handling private projects and JsonApi exceptions not defining 'detail'. --- pytransifex/api.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index caab97a..6836b98 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -62,18 +62,34 @@ def create_project( project_name = project_slug try: - res = tx_api.Project.create( - name=project_name, - slug=project_slug, - source_language=source_language, - private=private, - organization=self.organization, - ) + if private: + return tx_api.Project.create( + name=project_name, + slug=project_slug, + source_language=source_language, + private=private, + organization=self.organization, + ) + else: + if repository_url := kwargs.get("repository_url", None): + return tx_api.Project.create( + name=project_name, + slug=project_slug, + source_language=source_language, + private=private, + repository_url=repository_url, + organization=self.organization, + ) + else: + raise ValueError(f"Private projects need to pass a 'repository_url' (string) argument.") + logging.info("Project created!") - return res + except JsonApiException as error: - if "already exists" in error.detail: # type: ignore + if hasattr(error, "detail") and "already exists" in error.detail: # type: ignore return self.get_project(project_slug=project_slug) + else: + logging.error(f"Unable to create project; API replied with {error}") @ensure_login def get_project(self, project_slug: str) -> None | Resource: From e1e4e7ea6162de04b7f503625ea5a741a78addc9 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 17 Feb 2023 12:13:27 +0100 Subject: [PATCH 02/11] --- --- pytransifex/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index 6836b98..14aa9cb 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -81,7 +81,9 @@ def create_project( organization=self.organization, ) else: - raise ValueError(f"Private projects need to pass a 'repository_url' (string) argument.") + raise ValueError( + f"Private projects need to pass a 'repository_url' (non-empty string) argument." + ) logging.info("Project created!") From ccdead4dd2e543ee80456deb5da477c5ba30817d Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Sat, 18 Feb 2023 10:26:24 +0100 Subject: [PATCH 03/11] Pos params -> Named params. 'list_langages' re-implemented. Cleanup. --- pytransifex/api.py | 67 ++++++++++++++++++++++++------------------- pytransifex/config.py | 15 ++++++++-- tests/test_api.py | 11 +++++-- 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index 14aa9cb..99a92bf 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -18,6 +18,8 @@ class Client(Tx): """ The proper Transifex client expected by the cli and other consumers. By default instances are created and logged in 'lazyly' -- when creation or login cannot be deferred any longer. + Methods with more than 2 parameters don't allow for positional arguments. + '**kwargs' is used in methods that may need to forward extra named arguments to the API. """ def __init__(self, config: ApiConfig, defer_login: bool = False): @@ -48,18 +50,16 @@ def login(self): @ensure_login def create_project( self, + *, project_slug: str, project_name: str | None = None, source_language_code: str = "en_GB", private: bool = False, - *args, # absorbing extra args - **kwargs, # absorbing extra kwargs + **kwargs, ) -> None | Resource: """Create a project.""" source_language = tx_api.Language.get(code=source_language_code) - - if project_name is None: - project_name = project_slug + project_name = project_name or project_slug try: if private: @@ -69,6 +69,7 @@ def create_project( source_language=source_language, private=private, organization=self.organization, + **kwargs, ) else: if repository_url := kwargs.get("repository_url", None): @@ -79,6 +80,7 @@ def create_project( private=private, repository_url=repository_url, organization=self.organization, + **kwargs, ) else: raise ValueError( @@ -113,21 +115,23 @@ def list_resources(self, project_slug: str) -> list[Any]: else: return [] - raise Exception( + raise ValueError( f"Unable to find any project under this organization: '{self.organization}'" ) @ensure_login def create_resource( self, + *, project_slug: str, path_to_file: Path, resource_slug: str | None = None, resource_name: str | None = None, + **kwargs, ): """Create a resource using the given file contents, slugs and names""" if not (resource_slug or resource_name): - raise Exception("Please give either a resource_slug or a resource_name") + raise ValueError("Please give either a resource_slug or a resource_name") if project := self.get_project(project_slug=project_slug): resource = tx_api.Resource.create( @@ -135,6 +139,7 @@ def create_resource( name=resource_name or resource_slug, slug=resource_slug or resource_name, i18n_format=tx_api.I18nFormat(id=self.i18n_type), + **kwargs, ) with open(path_to_file, "r") as fh: @@ -144,20 +149,20 @@ def create_resource( logging.info(f"Resource created: {resource_slug or resource_name}") else: - raise Exception( + raise ValueError( f"Not project could be found wiht the slug '{project_slug}'. Please create a project first." ) @ensure_login def update_source_translation( - self, project_slug: str, resource_slug: str, path_to_file: Path + self, *, project_slug: str, resource_slug: str, path_to_file: Path ): """ Update the translation strings for the given resource using the content of the file passsed as argument """ if not "slug" in self.organization.attributes: - raise Exception( + raise ValueError( "Unable to fetch resource for this organization; define an 'organization slug' first." ) @@ -175,13 +180,14 @@ def update_source_translation( logging.info(f"Source updated for resource: {resource_slug}") return - raise Exception( + raise ValueError( f"Unable to find resource '{resource_slug}' in project '{project_slug}'" ) @ensure_login def get_translation( self, + *, project_slug: str, resource_slug: str, language_code: str, @@ -210,39 +216,43 @@ def get_translation( ) else: - raise Exception( + raise ValueError( f"Unable to find any resource with this slug: '{resource_slug}'" ) else: - raise Exception( + raise ValueError( f"Unable to find any resource for this project: '{project_slug}'" ) else: - raise Exception( + raise ValueError( f"Couldn't find any project with this slug: '{project_slug}'" ) @ensure_login - def list_languages(self, project_slug: str) -> list[Any]: + def list_languages(self, project_slug: str, resource_slug: str) -> list[Any]: """ - List all languages for which there is at least 1 resource registered - under the parameterised project + List languages for which there exist translations under the given resource. """ if self.projects: if project := self.projects.get(slug=project_slug): - languages = project.fetch("languages") - logging.info(f"Obtained these languages") - return languages - raise Exception( + if resource := project.fetch("resources").get(slug=resource_slug): + logging.info(f"Obtained these languages") + # FIXME: Extract a list[str] mapping to resource languages + return resource + raise ValueError( + f"Unable to find any resource with this slug: '{resource_slug}'" + ) + raise ValueError( f"Unable to find any project with this slug: '{project_slug}'" ) - raise Exception( + raise ValueError( f"Unable to find any project under this organization: '{self.organization}'" ) @ensure_login def create_language( self, + *, project_slug: str, language_code: str, coordinators: None | list[Any] = None, @@ -279,11 +289,12 @@ def get_project_stats(self, project_slug: str) -> dict[str, Any]: if resource_stats := tx_api.ResourceLanguageStats(project=project): return resource_stats.to_dict() - raise Exception(f"Unable to find translation for this project {project_slug}") + raise ValueError(f"Unable to find translation for this project {project_slug}") @ensure_login def pull( self, + *, project_slug: str, resource_slugs: list[str], language_codes: list[str], @@ -304,7 +315,7 @@ def pull( @ensure_login def push( - self, project_slug: str, resource_slugs: list[str], path_to_files: list[Path] + self, *, project_slug: str, resource_slugs: list[str], path_to_files: list[Path] ): """Push resources with files under project.""" if len(resource_slugs) != len(path_to_files): @@ -352,7 +363,7 @@ class Transifex: client = None - def __new__(cls, *, defer_login: bool = False, **kwargs): + def __new__(cls, *, defer_login: bool = False, **kwargs) -> None | "Client": if not cls.client: try: if kwargs: @@ -363,11 +374,9 @@ def __new__(cls, *, defer_login: bool = False, **kwargs): ) config = ApiConfig.from_env() - cls.client = Client(config, defer_login) + return Client(config, defer_login) - except Exception as error: + except ValueError as error: available = list(ApiConfig._fields) msg = f"Unable to define a proper config. API initialization uses the following fields, with only 'project_slug' optional: {available}" logging.error(f"{msg}:\n{error}") - - return cls.client diff --git a/pytransifex/config.py b/pytransifex/config.py index 93d8f87..18131ec 100644 --- a/pytransifex/config.py +++ b/pytransifex/config.py @@ -22,9 +22,18 @@ def from_env(cls) -> "ApiConfig": organization = environ.get("ORGANIZATION") i18n_type = environ.get("I18N_TYPE", "PO") - if any(not v for v in [token, organization, i18n_type]): - raise Exception( - "Envars 'TX_TOKEN' and 'ORGANIZATION' must be set to non-empty values. Aborting now." + if faulty := next( + filter( + lambda v: not v[1], + zip( + ["token", "organization", "i18_ntype"], + [token, organization, i18n_type], + ), + ), + None, + ): + raise ValueError( + f"Envars 'TX_TOKEN', 'ORGANIZATION' and 'I18N_TYPE must be set to non-empty values, yet this one was found missing ('None' or empty string): {faulty[0]}" ) return cls(token, organization, i18n_type) # type: ignore diff --git a/tests/test_api.py b/tests/test_api.py index 25efa03..d6ee7f8 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -9,7 +9,10 @@ class TestNewApi(unittest.TestCase): @classmethod def setUpClass(cls): - cls.tx = Transifex(defer_login=True) + client = Transifex(defer_login=True) + assert client + + cls.tx = client cls.project_slug = "test_project_pytransifex" cls.project_name = "Test Project PyTransifex" cls.resource_slug = "test_resource_fr" @@ -65,10 +68,12 @@ def test5_update_source_translation(self): assert True def test6_create_language(self): - self.tx.create_language(self.project_slug, "fr_CH") + self.tx.create_language(project_slug=self.project_slug, language_code="fr_CH") def test7_list_languages(self): - languages = self.tx.list_languages(project_slug=self.project_slug) + languages = self.tx.list_languages( + project_slug=self.project_slug, resource_slug=self.resource_slug + ) assert languages is not None def test8_get_translation(self): From 94f6cc9dc5be7d6febc14a04f3138aacff94b423 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Sat, 18 Feb 2023 11:03:04 +0100 Subject: [PATCH 04/11] Path moved to internals. Edges of program using str instead. --- pytransifex/api.py | 24 +++++++++++------------- pytransifex/cli.py | 10 +++++----- tests/test_api.py | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index 8efd524..c4fb8c4 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -1,5 +1,4 @@ import logging -from os import mkdir from pathlib import Path from typing import Any, Optional @@ -124,7 +123,7 @@ def create_resource( self, *, project_slug: str, - path_to_file: Path, + path_to_file: str, resource_slug: str | None = None, resource_name: str | None = None, **kwargs, @@ -155,7 +154,7 @@ def create_resource( @ensure_login def update_source_translation( - self, *, project_slug: str, resource_slug: str, path_to_file: Path + self, *, project_slug: str, resource_slug: str, path_to_file: str ): """ Update the translation strings for the given resource using the content of the file @@ -191,11 +190,10 @@ def get_translation( project_slug: str, resource_slug: str, language_code: str, - output_dir: Path, + path_to_file: str, ): """Fetch the translation resource matching the given language""" language = tx_api.Language.get(code=language_code) - file_name = Path.joinpath(output_dir, resource_slug) if project := self.get_project(project_slug=project_slug): if resources := project.fetch("resources"): @@ -205,10 +203,8 @@ def get_translation( ) translated_content = requests.get(url).text - if not Path.exists(output_dir): - mkdir(output_dir) - - with open(file_name, "w") as fh: + Path.mkdir(Path(path_to_file), parents=True, exist_ok=True) + with open(path_to_file, "w") as fh: fh.write(translated_content) logging.info( @@ -264,7 +260,9 @@ def create_language( if coordinators: project.add("coordinators", coordinators) - logging.info(f"Created language resource for {language_code}") + logging.info( + f"Created language resource for {language_code} and added these coordinators: {coordinators}" + ) @ensure_login def project_exists(self, project_slug: str) -> bool: @@ -298,7 +296,7 @@ def pull( project_slug: str, resource_slugs: list[str], language_codes: list[str], - output_dir: Path, + output_dir: str, ): """Pull resources from project.""" args = [] @@ -315,12 +313,12 @@ def pull( @ensure_login def push( - self, *, project_slug: str, resource_slugs: list[str], path_to_files: list[Path] + self, *, project_slug: str, resource_slugs: list[str], path_to_files: list[str] ): """Push resources with files under project.""" if len(resource_slugs) != len(path_to_files): raise ValueError( - f"Resources slugs ({len(resource_slugs)}) and Path to files ({len(path_to_files)}) must be equal in size!" + f"Resources slugs ({len(resource_slugs)}) and path to files ({len(path_to_files)}) must be equal in size!" ) resource_zipped_with_path = list(zip(resource_slugs, path_to_files)) diff --git a/pytransifex/cli.py b/pytransifex/cli.py index ca64746..b5ff07d 100644 --- a/pytransifex/cli.py +++ b/pytransifex/cli.py @@ -9,6 +9,7 @@ from pytransifex.config import CliSettings client = Transifex(defer_login=True) +assert client def path_to_slug(file_paths: list[Path]) -> list[str]: @@ -94,7 +95,7 @@ def push(input_directory: str | None): client.push( project_slug=settings.project_slug, resource_slugs=slugs, - path_to_files=files, + path_to_files=[str(f) for f in files], ) except Exception as error: reply += f"cli:push > Failed because of this error: {error}" @@ -107,16 +108,15 @@ def push(input_directory: str | None): @click.option("-l", "--only-lang", default="all") @click.option("-out", "--output-directory", is_flag=False) @cli.command("pull", help="Pull translation strings") -def pull(output_directory: str | Path | None, only_lang: str | None): +def pull(output_directory: str | None, only_lang: str | None): reply = "" settings = CliSettings.from_disk() language_codes = only_lang.split(",") if only_lang else [] if output_directory: - output_directory = Path(output_directory) - settings.output_directory = output_directory + settings.output_directory = Path(output_directory) else: - output_directory = settings.output_directory + output_directory = str(settings.output_directory) resource_slugs = [] try: diff --git a/tests/test_api.py b/tests/test_api.py index d6ee7f8..4aa7765 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -81,7 +81,7 @@ def test8_get_translation(self): project_slug=self.project_slug, resource_slug=self.resource_slug, language_code="fr_CH", - output_dir=self.output_dir, + path_to_file=self.output_dir, ) assert Path.exists(Path.joinpath(self.output_dir, self.resource_slug)) From 1328ff645d9f8a191a0d5a86f4b9ccfacc74e06c Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Sat, 18 Feb 2023 13:27:51 +0100 Subject: [PATCH 05/11] Better existence check for project. More strings in tests. --- pytransifex/api.py | 12 +++++++++--- tests/test_api.py | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index c4fb8c4..db9b77c 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -267,9 +267,15 @@ def create_language( @ensure_login def project_exists(self, project_slug: str) -> bool: """Check if the project exists in the remote Transifex repository""" - if self.projects and self.projects.get(slug=project_slug): - return True - return False + try: + if not self.projects: + return False + elif self.projects.get(slug=project_slug): + return True + else: + return False + except DoesNotExist: + return False @ensure_login def ping(self) -> bool: diff --git a/tests/test_api.py b/tests/test_api.py index 4aa7765..a80bd32 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -55,14 +55,14 @@ def test4_create_resource(self): project_slug=self.project_slug, resource_name=self.resource_name, resource_slug=self.resource_slug, - path_to_file=self.path_to_file, + path_to_file=str(self.path_to_file), ) assert True def test5_update_source_translation(self): self.tx.update_source_translation( project_slug=self.project_slug, - path_to_file=self.path_to_file, + path_to_file=str(self.path_to_file), resource_slug=self.resource_slug, ) assert True @@ -81,7 +81,7 @@ def test8_get_translation(self): project_slug=self.project_slug, resource_slug=self.resource_slug, language_code="fr_CH", - path_to_file=self.output_dir, + path_to_file=str(self.output_dir), ) assert Path.exists(Path.joinpath(self.output_dir, self.resource_slug)) From b05ed9c057630e0de877b603cd9838504bac1a94 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Sun, 19 Feb 2023 14:09:29 +0100 Subject: [PATCH 06/11] Cleaner test config initialization. --- tests/__init__.py | 11 +++++++++++ .../resources}/test_resource_fr.po | 0 tests/data/test_config.toml | 5 +++++ tests/test_api.py | 18 +++++++++++------- tests/test_cli.py | 19 ++++++++++++------- 5 files changed, 39 insertions(+), 14 deletions(-) rename tests/{input => data/resources}/test_resource_fr.po (100%) create mode 100644 tests/data/test_config.toml diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..c967a96 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,11 @@ +import logging +from pathlib import Path + +import toml + +logging.basicConfig(level=logging.INFO) + +p = Path.cwd().joinpath("./tests/data/test_config.toml") +test_config = toml.load(p) + +logging.info(f"Running tests with this test_config: {test_config}") diff --git a/tests/input/test_resource_fr.po b/tests/data/resources/test_resource_fr.po similarity index 100% rename from tests/input/test_resource_fr.po rename to tests/data/resources/test_resource_fr.po diff --git a/tests/data/test_config.toml b/tests/data/test_config.toml new file mode 100644 index 0000000..56f6796 --- /dev/null +++ b/tests/data/test_config.toml @@ -0,0 +1,5 @@ +organization_slug = "test_pytransifex" +project_slug = "test_project_pytransifex" +project_name = "Python Transifex API testing" +resource_slug = "test_resource_fr" +resource_name = "Test Resource FR" diff --git a/tests/test_api.py b/tests/test_api.py index a80bd32..c0d3bea 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,6 +4,7 @@ from pytransifex.api import Transifex from pytransifex.interfaces import Tx +from tests import test_config class TestNewApi(unittest.TestCase): @@ -12,14 +13,16 @@ def setUpClass(cls): client = Transifex(defer_login=True) assert client - cls.tx = client - cls.project_slug = "test_project_pytransifex" - cls.project_name = "Test Project PyTransifex" - cls.resource_slug = "test_resource_fr" - cls.resource_name = "Test Resource FR" - cls.path_to_file = Path.cwd().joinpath("tests", "input", "test_resource_fr.po") + cls.path_to_input_dir = Path.cwd().joinpath("tests", "data", "resources") + cls.path_to_file = cls.path_to_input_dir.joinpath("test_resource_fr.po") cls.output_dir = Path.cwd().joinpath("tests", "output") + cls.tx = client + cls.project_slug = test_config["project_slug"] + cls.project_name = test_config["project_name"] + cls.resource_slug = test_config["resource_slug"] + cls.resource_name = test_config["resource_name"] + if missing := next(filter(lambda p: not p.exists(), [cls.path_to_file]), None): raise ValueError( f"Unable to complete test with broken tests inputs. Found missing: {missing}" @@ -77,11 +80,12 @@ def test7_list_languages(self): assert languages is not None def test8_get_translation(self): + path_to_file = self.output_dir.joinpath(f"{self.resource_slug}.po") self.tx.get_translation( project_slug=self.project_slug, resource_slug=self.resource_slug, language_code="fr_CH", - path_to_file=str(self.output_dir), + path_to_file=str(path_to_file), ) assert Path.exists(Path.joinpath(self.output_dir, self.resource_slug)) diff --git a/tests/test_cli.py b/tests/test_cli.py index 3b8af21..5e40646 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,20 +6,25 @@ from pytransifex.api import Transifex from pytransifex.cli import cli +from tests import test_config class TestCli(unittest.TestCase): @classmethod def setUpClass(cls): - cls.tx = Transifex(defer_login=True) - cls.project_slug = "test_project_pytransifex" - cls.project_name = "Test Project PyTransifex" - cls.resource_slug = "test_resource_fr" - cls.resource_name = "Test Resource FR" - cls.path_to_input_dir = Path.cwd().joinpath("tests", "input") + client = Transifex(defer_login=True) + assert client + + cls.path_to_input_dir = Path.cwd().joinpath("tests", "data", "resources") cls.path_to_file = cls.path_to_input_dir.joinpath("test_resource_fr.po") cls.output_dir = Path.cwd().joinpath("tests", "output") + cls.tx = client + cls.project_slug = test_config["project_slug"] + cls.project_name = test_config["project_name"] + cls.resource_slug = test_config["resource_slug"] + cls.resource_name = test_config["resource_name"] + if missing := next( filter(lambda p: not p.exists(), [cls.path_to_file, cls.path_to_input_dir]), None, @@ -53,7 +58,7 @@ def test1_init(self): assert passed def test2_push(self): - result = self.runner.invoke(cli, ["push", "-in", str(self.path_to_input_dir)]) + result = self.runner.invoke(cli, ["push", "-in", str(self.path_to_file)]) passed = result.exit_code == 0 print(result.output) assert passed From ee794907f269f58e89be23cd8d547956fd77ba69 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Sun, 19 Feb 2023 15:04:10 +0100 Subject: [PATCH 07/11] Beaking backward compat. with 'get_translation' now requiring 'path_to_OUTPUT_file'. --- pytransifex/api.py | 16 +++++++++++----- pytransifex/cli.py | 2 +- tests/test_api.py | 7 +++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index db9b77c..b8127c3 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -190,8 +190,8 @@ def get_translation( project_slug: str, resource_slug: str, language_code: str, - path_to_file: str, - ): + path_to_output_dir: str, + ) -> str: """Fetch the translation resource matching the given language""" language = tx_api.Language.get(code=language_code) @@ -203,13 +203,19 @@ def get_translation( ) translated_content = requests.get(url).text - Path.mkdir(Path(path_to_file), parents=True, exist_ok=True) + path_to_output_dir_as_path = Path(path_to_output_dir) + Path.mkdir(path_to_output_dir_as_path, parents=True, exist_ok=True) + path_to_file = path_to_output_dir_as_path.joinpath( + f"{resource_slug}_{language_code}" + ) + with open(path_to_file, "w") as fh: fh.write(translated_content) logging.info( f"Translations downloaded and written to file (resource: {resource_slug})" ) + return str(path_to_file) else: raise ValueError( @@ -302,13 +308,13 @@ def pull( project_slug: str, resource_slugs: list[str], language_codes: list[str], - output_dir: str, + path_to_output_dir: str, ): """Pull resources from project.""" args = [] for l_code in language_codes: for slug in resource_slugs: - args.append(tuple([project_slug, slug, l_code, output_dir])) + args.append(tuple([project_slug, slug, l_code, path_to_output_dir])) res = concurrently( fn=self.get_translation, diff --git a/pytransifex/cli.py b/pytransifex/cli.py index b5ff07d..1900d77 100644 --- a/pytransifex/cli.py +++ b/pytransifex/cli.py @@ -127,7 +127,7 @@ def pull(output_directory: str | None, only_lang: str | None): project_slug=settings.project_slug, resource_slugs=resource_slugs, language_codes=language_codes, - output_dir=output_directory, + path_to_output_dir=output_directory, ) except Exception as error: reply += f"cli:pull > failed because of this error: {error}" diff --git a/tests/test_api.py b/tests/test_api.py index c0d3bea..4c89233 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -80,14 +80,13 @@ def test7_list_languages(self): assert languages is not None def test8_get_translation(self): - path_to_file = self.output_dir.joinpath(f"{self.resource_slug}.po") - self.tx.get_translation( + path_to_ouput_file = self.tx.get_translation( project_slug=self.project_slug, resource_slug=self.resource_slug, language_code="fr_CH", - path_to_file=str(path_to_file), + path_to_output_dir=str(self.output_dir), ) - assert Path.exists(Path.joinpath(self.output_dir, self.resource_slug)) + assert Path.exists(Path(path_to_ouput_file)) def test9_project_exists(self): verdict = self.tx.project_exists(project_slug=self.project_slug) From 9e4b44a4c0ff49bbb95fe1145c7ba8d84981d473 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Sun, 19 Feb 2023 16:01:11 +0100 Subject: [PATCH 08/11] Extra warning for get_translation testdata vs. output non-equivalence --- pytransifex/api.py | 6 ++---- tests/test_api.py | 26 ++++++++++++++++++++------ tests/test_cli.py | 10 +++++----- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index b8127c3..aaecc62 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -194,6 +194,8 @@ def get_translation( ) -> str: """Fetch the translation resource matching the given language""" language = tx_api.Language.get(code=language_code) + path_to_output_dir_as_path = Path(path_to_output_dir) + Path.mkdir(path_to_output_dir_as_path, parents=True, exist_ok=True) if project := self.get_project(project_slug=project_slug): if resources := project.fetch("resources"): @@ -202,13 +204,9 @@ def get_translation( resource=resource, language=language ) translated_content = requests.get(url).text - - path_to_output_dir_as_path = Path(path_to_output_dir) - Path.mkdir(path_to_output_dir_as_path, parents=True, exist_ok=True) path_to_file = path_to_output_dir_as_path.joinpath( f"{resource_slug}_{language_code}" ) - with open(path_to_file, "w") as fh: fh.write(translated_content) diff --git a/tests/test_api.py b/tests/test_api.py index 4c89233..3d3de04 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,7 +4,7 @@ from pytransifex.api import Transifex from pytransifex.interfaces import Tx -from tests import test_config +from tests import logging, test_config class TestNewApi(unittest.TestCase): @@ -29,10 +29,10 @@ def setUpClass(cls): ) if project := cls.tx.get_project(project_slug=cls.project_slug): - print("Found old project, removing.") + logging.info("Found old project, removing.") project.delete() - print("Creating a brand new project") + logging.info("Creating a brand new project") cls.tx.create_project( project_name=cls.project_name, project_slug=cls.project_slug, private=True ) @@ -80,13 +80,27 @@ def test7_list_languages(self): assert languages is not None def test8_get_translation(self): - path_to_ouput_file = self.tx.get_translation( + path_to_output_file = self.tx.get_translation( project_slug=self.project_slug, resource_slug=self.resource_slug, language_code="fr_CH", path_to_output_dir=str(self.output_dir), ) - assert Path.exists(Path(path_to_ouput_file)) + assert Path.exists(Path(path_to_output_file)) + + from difflib import Differ + from filecmp import cmp + from sys import stdout + + diff = Differ() + if not cmp(path_to_output_file, self.path_to_file): + with open(path_to_output_file, "r") as fh: + f1 = fh.readlines() + with open(path_to_output_file, "r") as fh: + f2 = fh.readlines() + res = list(diff.compare(f1, f2)) + logging.warning(f"Notice that the two files were found to differ:") + stdout.writelines(res) def test9_project_exists(self): verdict = self.tx.project_exists(project_slug=self.project_slug) @@ -98,7 +112,7 @@ def test10_ping(self): def test11_stats(self): stats = self.tx.get_project_stats(project_slug=self.project_slug) - print(str(stats)) + logging.info(str(stats)) assert stats def test12_stats(self): diff --git a/tests/test_cli.py b/tests/test_cli.py index 5e40646..0dc5d4b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,7 @@ from pytransifex.api import Transifex from pytransifex.cli import cli -from tests import test_config +from tests import logging, test_config class TestCli(unittest.TestCase): @@ -34,10 +34,10 @@ def setUpClass(cls): ) if project := cls.tx.get_project(project_slug=cls.project_slug): - print("Found old project, removing.") + logging.info("Found old project, removing.") project.delete() - print("Creating a brand new project") + logging.info("Creating a brand new project") cls.tx.create_project( project_name=cls.project_name, project_slug=cls.project_slug, private=True ) @@ -60,13 +60,13 @@ def test1_init(self): def test2_push(self): result = self.runner.invoke(cli, ["push", "-in", str(self.path_to_file)]) passed = result.exit_code == 0 - print(result.output) + logging.info(result.output) assert passed def test3_pull(self): result = self.runner.invoke(cli, ["pull", "-l", "fr_CH,en_GB"]) passed = result.exit_code == 0 - print(result.output) + logging.info(result.output) assert passed From 7a906c6a447f8cc43a19bd532086cc78b85f24d6 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 20 Feb 2023 09:44:02 +0100 Subject: [PATCH 09/11] Reverting 2 functions called concurrently to again use pos arguments --- pytransifex/api.py | 28 ++++++++++++++++++---------- tests/test_api.py | 17 ++++++++++------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index aaecc62..11546a2 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -154,7 +154,7 @@ def create_resource( @ensure_login def update_source_translation( - self, *, project_slug: str, resource_slug: str, path_to_file: str + self, project_slug: str, resource_slug: str, path_to_file: str ): """ Update the translation strings for the given resource using the content of the file @@ -186,16 +186,27 @@ def update_source_translation( @ensure_login def get_translation( self, - *, project_slug: str, resource_slug: str, language_code: str, - path_to_output_dir: str, + path_to_output_file: None | str = None, + path_to_output_dir: None | str = None, ) -> str: """Fetch the translation resource matching the given language""" + if path_to_output_dir and not path_to_output_file: + path_to_parent = Path(path_to_output_dir) + path_to_output_file = str( + path_to_parent.joinpath(f"{resource_slug}_{language_code}") + ) + elif path_to_output_file and not path_to_output_dir: + path_to_parent = Path(path_to_output_file).parent + else: + raise ValueError( + f"get_translation needs exactly one between 'path_to_output_file' (str) or 'path_to_output_dir (str)'. " + ) + + Path.mkdir(path_to_parent, parents=True, exist_ok=True) language = tx_api.Language.get(code=language_code) - path_to_output_dir_as_path = Path(path_to_output_dir) - Path.mkdir(path_to_output_dir_as_path, parents=True, exist_ok=True) if project := self.get_project(project_slug=project_slug): if resources := project.fetch("resources"): @@ -204,16 +215,13 @@ def get_translation( resource=resource, language=language ) translated_content = requests.get(url).text - path_to_file = path_to_output_dir_as_path.joinpath( - f"{resource_slug}_{language_code}" - ) - with open(path_to_file, "w") as fh: + with open(path_to_output_file, "w") as fh: fh.write(translated_content) logging.info( f"Translations downloaded and written to file (resource: {resource_slug})" ) - return str(path_to_file) + return str(path_to_output_file) else: raise ValueError( diff --git a/tests/test_api.py b/tests/test_api.py index 3d3de04..fd23740 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -16,6 +16,7 @@ def setUpClass(cls): cls.path_to_input_dir = Path.cwd().joinpath("tests", "data", "resources") cls.path_to_file = cls.path_to_input_dir.joinpath("test_resource_fr.po") cls.output_dir = Path.cwd().joinpath("tests", "output") + cls.output_file = cls.output_dir.joinpath("test_resource_fr_DOWNLOADED.po") cls.tx = client cls.project_slug = test_config["project_slug"] @@ -49,11 +50,7 @@ def test2_create_project(self): # Done in setUpClass pass - def test3_list_resources(self): - _ = self.tx.list_resources(project_slug=self.project_slug) - assert True - - def test4_create_resource(self): + def test3_create_resource(self): self.tx.create_resource( project_slug=self.project_slug, resource_name=self.resource_name, @@ -62,6 +59,11 @@ def test4_create_resource(self): ) assert True + def test4_list_resources(self): + resources = self.tx.list_resources(project_slug=self.project_slug) + logging.info(f"Resources found: {resources}") + assert resources + def test5_update_source_translation(self): self.tx.update_source_translation( project_slug=self.project_slug, @@ -77,14 +79,15 @@ def test7_list_languages(self): languages = self.tx.list_languages( project_slug=self.project_slug, resource_slug=self.resource_slug ) - assert languages is not None + logging.info(f"Languages found: {languages}") + assert languages def test8_get_translation(self): path_to_output_file = self.tx.get_translation( project_slug=self.project_slug, resource_slug=self.resource_slug, language_code="fr_CH", - path_to_output_dir=str(self.output_dir), + path_to_output_file=str(self.output_file), ) assert Path.exists(Path(path_to_output_file)) From a4dff86a52628afb171d6753e763bbfb37280404 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 20 Feb 2023 10:14:13 +0100 Subject: [PATCH 10/11] list_languages cleanup --- pytransifex/api.py | 26 ++++++++++++++++++++------ tests/test_api.py | 2 +- tests/test_cli.py | 2 +- tests/test_utils.py | 4 ++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pytransifex/api.py b/pytransifex/api.py index 11546a2..86b2b02 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -21,7 +21,7 @@ class Client(Tx): '**kwargs' is used in methods that may need to forward extra named arguments to the API. """ - def __init__(self, config: ApiConfig, defer_login: bool = False): + def __init__(self, config: ApiConfig, defer_login=False, reset=False): """Extract config values, consumes API token against SDK client""" self.api_token = config.api_token self.host = config.host_name @@ -237,16 +237,30 @@ def get_translation( ) @ensure_login - def list_languages(self, project_slug: str, resource_slug: str) -> list[Any]: + def list_languages(self, project_slug: str, resource_slug: str) -> list[str]: """ List languages for which there exist translations under the given resource. """ if self.projects: if project := self.projects.get(slug=project_slug): if resource := project.fetch("resources").get(slug=resource_slug): - logging.info(f"Obtained these languages") - # FIXME: Extract a list[str] mapping to resource languages - return resource + it = tx_api.ResourceLanguageStats.filter( + project=project, resource=resource + ).all() + + language_codes = [] + for tr in it: + """ + FIXME + This is hideous and probably unsound for some language_codes. + Couldn't find a more direct accessor to language codes. + """ + code = str(tr).rsplit("_", 1)[-1][:-1] + language_codes.append(code) + + logging.info(f"Obtained these languages: {language_codes}") + return language_codes + raise ValueError( f"Unable to find any resource with this slug: '{resource_slug}'" ) @@ -263,7 +277,7 @@ def create_language( *, project_slug: str, language_code: str, - coordinators: None | list[Any] = None, + coordinators: None | list[str] = None, ): """Create a new language resource in the remote Transifex repository""" if project := self.get_project(project_slug=project_slug): diff --git a/tests/test_api.py b/tests/test_api.py index fd23740..33f073d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -107,7 +107,7 @@ def test8_get_translation(self): def test9_project_exists(self): verdict = self.tx.project_exists(project_slug=self.project_slug) - assert verdict is not None + assert verdict def test10_ping(self): self.tx.ping() diff --git a/tests/test_cli.py b/tests/test_cli.py index 0dc5d4b..c9dd45e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -58,7 +58,7 @@ def test1_init(self): assert passed def test2_push(self): - result = self.runner.invoke(cli, ["push", "-in", str(self.path_to_file)]) + result = self.runner.invoke(cli, ["push", "-in", str(self.path_to_input_dir)]) passed = result.exit_code == 0 logging.info(result.output) assert passed diff --git a/tests/test_utils.py b/tests/test_utils.py index b5d4b35..b9edcb5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -26,3 +26,7 @@ def test1_map_async(self): def test2_map_async(self): res = concurrently(fn=fn, args=self.args) assert res == self.res + + +if __name__ == "__main__": + unittest.main() From 721eef0d0f22e04b21dc1bb834a1c6d53b4e2fbf Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 20 Feb 2023 11:41:05 +0100 Subject: [PATCH 11/11] Added test for public transifex repositories --- pytransifex/api.py | 36 ++++++------------- tests/__init__.py | 11 ++++-- tests/data/test_config_public.toml | 6 ++++ tests/test_api.py | 10 +++--- tests/test_cli.py | 10 +++--- tests/test_public_project.py | 57 ++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 39 deletions(-) create mode 100644 tests/data/test_config_public.toml create mode 100644 tests/test_public_project.py diff --git a/pytransifex/api.py b/pytransifex/api.py index 86b2b02..05557a9 100644 --- a/pytransifex/api.py +++ b/pytransifex/api.py @@ -61,32 +61,16 @@ def create_project( project_name = project_name or project_slug try: - if private: - return tx_api.Project.create( - name=project_name, - slug=project_slug, - source_language=source_language, - private=private, - organization=self.organization, - **kwargs, - ) - else: - if repository_url := kwargs.get("repository_url", None): - return tx_api.Project.create( - name=project_name, - slug=project_slug, - source_language=source_language, - private=private, - repository_url=repository_url, - organization=self.organization, - **kwargs, - ) - else: - raise ValueError( - f"Private projects need to pass a 'repository_url' (non-empty string) argument." - ) - - logging.info("Project created!") + proj = tx_api.Project.create( + name=project_name, + slug=project_slug, + source_language=source_language, + private=private, + organization=self.organization, + **kwargs, + ) + logging.info(f"Project created with name '{project_name}' !") + return proj except JsonApiException as error: if hasattr(error, "detail") and "already exists" in error.detail: # type: ignore diff --git a/tests/__init__.py b/tests/__init__.py index c967a96..44bf1b2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,7 +5,12 @@ logging.basicConfig(level=logging.INFO) -p = Path.cwd().joinpath("./tests/data/test_config.toml") -test_config = toml.load(p) +private = Path.cwd().joinpath("./tests/data/test_config.toml") +test_config = toml.load(private) -logging.info(f"Running tests with this test_config: {test_config}") +public = Path.cwd().joinpath("./tests/data/test_config_public.toml") +test_config_public = toml.load(public) + +logging.info( + f"Running tests with this test_config: {test_config} and test_config_public: {test_config_public}" +) diff --git a/tests/data/test_config_public.toml b/tests/data/test_config_public.toml new file mode 100644 index 0000000..b3d5fd7 --- /dev/null +++ b/tests/data/test_config_public.toml @@ -0,0 +1,6 @@ +organization_slug = "test_pytransifex" +project_slug = "test_project_pytransifex_public" +project_name = "Python Transifex API testing (public)" +repository_url = "https://github.com/opengisch/pytransifex" +resource_slug = "test_resource_fr" +resource_name = "Test Resource FR" diff --git a/tests/test_api.py b/tests/test_api.py index 33f073d..ce9a24e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,7 +4,7 @@ from pytransifex.api import Transifex from pytransifex.interfaces import Tx -from tests import logging, test_config +from tests import logging, test_config_public class TestNewApi(unittest.TestCase): @@ -19,10 +19,10 @@ def setUpClass(cls): cls.output_file = cls.output_dir.joinpath("test_resource_fr_DOWNLOADED.po") cls.tx = client - cls.project_slug = test_config["project_slug"] - cls.project_name = test_config["project_name"] - cls.resource_slug = test_config["resource_slug"] - cls.resource_name = test_config["resource_name"] + cls.project_slug = test_config_public["project_slug"] + cls.project_name = test_config_public["project_name"] + cls.resource_slug = test_config_public["resource_slug"] + cls.resource_name = test_config_public["resource_name"] if missing := next(filter(lambda p: not p.exists(), [cls.path_to_file]), None): raise ValueError( diff --git a/tests/test_cli.py b/tests/test_cli.py index c9dd45e..b0a0498 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,7 @@ from pytransifex.api import Transifex from pytransifex.cli import cli -from tests import logging, test_config +from tests import logging, test_config_public class TestCli(unittest.TestCase): @@ -20,10 +20,10 @@ def setUpClass(cls): cls.output_dir = Path.cwd().joinpath("tests", "output") cls.tx = client - cls.project_slug = test_config["project_slug"] - cls.project_name = test_config["project_name"] - cls.resource_slug = test_config["resource_slug"] - cls.resource_name = test_config["resource_name"] + cls.project_slug = test_config_public["project_slug"] + cls.project_name = test_config_public["project_name"] + cls.resource_slug = test_config_public["resource_slug"] + cls.resource_name = test_config_public["resource_name"] if missing := next( filter(lambda p: not p.exists(), [cls.path_to_file, cls.path_to_input_dir]), diff --git a/tests/test_public_project.py b/tests/test_public_project.py new file mode 100644 index 0000000..00ec364 --- /dev/null +++ b/tests/test_public_project.py @@ -0,0 +1,57 @@ +import unittest +from os import remove +from pathlib import Path + +from pytransifex.api import Transifex +from tests import logging, test_config_public + + +class TestCli(unittest.TestCase): + @classmethod + def setUpClass(cls): + client = Transifex(defer_login=True) + assert client + + cls.path_to_input_dir = Path.cwd().joinpath("tests", "data", "resources") + cls.path_to_file = cls.path_to_input_dir.joinpath("test_resource_fr.po") + cls.output_dir = Path.cwd().joinpath("tests", "output") + + cls.tx = client + cls.project_slug = test_config_public["project_slug"] + cls.project_name = test_config_public["project_name"] + cls.resource_slug = test_config_public["resource_slug"] + cls.resource_name = test_config_public["resource_name"] + repository_url = test_config_public["repository_url"] + + if missing := next( + filter(lambda p: not p.exists(), [cls.path_to_file, cls.path_to_input_dir]), + None, + ): + raise ValueError( + f"Unable to complete test with broken tests inputs. Found missing: {missing}" + ) + + if project := cls.tx.get_project(project_slug=cls.project_slug): + logging.info("Found old project, removing.") + project.delete() + + logging.info("Creating a brand new project") + cls.tx.create_project( + project_name=cls.project_name, + project_slug=cls.project_slug, + private=False, + repository_url=repository_url, + ) + + @classmethod + def tearDownClass(cls): + if Path.exists(cls.output_dir): + remove(cls.output_dir) + + def test1_project_exists(self): + verdict = self.tx.project_exists(project_slug=self.project_slug) + assert verdict + + +if __name__ == "__main__": + unittest.main()