From 7d01f7ef669aa83591aa566f034310d8d1ce596c Mon Sep 17 00:00:00 2001 From: gersona Date: Mon, 14 Oct 2024 17:35:42 +0300 Subject: [PATCH 1/8] GoogleV3 Glossary support implementation --- weblate/machinery/googlev3.py | 112 +++++++++++++++++++++++++- weblate/machinery/tests.py | 143 +++++++++++++++++++++++++++++++++- 2 files changed, 249 insertions(+), 6 deletions(-) diff --git a/weblate/machinery/googlev3.py b/weblate/machinery/googlev3.py index afe2fb9d5de3..b1abff42803e 100644 --- a/weblate/machinery/googlev3.py +++ b/weblate/machinery/googlev3.py @@ -5,13 +5,25 @@ from __future__ import annotations import json +import operator from typing import TYPE_CHECKING from django.utils.functional import cached_property -from google.cloud.translate import TranslationServiceClient +from google.cloud import storage +from google.cloud.translate_v3 import ( + GcsSource, + Glossary, + GlossaryInputConfig, + TranslateTextGlossaryConfig, + TranslationServiceClient, +) from google.oauth2 import service_account -from .base import DownloadTranslations, XMLMachineTranslationMixin +from .base import ( + DownloadTranslations, + GlossaryMachineTranslationMixin, + XMLMachineTranslationMixin, +) from .forms import GoogleV3MachineryForm from .google import GoogleBaseTranslation @@ -19,13 +31,23 @@ from weblate.trans.models import Unit -class GoogleV3Translation(XMLMachineTranslationMixin, GoogleBaseTranslation): +class GoogleV3Translation( + XMLMachineTranslationMixin, GoogleBaseTranslation, GlossaryMachineTranslationMixin +): """Google Translate API v3 machine translation support.""" name = "Google Cloud Translation Advanced" max_score = 90 settings_form = GoogleV3MachineryForm + # estimation, actual limit is 10.4 million (10,485,760) UTF-8 bytes + glossary_count_limit = 1000 + + # Identifier must contain only lowercase letters, digits, or hyphens. + glossary_name_format = ( + "weblate__{project}__{source_language}__{target_language}__{checksum}" + ) + @classmethod def get_identifier(cls) -> str: return "google-translate-api-v3" @@ -44,6 +66,17 @@ def client(self): credentials=credentials, client_options={"api_endpoint": api_endpoint} ) + @cached_property + def storage_client(self): + credentials = service_account.Credentials.from_service_account_info( + json.loads(self.settings["credentials"]) + ) + return storage.Client(credentials=credentials) + + @cached_property + def storage_bucket(self): + return self.storage_client.get_bucket(self.settings["bucket-name"]) + @cached_property def parent(self) -> str: project = self.settings["project"] @@ -72,10 +105,21 @@ def download_translations( "source_language_code": source, "mime_type": "text/html", } + glossary_path: str | None = None + if self.settings.get("bucket-name"): + glossary_path = self.get_glossary_id(source, language, unit) + request["glossary_config"] = TranslateTextGlossaryConfig( + glossary=glossary_path + ) + response = self.client.translate_text(request) + response_translations = ( + response.glossary_translations if glossary_path else response.translations + ) + yield { - "text": response.translations[0].translated_text, + "text": response_translations[0].translated_text, "quality": self.max_score, "service": self.name, "source": text, @@ -95,3 +139,63 @@ def cleanup_text(self, text, unit): replacements[replacement] = "\n" return text.replace("\n", replacement), replacements + + def list_glossaries(self) -> dict[str, str]: + """Return dictionary with the name/id of the glossary as the key and value.""" + return { + glossary.display_name: glossary.display_name + for glossary in self.client.list_glossaries(parent=self.parent) + } + + def create_glossary( + self, source_language: str, target_language: str, name: str, tsv: str + ) -> None: + """ + Create glossary in the service. + + - Uploads the TSV file to gcs bucket + - Creates the glossary in the service + """ + # upload tsv to storage bucket + glossary_bucket_file = self.storage_bucket.blob(f"{name}.tsv") + glossary_bucket_file.upload_from_string( + tsv, content_type="text/tab-separated-values" + ) + # create glossary + gcs_source = GcsSource( + input_uri=f"gs://{self.settings["bucket-name"]}/{name}.tsv" + ) + input_config = GlossaryInputConfig(gcs_source=gcs_source) + + glossary = Glossary( + name=self.get_glossary_resource_path(name), + language_pair=Glossary.LanguageCodePair( + source_language_code=source_language, + target_language_code=target_language, + ), + input_config=input_config, + ) + self.client.create_glossary(parent=self.parent, glossary=glossary) + + def delete_glossary(self, glossary_name: str) -> None: + """Delete the glossary in service and storage bucket.""" + self.client.delete_glossary(name=self.get_glossary_resource_path(glossary_name)) + + # delete tsv from storage bucket + glossary_bucket_file = self.storage_bucket.blob(f"{glossary_name}.tsv") + glossary_bucket_file.delete() + + def delete_oldest_glossary(self) -> None: + """Delete the oldest glossary if any.""" + glossaries = sorted( + self.client.list_glossaries(parent=self.parent), + key=operator.attrgetter("submit_time"), + ) + if glossaries: + self.delete_glossary(glossaries[0].display_name) + + def get_glossary_resource_path(self, glossary_name: str): + """Return the resource path used by the Translation API.""" + return self.client.glossary_path( + self.settings["project"], self.settings["location"], glossary_name + ) diff --git a/weblate/machinery/tests.py b/weblate/machinery/tests.py index e91025598743..3c854778f498 100644 --- a/weblate/machinery/tests.py +++ b/weblate/machinery/tests.py @@ -7,11 +7,12 @@ import json import re from copy import copy +from datetime import UTC, datetime from functools import partial from io import StringIO from typing import TYPE_CHECKING, NoReturn from unittest import SkipTest -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, call, patch import httpx import responses @@ -721,7 +722,7 @@ def mock_empty(self) -> NoReturn: def mock_error(self) -> NoReturn: raise SkipTest("Not tested") - def mock_response(self) -> None: + def mock_languages(self) -> None: # Mock get supported languages patcher = patch.object( TranslationServiceClient, @@ -741,6 +742,9 @@ def mock_response(self) -> None: patcher.start() self.addCleanup(patcher.stop) + def mock_response(self) -> None: + self.mock_languages() + # Mock translate patcher = patch.object( TranslationServiceClient, @@ -787,6 +791,141 @@ def test_replacements(self) -> None: unit.source, machine_translation.uncleanup_text(replacements, replaced) ) + # set glossary_count_limit to 1 to also trigger delete_oldest_glossary + @patch("weblate.glossary.models.get_glossary_tsv", new=lambda _: "foo\tbar") + @patch("weblate.machinery.googlev3.GoogleV3Translation.glossary_count_limit", new=1) + def test_glossary(self) -> None: + self.mock_languages() + self.mock_glossary_responses() + + self.CONFIGURATION["bucket-name"] = "test-bucket" + + with patch( + "weblate.machinery.googlev3.GoogleV3Translation.delete_glossary" + ) as delete_glossary_method: + self.assert_translate( + self.SUPPORTED, self.SOURCE_TRANSLATED, self.EXPECTED_LEN + ) + delete_glossary_method.assert_has_calls( + [ + call("weblate__1__en__cs__a85e314d2f7614eb"), + call("weblate__1__en__it__2d9a814c5f6321a8"), + ] + ) + + def mock_glossary_responses(self) -> None: + """ + Mock the responses for Google Cloud Translate V3 API. + + Patches list_glossaries, create_glossary, delete_glossary, translate_text + and also the storage client. + """ + from google.cloud.translate_v3 import Glossary + from google.oauth2 import service_account + + def _glossary(name: str, submit_time: datetime) -> Glossary: + """Return a mock Glossary object with given name and submit time.""" + return Glossary( + display_name=name, + submit_time=submit_time, + ) + + # Mock list glossaries + list_glossaries_patcher = patch.object( + TranslationServiceClient, + "list_glossaries", + Mock( + side_effect=[ + # with stale glossary and another glossary + [ + _glossary( + "weblate__1__en__cs__a85e314d2f7614eb", + datetime(2024, 9, 1, tzinfo=UTC), + ), + _glossary( + "weblate__1__en__it__2d9a814c5f6321a8", + datetime(2024, 9, 1, tzinfo=UTC), + ), + ], + # the stale glossary has been deleted + [ + _glossary( + "weblate__1__en__it__2d9a814c5f6321a8", + datetime(2024, 9, 1, tzinfo=UTC), + ) + ], + # new glossary + [ + _glossary( + "weblate__1__en__cs__9e250d830c11d70f", + datetime(2024, 10, 1, tzinfo=UTC), + ) + ], + ] + ), + ) + list_glossaries_patcher.start() + self.addCleanup(list_glossaries_patcher.stop) + + # Mock create glossary + create_glossary_patcher = patch.object( + TranslationServiceClient, "create_glossary", Mock() + ) + create_glossary_patcher.start() + self.addCleanup(create_glossary_patcher.stop) + + # Mock delete glossary + delete_glossary_patcher = patch.object( + TranslationServiceClient, "delete_glossary", Mock() + ) + delete_glossary_patcher.start() + self.addCleanup(delete_glossary_patcher.stop) + + # Mock translate with glossary + translate_patcher = patch.object( + TranslationServiceClient, + "translate_text", + Mock( + return_value=TranslateTextResponse( + { + "translations": [{"translated_text": "Ahoj"}], + "glossary_translations": [{"translated_text": "Ahoj"}], + } + ), + ), + ) + translate_patcher.start() + self.addCleanup(translate_patcher.stop) + + get_credentials_patcher = patch.object( + service_account.Credentials, + "from_service_account_info", + return_value=MagicMock(), + ) + get_credentials_patcher.start() + self.addCleanup(get_credentials_patcher.stop) + + class MockBlob(MagicMock): + def upload_from_string(self, *args, **kwargs): + """Mock google.cloud.storage.Blob.upload_from_string.""" + + def delete(self, *args, **kwargs): + """Mock google.cloud.storage.Blob.delete.""" + + class MockBucket(MagicMock): + def blob(self, *args, **kwargs): + """Mock google.cloud.storage.Bucket.blob.""" + return MockBlob() + + class MockStorageClient(MagicMock): + def get_bucket(self, *args, **kwargs): + """google.cloud.storage.Client.get_bucket.""" + return MockBucket() + + patcher = patch("google.cloud.storage.Client", new=MockStorageClient) + patcher.start() + self.addCleanup(patcher.stop) + class TMServerTranslationTest(BaseMachineTranslationTest): MACHINE_CLS = TMServerTranslation From f1ee7bf77ce24c311f1c52809a60f0b03d912444 Mon Sep 17 00:00:00 2001 From: gersona Date: Tue, 15 Oct 2024 10:38:04 +0300 Subject: [PATCH 2/8] handle multiple dependencies for show-extras --- scripts/show-extras | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/show-extras b/scripts/show-extras index 06a63c787b81..b461f8337994 100755 --- a/scripts/show-extras +++ b/scripts/show-extras @@ -26,11 +26,13 @@ with open("pyproject.toml", "rb") as handle: for section, data in toml_dict["project"]["optional-dependencies"].items(): if section in {"all", "ci", "dev", "lint", "mypy", "test"}: continue - dependency = re.split("[;<>=[]", data[0])[0].strip() - print( - f""" - * - ``{section}`` - - `{dependency} `_ - - -""" - ) + for index, dependency in enumerate(data): + dependency = re.split("[;<>=[]", dependency)[0].strip() + section = f"``{section}``" if index == 0 else "" + print( + f""" + * - {section} + - `{dependency} `_ + - + """ + ) From 8e78d934410f681101d66e0e36d90dc7b26587de Mon Sep 17 00:00:00 2001 From: gersona Date: Tue, 15 Oct 2024 13:38:48 +0300 Subject: [PATCH 3/8] Documentation update --- docs/admin/install.rst | 6 +++++- docs/admin/machine.rst | 14 ++++++++++++++ docs/changes.rst | 20 ++++++++++++++++++++ docs/user/glossary.rst | 1 + pyproject.toml | 3 ++- weblate/machinery/forms.py | 10 ++++++++++ weblate/machinery/googlev3.py | 6 +++--- weblate/machinery/tests.py | 2 +- 8 files changed, 56 insertions(+), 6 deletions(-) diff --git a/docs/admin/install.rst b/docs/admin/install.rst index 741ae3a8c973..fd1d78170d8d 100644 --- a/docs/admin/install.rst +++ b/docs/admin/install.rst @@ -230,12 +230,16 @@ Django REST Framework - :ref:`mt-google-translate-api-v3` + * - + - `google-cloud-storage `_ + - Optional glossary support for :ref:`mt-google-translate-api-v3` + + * - ``ldap`` - `django-auth-ldap `_ - :ref:`ldap-auth` - * - ``mercurial`` - `mercurial `_ - :ref:`vcs-mercurial` diff --git a/docs/admin/machine.rst b/docs/admin/machine.rst index 28c1428ac1a1..d28ec51f4274 100644 --- a/docs/admin/machine.rst +++ b/docs/admin/machine.rst @@ -239,6 +239,8 @@ Google Cloud Translation Advanced +-----------------+---------------------------------------+----------------------------------------------------------------------------------------------------------+ | ``location`` | Google Translate location | Choose a Google Cloud Translation region that is used for the Google Cloud project or is closest to you. | +-----------------+---------------------------------------+----------------------------------------------------------------------------------------------------------+ + | ``bucket_name`` | Google Storage Bucket name | Enter the name of the Google Cloud Storage bucket. | + +-----------------+---------------------------------------+----------------------------------------------------------------------------------------------------------+ Machine translation service provided by the Google Cloud services. @@ -256,6 +258,18 @@ In order to use this service, you first need to go through the following steps: .. _Enable the Cloud Translation.: https://cloud.google.com/translate/docs/ .. _Setup Authentication.: https://googleapis.dev/python/google-api-core/latest/auth.html + +Optionally, you can configure the service to use :ref:`glossary` by setting up a Bucket: + +1. `Create a Google Cloud bucket.`_ +2. `Set bucket location to "us-central1".`_ +3. `Grant 'Storage Admin' permission to the Service Account.`_ + +.. _Create a Google Cloud bucket.: https://cloud.google.com/storage/docs/creating-buckets +.. _Set bucket location to "us-central1".: https://cloud.google.com/translate/docs/migrate-to-v3#resources_projects_and_locations +.. _Grant 'Storage Admin' permission to the Service Account.: https://cloud.google.com/translate/docs/access-control + + .. seealso:: `Google translate documentation `_, diff --git a/docs/changes.rst b/docs/changes.rst index 68b057617aa9..36ea358e20cc 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,3 +1,23 @@ +Weblate 5.8.1 +----------- + +Not yet released. + +**New features** + +**Improvements** + +* :ref:`mt-google-translate-api-v3` now supports :ref:`glossary-mt` (optional). + +**Bug fixes** + +**Compatibility** + +**Upgrading** + +**Contributors** + + Weblate 5.8 ----------- diff --git a/docs/user/glossary.rst b/docs/user/glossary.rst index a4494597ce2d..78301f7f9a04 100644 --- a/docs/user/glossary.rst +++ b/docs/user/glossary.rst @@ -129,6 +129,7 @@ Following automatic suggestion services utilize glossaries during the translatio * :ref:`mt-microsoft-translator` * :ref:`mt-modernmt` * :ref:`mt-aws` +* :ref:`mt-google-translate-api-v3` The glossary is processed before exposed to the service: diff --git a/pyproject.toml b/pyproject.toml index c09666bf656b..d2bf6cb91d9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,7 +130,8 @@ gerrit = [ "git-review>=2.4.0,<2.5.0" ] google = [ - "google-cloud-translate>=3.13.0,<4.0" + "google-cloud-translate>=3.13.0,<4.0", + "google-cloud-storage>=2.18.2,<3.0" ] ldap = [ "django-auth-ldap>=4.6.0,<5.1.0" diff --git a/weblate/machinery/forms.py b/weblate/machinery/forms.py index bd0d1d0563e4..5cb7c34e3939 100644 --- a/weblate/machinery/forms.py +++ b/weblate/machinery/forms.py @@ -215,6 +215,16 @@ class GoogleV3MachineryForm(BaseMachineryForm): ) ), ) + bucket_name = forms.CharField( + label=pgettext_lazy( + "Automatic suggestion service configuration", "Google Storage Bucket name" + ), + help_text=pgettext_lazy( + "Google Cloud Translation configuration", + "Enter the name of the Google Cloud Storage bucket.", + ), + required=False, + ) def clean_credentials(self): try: diff --git a/weblate/machinery/googlev3.py b/weblate/machinery/googlev3.py index b1abff42803e..c6cc28b0a1a0 100644 --- a/weblate/machinery/googlev3.py +++ b/weblate/machinery/googlev3.py @@ -75,7 +75,7 @@ def storage_client(self): @cached_property def storage_bucket(self): - return self.storage_client.get_bucket(self.settings["bucket-name"]) + return self.storage_client.get_bucket(self.settings["bucket_name"]) @cached_property def parent(self) -> str: @@ -106,7 +106,7 @@ def download_translations( "mime_type": "text/html", } glossary_path: str | None = None - if self.settings.get("bucket-name"): + if self.settings.get("bucket_name"): glossary_path = self.get_glossary_id(source, language, unit) request["glossary_config"] = TranslateTextGlossaryConfig( glossary=glossary_path @@ -163,7 +163,7 @@ def create_glossary( ) # create glossary gcs_source = GcsSource( - input_uri=f"gs://{self.settings["bucket-name"]}/{name}.tsv" + input_uri=f"gs://{self.settings["bucket_name"]}/{name}.tsv" ) input_config = GlossaryInputConfig(gcs_source=gcs_source) diff --git a/weblate/machinery/tests.py b/weblate/machinery/tests.py index 3c854778f498..aa08f39585ca 100644 --- a/weblate/machinery/tests.py +++ b/weblate/machinery/tests.py @@ -798,7 +798,7 @@ def test_glossary(self) -> None: self.mock_languages() self.mock_glossary_responses() - self.CONFIGURATION["bucket-name"] = "test-bucket" + self.CONFIGURATION["bucket_name"] = "test-bucket" with patch( "weblate.machinery.googlev3.GoogleV3Translation.delete_glossary" From 7db8a7d4799663c747cd770bdd0a6a36a65afb0e Mon Sep 17 00:00:00 2001 From: gersona Date: Tue, 15 Oct 2024 14:14:49 +0300 Subject: [PATCH 4/8] syntax error fix --- weblate/machinery/googlev3.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/weblate/machinery/googlev3.py b/weblate/machinery/googlev3.py index c6cc28b0a1a0..13dd5cc88970 100644 --- a/weblate/machinery/googlev3.py +++ b/weblate/machinery/googlev3.py @@ -162,9 +162,8 @@ def create_glossary( tsv, content_type="text/tab-separated-values" ) # create glossary - gcs_source = GcsSource( - input_uri=f"gs://{self.settings["bucket_name"]}/{name}.tsv" - ) + bucket_name = self.settings["bucket_name"] + gcs_source = GcsSource(input_uri=f"gs://{bucket_name}/{name}.tsv") input_config = GlossaryInputConfig(gcs_source=gcs_source) glossary = Glossary( From e413a3e81198e5102a652c7b29bcfc3bdc1fbddf Mon Sep 17 00:00:00 2001 From: gersona Date: Thu, 17 Oct 2024 08:38:52 +0300 Subject: [PATCH 5/8] fix missing test coverage --- weblate/machinery/tests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/weblate/machinery/tests.py b/weblate/machinery/tests.py index aa08f39585ca..89cb398c26b0 100644 --- a/weblate/machinery/tests.py +++ b/weblate/machinery/tests.py @@ -797,7 +797,15 @@ def test_replacements(self) -> None: def test_glossary(self) -> None: self.mock_languages() self.mock_glossary_responses() + self.CONFIGURATION["bucket_name"] = "test-bucket" + + self.assert_translate(self.SUPPORTED, self.SOURCE_TRANSLATED, self.EXPECTED_LEN) + @patch("weblate.glossary.models.get_glossary_tsv", new=lambda _: "foo\tbar") + @patch("weblate.machinery.googlev3.GoogleV3Translation.glossary_count_limit", new=1) + def test_glossary_with_calls_check(self) -> None: + self.mock_languages() + self.mock_glossary_responses() self.CONFIGURATION["bucket_name"] = "test-bucket" with patch( From 2295605d674d84b7db9c0b8fe9c5f8f2a5e6993c Mon Sep 17 00:00:00 2001 From: gersona Date: Tue, 12 Nov 2024 08:18:49 +0300 Subject: [PATCH 6/8] add bucket usage explanation --- docs/admin/machine.rst | 2 +- weblate/machinery/forms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/admin/machine.rst b/docs/admin/machine.rst index a78e30e72c27..b360255f700a 100644 --- a/docs/admin/machine.rst +++ b/docs/admin/machine.rst @@ -241,7 +241,7 @@ Google Cloud Translation Advanced +-----------------+---------------------------------------+----------------------------------------------------------------------------------------------------------+ | ``location`` | Google Translate location | Choose a Google Cloud Translation region that is used for the Google Cloud project or is closest to you. | +-----------------+---------------------------------------+----------------------------------------------------------------------------------------------------------+ - | ``bucket_name`` | Google Storage Bucket name | Enter the name of the Google Cloud Storage bucket. | + | ``bucket_name`` | Google Storage Bucket name | Enter the name of the Google Cloud Storage bucket that is used to store the Glossary files. | +-----------------+---------------------------------------+----------------------------------------------------------------------------------------------------------+ Machine translation service provided by the Google Cloud services. diff --git a/weblate/machinery/forms.py b/weblate/machinery/forms.py index 5cb7c34e3939..a7027b523311 100644 --- a/weblate/machinery/forms.py +++ b/weblate/machinery/forms.py @@ -221,7 +221,7 @@ class GoogleV3MachineryForm(BaseMachineryForm): ), help_text=pgettext_lazy( "Google Cloud Translation configuration", - "Enter the name of the Google Cloud Storage bucket.", + "Enter the name of the Google Cloud Storage bucket that is used to store the Glossary files.", ), required=False, ) From 9d1e2319187fd5d213e3d4f733c9e2dc6378211e Mon Sep 17 00:00:00 2001 From: gersona Date: Tue, 12 Nov 2024 18:43:20 +0300 Subject: [PATCH 7/8] "bucket_name" added to SettingsDict --- weblate/machinery/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/weblate/machinery/base.py b/weblate/machinery/base.py index 527f961a603a..0db06c9e067f 100644 --- a/weblate/machinery/base.py +++ b/weblate/machinery/base.py @@ -77,6 +77,7 @@ class SettingsDict(TypedDict, total=False): persona: str style: str custom_model: str + bucket_name: str class TranslationResultDict(TypedDict): From 0121fb2fc28c8f95019fff60e27f3f2e6dfb7fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Wed, 13 Nov 2024 10:48:54 +0100 Subject: [PATCH 8/8] chore: lockfile maintenance --- uv.lock | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/uv.lock b/uv.lock index 58627a56e037..11e381082de0 100644 --- a/uv.lock +++ b/uv.lock @@ -1231,6 +1231,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/0f/2e2061e3fbcb9d535d5da3f58cc8de4947df1786fe6a1355960feb05a681/google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61", size = 29233 }, ] +[[package]] +name = "google-cloud-storage" +version = "2.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/b7/1554cdeb55d9626a4b8720746cba8119af35527b12e1780164f9ba0f659a/google_cloud_storage-2.18.2.tar.gz", hash = "sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99", size = 5532864 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/da/95db7bd4f0bd1644378ac1702c565c0210b004754d925a74f526a710c087/google_cloud_storage-2.18.2-py2.py3-none-any.whl", hash = "sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166", size = 130466 }, +] + [[package]] name = "google-cloud-translate" version = "3.17.0" @@ -1248,6 +1265,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/8e/274bf1ff50f7c372c77db5a3ee903acc1cfb2a6a0a15e90592ca9b2b6bf8/google_cloud_translate-3.17.0-py2.py3-none-any.whl", hash = "sha256:de6f6c11afd625c880adeba0da434e0e83863903fada20214f93c1e3dee5e912", size = 183053 }, ] +[[package]] +name = "google-crc32c" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/72/c3298da1a3773102359c5a78f20dae8925f5ea876e37354415f68594a6fb/google_crc32c-1.6.0.tar.gz", hash = "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc", size = 14472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/14/ab47972ac79b6e7b03c8be3a7ef44b530a60e69555668dbbf08fc5692a98/google_crc32c-1.6.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4", size = 30267 }, + { url = "https://files.pythonhosted.org/packages/54/7d/738cb0d25ee55629e7d07da686decf03864a366e5e863091a97b7bd2b8aa/google_crc32c-1.6.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8", size = 30112 }, + { url = "https://files.pythonhosted.org/packages/3e/6d/33ca50cbdeec09c31bb5dac277c90994edee975662a4c890bda7ffac90ef/google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d", size = 32861 }, + { url = "https://files.pythonhosted.org/packages/67/1e/4870896fc81ec77b1b5ebae7fdd680d5a4d40e19a4b6d724032f996ca77a/google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f", size = 32490 }, + { url = "https://files.pythonhosted.org/packages/00/9c/f5f5af3ddaa7a639d915f8f58b09bbb8d1db90ecd0459b62cd430eb9a4b6/google_crc32c-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3", size = 33446 }, + { url = "https://files.pythonhosted.org/packages/cf/41/65a91657d6a8123c6c12f9aac72127b6ac76dda9e2ba1834026a842eb77c/google_crc32c-1.6.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d", size = 30268 }, + { url = "https://files.pythonhosted.org/packages/59/d0/ee743a267c7d5c4bb8bd865f7d4c039505f1c8a4b439df047fdc17be9769/google_crc32c-1.6.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b", size = 30113 }, + { url = "https://files.pythonhosted.org/packages/25/53/e5e449c368dd26ade5fb2bb209e046d4309ed0623be65b13f0ce026cb520/google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00", size = 32995 }, + { url = "https://files.pythonhosted.org/packages/52/12/9bf6042d5b0ac8c25afed562fb78e51b0641474097e4139e858b45de40a5/google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3", size = 32614 }, + { url = "https://files.pythonhosted.org/packages/76/29/fc20f5ec36eac1eea0d0b2de4118c774c5f59c513f2a8630d4db6991f3e0/google_crc32c-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760", size = 33445 }, +] + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251 }, +] + [[package]] name = "googleapis-common-protos" version = "1.65.0" @@ -3513,6 +3560,7 @@ all = [ { name = "django-auth-ldap" }, { name = "django-zxcvbn-password" }, { name = "git-review" }, + { name = "google-cloud-storage" }, { name = "google-cloud-translate" }, { name = "mercurial" }, { name = "openai" }, @@ -3530,6 +3578,7 @@ gerrit = [ { name = "git-review" }, ] google = [ + { name = "google-cloud-storage" }, { name = "google-cloud-translate" }, ] ldap = [ @@ -3668,6 +3717,7 @@ requires-dist = [ { name = "fluent-syntax", specifier = ">=0.18.1,<0.20" }, { name = "git-review", marker = "extra == 'gerrit'", specifier = ">=2.4.0,<2.5.0" }, { name = "gitpython", specifier = ">=3.1.0,<3.2" }, + { name = "google-cloud-storage", marker = "extra == 'google'", specifier = ">=2.18.2,<3.0" }, { name = "google-cloud-translate", marker = "extra == 'google'", specifier = ">=3.13.0,<4.0" }, { name = "gunicorn", marker = "extra == 'wsgi'", specifier = "==23.0.0" }, { name = "hiredis", specifier = ">=2.2.1,<3.1" },