From 468cb140d7119f9cc3c8c9effca8016eec9e6cde Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Wed, 20 Nov 2024 21:26:15 +0900 Subject: [PATCH 01/16] refactor: migrate to pyproject, use ruff linter --- .zed/settings.json | 30 ++++ docs/conf.py | 163 +++++++++---------- example/translate_word_doc.py | 18 ++- googletrans/__init__.py | 5 +- googletrans/client.py | 150 +++++++++++------- googletrans/constants.py | 289 +++++++++++++++++++++++++--------- googletrans/gtoken.py | 72 +++++---- googletrans/models.py | 20 ++- googletrans/urls.py | 5 +- googletrans/utils.py | 38 ++--- pyproject.toml | 40 +++++ requirements.txt | 28 ++++ setup.py | 73 --------- tests/conftest.py | 3 +- tests/test_client.py | 110 ++++++------- tests/test_gtoken.py | 11 +- tests/test_utils.py | 27 ++-- 17 files changed, 653 insertions(+), 429 deletions(-) create mode 100644 .zed/settings.json create mode 100644 pyproject.toml create mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..66f9b1d --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,30 @@ +{ + "languages": { + "Python": { + "language_servers": ["pyright", "ruff"], + "format_on_save": "on", + "formatter": [ + { + "code_actions": { + "source.fixAll.ruff": true, + "source.organizeImports.ruff": true + } + }, + { + "language_server": { + "name": "ruff" + } + } + ] + } + }, + "lsp": { + "ruff": { + "initialization_options": { + "settings": { + "path": "./pyproject.toml" + } + } + } + } +} diff --git a/docs/conf.py b/docs/conf.py index ebf14e9..5ba9bdc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,49 +15,47 @@ import datetime import sys -import os -import shlex -sys.path.append('..') +sys.path.append("..") import googletrans # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", # 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Googletrans' -copyright = str(datetime.date.today().year) + ', SuHun Han (ssut)' -author = 'SuHun Han (ssut)' +project = "Googletrans" +copyright = str(datetime.date.today().year) + ", SuHun Han (ssut)" +author = "SuHun Han (ssut)" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -77,73 +75,73 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build', '_themes'] +exclude_patterns = ["_build", "_themes"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False -suppress_warnings = ['image.nonlocal_uri'] +suppress_warnings = ["image.nonlocal_uri"] # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'solar' +html_theme = "solar" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -153,124 +151,123 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. html_use_index = False # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'Googletransdoc' +htmlhelp_basename = "Googletransdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'Googletrans.tex', 'Googletrans Documentation', - 'SuHun Han (ssut)', 'manual'), + ( + master_doc, + "Googletrans.tex", + "Googletrans Documentation", + "SuHun Han (ssut)", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'Googletrans', 'Googletrans Documentation', - [author], 1) -] +man_pages = [(master_doc, "Googletrans", "Googletrans Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {"http://docs.python.org/": None} # -- Options for Texinfo output ------------------------------------------- @@ -279,21 +276,27 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'Googletrans', 'Googletrans Documentation', - author, 'Googletrans', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "Googletrans", + "Googletrans Documentation", + author, + "Googletrans", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False -autodoc_member_order = 'bysource' \ No newline at end of file +autodoc_member_order = "bysource" diff --git a/example/translate_word_doc.py b/example/translate_word_doc.py index d5c74f8..7d3669b 100644 --- a/example/translate_word_doc.py +++ b/example/translate_word_doc.py @@ -2,29 +2,33 @@ from googletrans import Translator -def translate_doc(filename, destination='zh-CN', mix=True): +def translate_doc(filename, destination="zh-CN", mix=True): """ translate a word document type of file and save the result as document and keep the exactly same file format. :param filename: word doc file :param destination='zh-CN': :param mix=True: if True, will have original language and target language into the same doc. paragraphs by paragraphs. """ - def tx(t): return Translator().translate(t, dest=destination).text + + def tx(t): + return Translator().translate(t, dest=destination).text + doc = Document(filename) for p in doc.paragraphs: txd = tx(p.text) - p.text = p.text + ('\n' + txd if mix else '') + p.text = p.text + ("\n" + txd if mix else "") for table in doc.tables: for row in table.rows: for cell in row.cells: txd = tx(cell.text) - p.text = cell.text + ('\n' + txd if mix else '') + p.text = cell.text + ("\n" + txd if mix else "") - f = filename.replace('.doc', destination.lower() + '.doc') + f = filename.replace(".doc", destination.lower() + ".doc") doc.save(f) -if __name__ == '__main__': - filename = 'p1.docx' + +if __name__ == "__main__": + filename = "p1.docx" translate_doc(filename) diff --git a/googletrans/__init__.py b/googletrans/__init__.py index 1ceb469..6dad9ad 100644 --- a/googletrans/__init__.py +++ b/googletrans/__init__.py @@ -1,6 +1,7 @@ """Free Google Translate API for Python. Translates totally free of charge.""" -__all__ = 'Translator', -__version__ = '3.4.0' + +__all__ = ("Translator",) +__version__ = "3.4.0" from googletrans.client import Translator diff --git a/googletrans/client.py b/googletrans/client.py index efb17fb..6eaf7be 100644 --- a/googletrans/client.py +++ b/googletrans/client.py @@ -4,24 +4,28 @@ You can translate text using this module. """ + import random -import typing import re +import typing -import httpcore import httpx from httpx import Timeout from googletrans import urls, utils -from googletrans.gtoken import TokenAcquirer from googletrans.constants import ( DEFAULT_CLIENT_SERVICE_URLS, - DEFAULT_USER_AGENT, LANGCODES, LANGUAGES, SPECIAL_CASES, - DEFAULT_RAISE_EXCEPTION, DUMMY_DATA + DEFAULT_RAISE_EXCEPTION, + DEFAULT_USER_AGENT, + DUMMY_DATA, + LANGCODES, + LANGUAGES, + SPECIAL_CASES, ) -from googletrans.models import Translated, Detected +from googletrans.gtoken import TokenAcquirer +from googletrans.models import Detected, Translated -EXCLUDES = ('en', 'ca', 'fr') +EXCLUDES = ("en", "ca", "fr") class Translator: @@ -52,39 +56,48 @@ class Translator: :type raise_exception: boolean """ - def __init__(self, service_urls=DEFAULT_CLIENT_SERVICE_URLS, user_agent=DEFAULT_USER_AGENT, - raise_exception=DEFAULT_RAISE_EXCEPTION, - proxies: typing.Optional[typing.Dict[typing.Union[str, httpx.URL], typing.Union[str, httpx.Proxy]]] = None, - timeout: typing.Optional[Timeout] = None, - http2=True): - + def __init__( + self, + service_urls=DEFAULT_CLIENT_SERVICE_URLS, + user_agent=DEFAULT_USER_AGENT, + raise_exception=DEFAULT_RAISE_EXCEPTION, + proxies: typing.Optional[ + typing.Dict[typing.Union[str, httpx.URL], typing.Union[str, httpx.Proxy]] + ] = None, + timeout: typing.Optional[Timeout] = None, + http2=True, + ): self.client = httpx.Client(http2=http2, proxies=proxies) - self.client.headers.update({ - 'User-Agent': user_agent, - }) + self.client.headers.update( + { + "User-Agent": user_agent, + } + ) - self.service_urls = ['translate.google.com'] - self.client_type = 'webapp' + self.service_urls = ["translate.google.com"] + self.client_type = "webapp" self.token_acquirer = TokenAcquirer( - client=self.client, host=self.service_urls[0]) + client=self.client, host=self.service_urls[0] + ) if timeout is not None: self.client.timeout = timeout if service_urls: - #default way of working: use the defined values from user app + # default way of working: use the defined values from user app self.service_urls = service_urls - self.client_type = 'webapp' + self.client_type = "webapp" self.tok1en_acquirer = TokenAcquirer( - client=self.client, host=self.service_urls[0]) + client=self.client, host=self.service_urls[0] + ) - #if we have a service url pointing to client api we force the use of it as defaut client + # if we have a service url pointing to client api we force the use of it as defaut client for t in enumerate(service_urls): - api_type = re.search('googleapis',service_urls[0]) - if (api_type): - self.service_urls = ['translate.googleapis.com'] - self.client_type = 'gtx' + api_type = re.search("googleapis", service_urls[0]) + if api_type: + self.service_urls = ["translate.googleapis.com"] + self.client_type = "gtx" break self.raise_exception = raise_exception @@ -95,12 +108,18 @@ def _pick_service_url(self): return random.choice(self.service_urls) def _translate(self, text, dest, src, override): - token = 'xxxx' #dummy default value here as it is not used by api client - if self.client_type == 'webapp': + token = "xxxx" # dummy default value here as it is not used by api client + if self.client_type == "webapp": token = self.token_acquirer.do(text) - params = utils.build_params(client=self.client_type, query=text, src=src, dest=dest, - token=token, override=override) + params = utils.build_params( + client=self.client_type, + query=text, + src=src, + dest=dest, + token=token, + override=override, + ) url = urls.TRANSLATE.format(host=self._pick_service_url()) r = self.client.get(url, params=params) @@ -110,36 +129,40 @@ def _translate(self, text, dest, src, override): return data, r if self.raise_exception: - raise Exception('Unexpected status code "{}" from {}'.format( - r.status_code, self.service_urls)) + raise Exception( + 'Unexpected status code "{}" from {}'.format( + r.status_code, self.service_urls + ) + ) DUMMY_DATA[0][0][0] = text return DUMMY_DATA, r def _parse_extra_data(self, data): response_parts_name_mapping = { - 0: 'translation', - 1: 'all-translations', - 2: 'original-language', - 5: 'possible-translations', - 6: 'confidence', - 7: 'possible-mistakes', - 8: 'language', - 11: 'synonyms', - 12: 'definitions', - 13: 'examples', - 14: 'see-also', + 0: "translation", + 1: "all-translations", + 2: "original-language", + 5: "possible-translations", + 6: "confidence", + 7: "possible-mistakes", + 8: "language", + 11: "synonyms", + 12: "definitions", + 13: "examples", + 14: "see-also", } extra = {} for index, category in response_parts_name_mapping.items(): - extra[category] = data[index] if ( - index < len(data) and data[index]) else None + extra[category] = ( + data[index] if (index < len(data) and data[index]) else None + ) return extra - def translate(self, text, dest='en', src='auto', **kwargs): + def translate(self, text, dest="en", src="auto", **kwargs): """Translate text from source language to destination language :param text: The source text(s) to be translated. Batch translation is supported via sequence input. @@ -178,16 +201,16 @@ def translate(self, text, dest='en', src='auto', **kwargs): jumps over -> 이상 점프 the lazy dog -> 게으른 개 """ - dest = dest.lower().split('_', 1)[0] - src = src.lower().split('_', 1)[0] + dest = dest.lower().split("_", 1)[0] + src = src.lower().split("_", 1)[0] - if src != 'auto' and src not in LANGUAGES: + if src != "auto" and src not in LANGUAGES: if src in SPECIAL_CASES: src = SPECIAL_CASES[src] elif src in LANGCODES: src = LANGCODES[src] else: - raise ValueError('invalid source language') + raise ValueError("invalid source language") if dest not in LANGUAGES: if dest in SPECIAL_CASES: @@ -195,7 +218,7 @@ def translate(self, text, dest='en', src='auto', **kwargs): elif dest in LANGCODES: dest = LANGCODES[dest] else: - raise ValueError('invalid destination language') + raise ValueError("invalid destination language") if isinstance(text, list): result = [] @@ -208,7 +231,7 @@ def translate(self, text, dest='en', src='auto', **kwargs): data, response = self._translate(text, dest, src, kwargs) # this code will be updated when the format is changed. - translated = ''.join([d[0] if d[0] else '' for d in data[0]]) + translated = "".join([d[0] if d[0] else "" for d in data[0]]) extra_data = self._parse_extra_data(data) @@ -228,17 +251,22 @@ def translate(self, text, dest='en', src='auto', **kwargs): if pron is None: try: pron = data[0][1][2] - except: # pragma: nocover + except: # pragma: nocover # noqa: E722 pass if dest in EXCLUDES and pron == origin: pron = translated # put final values into a new Translated object - result = Translated(src=src, dest=dest, origin=origin, - text=translated, pronunciation=pron, - extra_data=extra_data, - response=response) + result = Translated( + src=src, + dest=dest, + origin=origin, + text=translated, + pronunciation=pron, + extra_data=extra_data, + response=response, + ) return result @@ -280,18 +308,18 @@ def detect(self, text, **kwargs): result.append(lang) return result - data, response = self._translate(text, 'en', 'auto', kwargs) + data, response = self._translate(text, "en", "auto", kwargs) # actual source language that will be recognized by Google Translator when the # src passed is equal to auto. - src = '' + src = "" confidence = 0.0 try: if len(data[8][0]) > 1: src = data[8][0] confidence = data[8][-2] else: - src = ''.join(data[8][0]) + src = "".join(data[8][0]) confidence = data[8][-2][0] except Exception: # pragma: nocover pass diff --git a/googletrans/constants.py b/googletrans/constants.py index 23b1564..c11eef1 100644 --- a/googletrans/constants.py +++ b/googletrans/constants.py @@ -1,79 +1,211 @@ -DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' +DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" -DEFAULT_CLIENT_SERVICE_URLS = ( - 'translate.googleapis.com', -) +DEFAULT_CLIENT_SERVICE_URLS = ("translate.googleapis.com",) -DEFAULT_SERVICE_URLS = ('translate.google.ac', 'translate.google.ad', 'translate.google.ae', - 'translate.google.al', 'translate.google.am', 'translate.google.as', - 'translate.google.at', 'translate.google.az', 'translate.google.ba', - 'translate.google.be', 'translate.google.bf', 'translate.google.bg', - 'translate.google.bi', 'translate.google.bj', 'translate.google.bs', - 'translate.google.bt', 'translate.google.by', 'translate.google.ca', - 'translate.google.cat', 'translate.google.cc', 'translate.google.cd', - 'translate.google.cf', 'translate.google.cg', 'translate.google.ch', - 'translate.google.ci', 'translate.google.cl', 'translate.google.cm', - 'translate.google.cn', 'translate.google.co.ao', 'translate.google.co.bw', - 'translate.google.co.ck', 'translate.google.co.cr', 'translate.google.co.id', - 'translate.google.co.il', 'translate.google.co.in', 'translate.google.co.jp', - 'translate.google.co.ke', 'translate.google.co.kr', 'translate.google.co.ls', - 'translate.google.co.ma', 'translate.google.co.mz', 'translate.google.co.nz', - 'translate.google.co.th', 'translate.google.co.tz', 'translate.google.co.ug', - 'translate.google.co.uk', 'translate.google.co.uz', 'translate.google.co.ve', - 'translate.google.co.vi', 'translate.google.co.za', 'translate.google.co.zm', - 'translate.google.co.zw', 'translate.google.com.af', 'translate.google.com.ag', - 'translate.google.com.ai', 'translate.google.com.ar', 'translate.google.com.au', - 'translate.google.com.bd', 'translate.google.com.bh', 'translate.google.com.bn', - 'translate.google.com.bo', 'translate.google.com.br', 'translate.google.com.bz', - 'translate.google.com.co', 'translate.google.com.cu', 'translate.google.com.cy', - 'translate.google.com.do', 'translate.google.com.ec', 'translate.google.com.eg', - 'translate.google.com.et', 'translate.google.com.fj', 'translate.google.com.gh', - 'translate.google.com.gi', 'translate.google.com.gt', 'translate.google.com.hk', - 'translate.google.com.jm', 'translate.google.com.kh', 'translate.google.com.kw', - 'translate.google.com.lb', 'translate.google.com.ly', 'translate.google.com.mm', - 'translate.google.com.mt', 'translate.google.com.mx', 'translate.google.com.my', - 'translate.google.com.na', 'translate.google.com.ng', 'translate.google.com.ni', - 'translate.google.com.np', 'translate.google.com.om', 'translate.google.com.pa', - 'translate.google.com.pe', 'translate.google.com.pg', 'translate.google.com.ph', - 'translate.google.com.pk', 'translate.google.com.pr', 'translate.google.com.py', - 'translate.google.com.qa', 'translate.google.com.sa', 'translate.google.com.sb', - 'translate.google.com.sg', 'translate.google.com.sl', 'translate.google.com.sv', - 'translate.google.com.tj', 'translate.google.com.tr', 'translate.google.com.tw', - 'translate.google.com.ua', 'translate.google.com.uy', 'translate.google.com.vc', - 'translate.google.com.vn', 'translate.google.com', 'translate.google.cv', - 'translate.google.cz', 'translate.google.de', 'translate.google.dj', - 'translate.google.dk', 'translate.google.dm', 'translate.google.dz', - 'translate.google.ee', 'translate.google.es', 'translate.google.eu', - 'translate.google.fi', 'translate.google.fm', 'translate.google.fr', - 'translate.google.ga', 'translate.google.ge', 'translate.google.gf', - 'translate.google.gg', 'translate.google.gl', 'translate.google.gm', - 'translate.google.gp', 'translate.google.gr', 'translate.google.gy', - 'translate.google.hn', 'translate.google.hr', 'translate.google.ht', - 'translate.google.hu', 'translate.google.ie', 'translate.google.im', - 'translate.google.io', 'translate.google.iq', 'translate.google.is', - 'translate.google.it', 'translate.google.je', 'translate.google.jo', - 'translate.google.kg', 'translate.google.ki', 'translate.google.kz', - 'translate.google.la', 'translate.google.li', 'translate.google.lk', - 'translate.google.lt', 'translate.google.lu', 'translate.google.lv', - 'translate.google.md', 'translate.google.me', 'translate.google.mg', - 'translate.google.mk', 'translate.google.ml', 'translate.google.mn', - 'translate.google.ms', 'translate.google.mu', 'translate.google.mv', - 'translate.google.mw', 'translate.google.ne', 'translate.google.nf', - 'translate.google.nl', 'translate.google.no', 'translate.google.nr', - 'translate.google.nu', 'translate.google.pl', 'translate.google.pn', - 'translate.google.ps', 'translate.google.pt', 'translate.google.ro', - 'translate.google.rs', 'translate.google.ru', 'translate.google.rw', - 'translate.google.sc', 'translate.google.se', 'translate.google.sh', - 'translate.google.si', 'translate.google.sk', 'translate.google.sm', - 'translate.google.sn', 'translate.google.so', 'translate.google.sr', - 'translate.google.st', 'translate.google.td', 'translate.google.tg', - 'translate.google.tk', 'translate.google.tl', 'translate.google.tm', - 'translate.google.tn', 'translate.google.to', 'translate.google.tt', - 'translate.google.us', 'translate.google.vg', 'translate.google.vu', - 'translate.google.ws') +DEFAULT_SERVICE_URLS = ( + "translate.google.ac", + "translate.google.ad", + "translate.google.ae", + "translate.google.al", + "translate.google.am", + "translate.google.as", + "translate.google.at", + "translate.google.az", + "translate.google.ba", + "translate.google.be", + "translate.google.bf", + "translate.google.bg", + "translate.google.bi", + "translate.google.bj", + "translate.google.bs", + "translate.google.bt", + "translate.google.by", + "translate.google.ca", + "translate.google.cat", + "translate.google.cc", + "translate.google.cd", + "translate.google.cf", + "translate.google.cg", + "translate.google.ch", + "translate.google.ci", + "translate.google.cl", + "translate.google.cm", + "translate.google.cn", + "translate.google.co.ao", + "translate.google.co.bw", + "translate.google.co.ck", + "translate.google.co.cr", + "translate.google.co.id", + "translate.google.co.il", + "translate.google.co.in", + "translate.google.co.jp", + "translate.google.co.ke", + "translate.google.co.kr", + "translate.google.co.ls", + "translate.google.co.ma", + "translate.google.co.mz", + "translate.google.co.nz", + "translate.google.co.th", + "translate.google.co.tz", + "translate.google.co.ug", + "translate.google.co.uk", + "translate.google.co.uz", + "translate.google.co.ve", + "translate.google.co.vi", + "translate.google.co.za", + "translate.google.co.zm", + "translate.google.co.zw", + "translate.google.com.af", + "translate.google.com.ag", + "translate.google.com.ai", + "translate.google.com.ar", + "translate.google.com.au", + "translate.google.com.bd", + "translate.google.com.bh", + "translate.google.com.bn", + "translate.google.com.bo", + "translate.google.com.br", + "translate.google.com.bz", + "translate.google.com.co", + "translate.google.com.cu", + "translate.google.com.cy", + "translate.google.com.do", + "translate.google.com.ec", + "translate.google.com.eg", + "translate.google.com.et", + "translate.google.com.fj", + "translate.google.com.gh", + "translate.google.com.gi", + "translate.google.com.gt", + "translate.google.com.hk", + "translate.google.com.jm", + "translate.google.com.kh", + "translate.google.com.kw", + "translate.google.com.lb", + "translate.google.com.ly", + "translate.google.com.mm", + "translate.google.com.mt", + "translate.google.com.mx", + "translate.google.com.my", + "translate.google.com.na", + "translate.google.com.ng", + "translate.google.com.ni", + "translate.google.com.np", + "translate.google.com.om", + "translate.google.com.pa", + "translate.google.com.pe", + "translate.google.com.pg", + "translate.google.com.ph", + "translate.google.com.pk", + "translate.google.com.pr", + "translate.google.com.py", + "translate.google.com.qa", + "translate.google.com.sa", + "translate.google.com.sb", + "translate.google.com.sg", + "translate.google.com.sl", + "translate.google.com.sv", + "translate.google.com.tj", + "translate.google.com.tr", + "translate.google.com.tw", + "translate.google.com.ua", + "translate.google.com.uy", + "translate.google.com.vc", + "translate.google.com.vn", + "translate.google.com", + "translate.google.cv", + "translate.google.cz", + "translate.google.de", + "translate.google.dj", + "translate.google.dk", + "translate.google.dm", + "translate.google.dz", + "translate.google.ee", + "translate.google.es", + "translate.google.eu", + "translate.google.fi", + "translate.google.fm", + "translate.google.fr", + "translate.google.ga", + "translate.google.ge", + "translate.google.gf", + "translate.google.gg", + "translate.google.gl", + "translate.google.gm", + "translate.google.gp", + "translate.google.gr", + "translate.google.gy", + "translate.google.hn", + "translate.google.hr", + "translate.google.ht", + "translate.google.hu", + "translate.google.ie", + "translate.google.im", + "translate.google.io", + "translate.google.iq", + "translate.google.is", + "translate.google.it", + "translate.google.je", + "translate.google.jo", + "translate.google.kg", + "translate.google.ki", + "translate.google.kz", + "translate.google.la", + "translate.google.li", + "translate.google.lk", + "translate.google.lt", + "translate.google.lu", + "translate.google.lv", + "translate.google.md", + "translate.google.me", + "translate.google.mg", + "translate.google.mk", + "translate.google.ml", + "translate.google.mn", + "translate.google.ms", + "translate.google.mu", + "translate.google.mv", + "translate.google.mw", + "translate.google.ne", + "translate.google.nf", + "translate.google.nl", + "translate.google.no", + "translate.google.nr", + "translate.google.nu", + "translate.google.pl", + "translate.google.pn", + "translate.google.ps", + "translate.google.pt", + "translate.google.ro", + "translate.google.rs", + "translate.google.ru", + "translate.google.rw", + "translate.google.sc", + "translate.google.se", + "translate.google.sh", + "translate.google.si", + "translate.google.sk", + "translate.google.sm", + "translate.google.sn", + "translate.google.so", + "translate.google.sr", + "translate.google.st", + "translate.google.td", + "translate.google.tg", + "translate.google.tk", + "translate.google.tl", + "translate.google.tm", + "translate.google.tn", + "translate.google.to", + "translate.google.tt", + "translate.google.us", + "translate.google.vg", + "translate.google.vu", + "translate.google.ws", +) SPECIAL_CASES = { - 'ee': 'et', + "ee": "et", } LANGUAGES = { @@ -327,5 +459,14 @@ LANGCODES = dict(map(reversed, LANGUAGES.items())) DEFAULT_RAISE_EXCEPTION = False -DUMMY_DATA = [[["", None, None, 0]], None, "en", None, - None, None, 1, None, [["en"], None, [1], ["en"]]] +DUMMY_DATA = [ + [["", None, None, 0]], + None, + "en", + None, + None, + None, + 1, + None, + [["en"], None, [1], ["en"]], +] diff --git a/googletrans/gtoken.py b/googletrans/gtoken.py index a6e6338..46da0cd 100644 --- a/googletrans/gtoken.py +++ b/googletrans/gtoken.py @@ -35,20 +35,19 @@ class TokenAcquirer: 950629.577246 """ - RE_TKK = re.compile(r'tkk:\'(.+?)\'', re.DOTALL) - RE_RAWTKK = re.compile(r'tkk:\'(.+?)\'', re.DOTALL) + RE_TKK = re.compile(r"tkk:\'(.+?)\'", re.DOTALL) + RE_RAWTKK = re.compile(r"tkk:\'(.+?)\'", re.DOTALL) - def __init__(self, client: httpx.Client, tkk='0', host='translate.google.com'): + def __init__(self, client: httpx.Client, tkk="0", host="translate.google.com"): self.client = client self.tkk = tkk - self.host = host if 'http' in host else 'https://' + host + self.host = host if "http" in host else "https://" + host def _update(self): - """update tkk - """ + """update tkk""" # we don't need to update the base TKK value when it is still valid now = math.floor(int(time.time() * 1000) / 3600000.0) - if self.tkk and int(self.tkk.split('.')[0]) == now: + if self.tkk and int(self.tkk.split(".")[0]) == now: return r = self.client.get(self.host) @@ -62,14 +61,14 @@ def _update(self): if code is not None: # this will be the same as python code after stripping out a reserved word 'var' - code = code.group(1).replace('var ', '') + code = code.group(1).replace("var ", "") # unescape special ascii characters such like a \x3d(=) - code = code.encode().decode('unicode-escape') - + code = code.encode().decode("unicode-escape") + if code: tree = ast.parse(code) visit_return = False - operator = '+' + operator = "+" n, keys = 0, dict(a=0, b=0) for node in ast.walk(tree): if isinstance(node, ast.Assign): @@ -78,8 +77,9 @@ def _update(self): if isinstance(node.value, ast.Num): keys[name] = node.value.n # the value can sometimes be negative - elif isinstance(node.value, ast.UnaryOp) and \ - isinstance(node.value.op, ast.USub): # pragma: nocover + elif isinstance(node.value, ast.UnaryOp) and isinstance( + node.value.op, ast.USub + ): # pragma: nocover keys[name] = -node.value.operand.n elif isinstance(node, ast.Return): # parameters should be set after this point @@ -92,18 +92,19 @@ def _update(self): if isinstance(node, ast.Add): # pragma: nocover pass elif isinstance(node, ast.Sub): # pragma: nocover - operator = '-' + operator = "-" elif isinstance(node, ast.Mult): # pragma: nocover - operator = '*' + operator = "*" elif isinstance(node, ast.Pow): # pragma: nocover - operator = '**' + operator = "**" elif isinstance(node, ast.BitXor): # pragma: nocover - operator = '^' + operator = "^" # a safety way to avoid Exceptions - clause = compile('{1}{0}{2}'.format( - operator, keys['a'], keys['b']), '', 'eval') + clause = compile( + "{1}{0}{2}".format(operator, keys["a"], keys["b"]), "", "eval" + ) value = eval(clause, dict(__builtin__={})) - result = '{}.{}'.format(n, value) + result = "{}.{}".format(n, value) self.tkk = result @@ -130,9 +131,9 @@ def _xr(self, a, b): c = 0 while c < size_b - 2: d = b[c + 2] - d = ord(d[0]) - 87 if 'a' <= d else int(d) - d = rshift(a, d) if '+' == b[c + 1] else a << d - a = a + d & 4294967295 if '+' == b[c] else a ^ d + d = ord(d[0]) - 87 if "a" <= d else int(d) + d = rshift(a, d) if "+" == b[c + 1] else a << d + a = a + d & 4294967295 if "+" == b[c] else a ^ d c += 3 return a @@ -148,11 +149,11 @@ def acquire(self, text): # Python doesn't natively use Unicode surrogates, so account for those a += [ math.floor((val - 0x10000) / 0x400 + 0xD800), - math.floor((val - 0x10000) % 0x400 + 0xDC00) + math.floor((val - 0x10000) % 0x400 + 0xDC00), ] - b = self.tkk if self.tkk != '0' else '' - d = b.split('.') + b = self.tkk if self.tkk != "0" else "" + d = b.split(".") b = int(d[0]) if len(d) > 1 else 0 # assume e means char code array @@ -160,7 +161,7 @@ def acquire(self, text): g = 0 size = len(a) while g < size: - l = a[g] + l = a[g] # noqa: E741 # just append if l is less than 128(ascii: DEL) if l < 128: e.append(l) @@ -170,10 +171,15 @@ def acquire(self, text): e.append(l >> 6 | 192) else: # append calculated value if l matches special condition - if (l & 64512) == 55296 and g + 1 < size and \ - a[g + 1] & 64512 == 56320: + if ( + (l & 64512) == 55296 + and g + 1 < size + and a[g + 1] & 64512 == 56320 + ): g += 1 - l = 65536 + ((l & 1023) << 10) + (a[g] & 1023) # This bracket is important + l = ( # noqa: E741 + 65536 + ((l & 1023) << 10) + (a[g] & 1023) + ) # This bracket is important e.append(l >> 18 | 240) e.append(l >> 12 & 63 | 128) else: @@ -184,14 +190,14 @@ def acquire(self, text): a = b for i, value in enumerate(e): a += value - a = self._xr(a, '+-a^+6') - a = self._xr(a, '+-3^+b+-f') + a = self._xr(a, "+-a^+6") + a = self._xr(a, "+-3^+b+-f") a ^= int(d[1]) if len(d) > 1 else 0 if a < 0: # pragma: nocover a = (a & 2147483647) + 2147483648 a %= 1000000 # int(1E6) - return '{}.{}'.format(a, a ^ b) + return "{}.{}".format(a, a ^ b) def do(self, text): self._update() diff --git a/googletrans/models.py b/googletrans/models.py index 7fbeaba..818400d 100644 --- a/googletrans/models.py +++ b/googletrans/models.py @@ -16,8 +16,9 @@ class Translated(Base): :param pronunciation: pronunciation """ - def __init__(self, src, dest, origin, text, pronunciation, extra_data=None, - **kwargs): + def __init__( + self, src, dest, origin, text, pronunciation, extra_data=None, **kwargs + ): super().__init__(**kwargs) self.src = src self.dest = dest @@ -31,11 +32,13 @@ def __str__(self): # pragma: nocover def __unicode__(self): # pragma: nocover return ( - u'Translated(src={src}, dest={dest}, text={text}, pronunciation={pronunciation}, ' - u'extra_data={extra_data})'.format( - src=self.src, dest=self.dest, text=self.text, + "Translated(src={src}, dest={dest}, text={text}, pronunciation={pronunciation}, " + "extra_data={extra_data})".format( + src=self.src, + dest=self.dest, + text=self.text, pronunciation=self.pronunciation, - extra_data='"' + repr(self.extra_data)[:10] + '..."' + extra_data='"' + repr(self.extra_data)[:10] + '..."', ) ) @@ -56,5 +59,6 @@ def __str__(self): # pragma: nocover return self.__unicode__() def __unicode__(self): # pragma: nocover - return u'Detected(lang={lang}, confidence={confidence})'.format( - lang=self.lang, confidence=self.confidence) + return "Detected(lang={lang}, confidence={confidence})".format( + lang=self.lang, confidence=self.confidence + ) diff --git a/googletrans/urls.py b/googletrans/urls.py index 0bf8933..6013218 100644 --- a/googletrans/urls.py +++ b/googletrans/urls.py @@ -2,5 +2,6 @@ """ Predefined URLs used to make google translate requests. """ -BASE = 'https://translate.google.com' -TRANSLATE = 'https://{host}/translate_a/single' + +BASE = "https://translate.google.com" +TRANSLATE = "https://{host}/translate_a/single" diff --git a/googletrans/utils.py b/googletrans/utils.py index 70cd24e..f17c9bb 100644 --- a/googletrans/utils.py +++ b/googletrans/utils.py @@ -1,22 +1,23 @@ """A conversion module for googletrans""" + import json import re -def build_params(client,query, src, dest, token, override): +def build_params(client, query, src, dest, token, override): params = { - 'client': client, - 'sl': src, - 'tl': dest, - 'hl': dest, - 'dt': ['at', 'bd', 'ex', 'ld', 'md', 'qca', 'rw', 'rm', 'ss', 't'], - 'ie': 'UTF-8', - 'oe': 'UTF-8', - 'otf': 1, - 'ssel': 0, - 'tsel': 0, - 'tk': token, - 'q': query, + "client": client, + "sl": src, + "tl": dest, + "hl": dest, + "dt": ["at", "bd", "ex", "ld", "md", "qca", "rw", "rm", "ss", "t"], + "ie": "UTF-8", + "oe": "UTF-8", + "otf": 1, + "ssel": 0, + "tsel": 0, + "tk": token, + "q": query, } if override is not None: @@ -40,10 +41,10 @@ def legacy_format_json(original): states.append((p, text[p:nxt])) # replace all wiered characters in text - while text.find(',,') > -1: - text = text.replace(',,', ',null,') - while text.find('[,') > -1: - text = text.replace('[,', '[null,') + while text.find(",,") > -1: + text = text.replace(",,", ",null,") + while text.find("[,") > -1: + text = text.replace("[,", "[null,") # recover state for i, pos in enumerate(re.finditer('"', text)): @@ -74,6 +75,5 @@ def format_json(original): def rshift(val, n): - """python port for '>>>'(right shift with padding) - """ + """python port for '>>>'(right shift with padding)""" return (val % 0x100000000) >> n diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d49558b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[project] +name = "py-googletrans" +version = "4.0.0" +description = "An unofficial Google Translate API for Python" +readme = "README.md" +requires-python = ">=3.8" +license = { file = "LICENSE" } +dependencies = ["httpx[http2]>=0.27.2"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "License :: Freeware", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Topic :: Education", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[project.optional-dependencies] +dev = ["pytest", "coveralls", "ruff>=0.7"] + +[tool.uv] +dev-dependencies = ["ruff>=0.7"] + +[project.scripts] +translate = "googletrans:translate" + +[project.urls] +homepage = "https://github.com/ssut/py-googletrans" + +[[project.authors]] +name = "SuHun Han" +email = "ssut@ssut.me" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6af1fff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,28 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml -o requirements.txt +anyio==4.6.2.post1 + # via httpx +certifi==2024.8.30 + # via + # httpcore + # httpx +h11==0.14.0 + # via httpcore +h2==4.1.0 + # via httpx +hpack==4.0.0 + # via h2 +httpcore==1.0.7 + # via httpx +httpx==0.27.2 + # via py-googletrans (pyproject.toml) +hyperframe==6.0.1 + # via h2 +idna==3.10 + # via + # anyio + # httpx +sniffio==1.3.1 + # via + # anyio + # httpx diff --git a/setup.py b/setup.py deleted file mode 100644 index 3168c93..0000000 --- a/setup.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import os.path -import re - -from setuptools import setup, find_packages - - -def get_file(*paths): - path = os.path.join(*paths) - try: - with open(path, 'rb') as f: - return f.read().decode('utf8') - except IOError: - pass - - -def get_version(): - init_py = get_file(os.path.dirname(__file__), 'googletrans', '__init__.py') - pattern = r"{0}\W*=\W*'([^']+)'".format('__version__') - version, = re.findall(pattern, init_py) - return version - - -def get_description(): - init_py = get_file(os.path.dirname(__file__), 'googletrans', '__init__.py') - pattern = r'"""(.*?)"""' - description, = re.findall(pattern, init_py, re.DOTALL) - return description - - -def get_readme(): - return get_file(os.path.dirname(__file__), 'README.rst') - - -def install(): - setup( - name='googletrans', - version=get_version(), - description=get_description(), - long_description=get_readme(), - license='MIT', - author='SuHun Han', - author_email='ssut' '@' 'ssut.me', - url='https://github.com/ssut/py-googletrans', - classifiers=['Development Status :: 5 - Production/Stable', - 'Intended Audience :: Education', - 'Intended Audience :: End Users/Desktop', - 'License :: Freeware', - 'Operating System :: POSIX', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: MacOS :: MacOS X', - 'Topic :: Education', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8'], - packages=find_packages(exclude=['docs', 'tests']), - keywords='google translate translator', - install_requires=[ - 'httpx[http2]>=0.23,<0.27.3', - ], - python_requires= '>=3.6', - tests_require=[ - 'pytest', - 'coveralls', - ], - scripts=['translate'] - ) - - -if __name__ == "__main__": - install() diff --git a/tests/conftest.py b/tests/conftest.py index 20ca51e..4042b17 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ from pytest import fixture -@fixture(scope='session') +@fixture(scope="session") def translator(): from googletrans import Translator + return Translator() diff --git a/tests/test_client.py b/tests/test_client.py index 599559b..035c609 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -9,138 +9,138 @@ def test_bind_multiple_service_urls(): service_urls = [ - 'translate.google.com', - 'translate.google.co.kr', + "translate.google.com", + "translate.google.co.kr", ] translator = Translator(service_urls=service_urls) assert translator.service_urls == service_urls - assert translator.translate('test', dest='ko') - assert translator.detect('Hello') + assert translator.translate("test", dest="ko") + assert translator.detect("Hello") + def test_api_service_urls(): - service_urls = ['translate.googleapis.com'] + service_urls = ["translate.googleapis.com"] translator = Translator(service_urls=service_urls) assert translator.service_urls == service_urls - assert translator.translate('test', dest='ko') - assert translator.detect('Hello') + assert translator.translate("test", dest="ko") + assert translator.detect("Hello") def test_source_language(translator): - result = translator.translate('안녕하세요.') - assert result.src == 'ko' + result = translator.translate("안녕하세요.") + assert result.src == "ko" def test_pronunciation(translator): - result = translator.translate('안녕하세요.', dest='ja') - assert result.pronunciation == 'Kon\'nichiwa.' + result = translator.translate("안녕하세요.", dest="ja") + assert result.pronunciation == "Kon'nichiwa." def test_pronunciation_issue_175(translator): - result = translator.translate('Hello', src='en', dest='ru') + result = translator.translate("Hello", src="en", dest="ru") assert result.pronunciation is not None def test_latin_to_english(translator): - result = translator.translate('veritas lux mea', src='la', dest='en') - assert result.text == 'truth is my light' + result = translator.translate("veritas lux mea", src="la", dest="en") + assert result.text == "truth is my light" def test_unicode(translator): - result = translator.translate(u'안녕하세요.', src='ko', dest='ja') - assert result.text == u'こんにちは。' + result = translator.translate("안녕하세요.", src="ko", dest="ja") + assert result.text == "こんにちは。" def test_emoji(translator): - result = translator.translate('😀') - assert result.text == u'😀' + result = translator.translate("😀") + assert result.text == "😀" def test_language_name(translator): - result = translator.translate(u'Hello', src='ENGLISH', dest='iRiSh') - assert result.text == u'Dia duit' + result = translator.translate("Hello", src="ENGLISH", dest="iRiSh") + assert result.text == "Dia duit" def test_language_name_with_space(translator): - result = translator.translate( - u'Hello', src='en', dest='chinese (simplified)') - assert result.dest == 'zh-cn' + result = translator.translate("Hello", src="en", dest="chinese (simplified)") + assert result.dest == "zh-cn" def test_language_rfc1766(translator): - result = translator.translate(u'luna', src='it_ch@euro', dest='en') - assert result.text == u'moon' + result = translator.translate("luna", src="it_ch@euro", dest="en") + assert result.text == "moon" def test_special_chars(translator): - text = u"©×《》" + text = "©×《》" - result = translator.translate(text, src='en', dest='en') + result = translator.translate(text, src="en", dest="en") assert result.text == text def test_translate_list(translator): - args = (['test', 'exam', 'exam paper'], 'ko', 'en') + args = (["test", "exam", "exam paper"], "ko", "en") translations = translator.translate(*args) - assert translations[0].text == u'시험' - assert translations[1].text == u'시험' - assert translations[2].text == u'시험지' + assert translations[0].text == "시험" + assert translations[1].text == "시험" + assert translations[2].text == "시험지" def test_detect_language(translator): - ko = translator.detect(u'한국어') - en = translator.detect('English') - rubg = translator.detect('тест') - russ = translator.detect('привет') - - assert ko.lang == 'ko' - assert en.lang == 'en' - assert rubg.lang == 'mk' - assert russ.lang == 'ru' + ko = translator.detect("한국어") + en = translator.detect("English") + rubg = translator.detect("тест") + russ = translator.detect("привет") + + assert ko.lang == "ko" + assert en.lang == "en" + assert rubg.lang == "mk" + assert russ.lang == "ru" #'bg'] def test_detect_list(translator): - items = [u'한국어', ' English', 'тест', 'привет'] + items = ["한국어", " English", "тест", "привет"] result = translator.detect(items) - assert result[0].lang == 'ko' - assert result[1].lang == 'en' - assert result[2].lang == 'mk' - assert result[3].lang == 'ru' + assert result[0].lang == "ko" + assert result[1].lang == "en" + assert result[2].lang == "mk" + assert result[3].lang == "ru" def test_src_in_special_cases(translator): - args = ('tere', 'en', 'ee') + args = ("tere", "en", "ee") result = translator.translate(*args) - assert result.text in ('hello', 'hi,') + assert result.text in ("hello", "hi,") def test_src_not_in_supported_languages(translator): - args = ('Hello', 'en', 'zzz') + args = ("Hello", "en", "zzz") with raises(ValueError): translator.translate(*args) def test_dest_in_special_cases(translator): - args = ('hello', 'ee', 'en') + args = ("hello", "ee", "en") result = translator.translate(*args) - assert result.text == 'tere' + assert result.text == "tere" def test_dest_not_in_supported_languages(translator): - args = ('Hello', 'zzz', 'en') + args = ("Hello", "zzz", "en") with raises(ValueError): translator.translate(*args) @@ -150,16 +150,16 @@ def test_timeout(): # httpx will raise ConnectError in some conditions with raises((TimeoutException, ConnectError, ConnectTimeout)): translator = Translator(timeout=Timeout(0.0001)) - translator.translate('안녕하세요.') + translator.translate("안녕하세요.") class MockResponse: def __init__(self, status_code): self.status_code = status_code - self.text = 'tkk:\'translation\'' + self.text = "tkk:'translation'" -@patch.object(Client, 'get', return_value=MockResponse('403')) +@patch.object(Client, "get", return_value=MockResponse("403")) def test_403_error(session_mock): translator = Translator() - assert translator.translate('test', dest='ko') + assert translator.translate("test", dest="ko") diff --git a/tests/test_gtoken.py b/tests/test_gtoken.py index be80729..258a400 100644 --- a/tests/test_gtoken.py +++ b/tests/test_gtoken.py @@ -5,14 +5,14 @@ from pytest import fixture -@fixture(scope='session') +@fixture(scope="session") def acquirer(): client = httpx.Client(http2=True) return gtoken.TokenAcquirer(client=client) def test_acquire_token(acquirer): - text = 'test' + text = "test" result = acquirer.do(text) @@ -20,7 +20,7 @@ def test_acquire_token(acquirer): def test_acquire_token_ascii_less_than_2048(acquirer): - text = u'Ѐ' + text = "Ѐ" result = acquirer.do(text) @@ -33,6 +33,7 @@ def unichar(i): return unichr(i) except NameError: return chr(i) + text = unichar(55296) + unichar(56320) result = acquirer.do(text) @@ -41,7 +42,7 @@ def unichar(i): def test_acquire_token_ascii_else(acquirer): - text = u'가' + text = "가" result = acquirer.do(text) @@ -49,7 +50,7 @@ def test_acquire_token_ascii_else(acquirer): def test_reuse_valid_token(acquirer): - text = 'test' + text = "test" first = acquirer.do(text) second = acquirer.do(text) diff --git a/tests/test_utils.py b/tests/test_utils.py index 7d837da..4bca4bc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,8 +7,17 @@ def test_format_json(): result = utils.format_json(text) - assert result == [None, None, 'en', None, None, None, 0.96954316, None, - [['en'], None, [0.96954316]]] + assert result == [ + None, + None, + "en", + None, + None, + None, + 0.96954316, + None, + [["en"], None, [0.96954316]], + ] def test_format_malformed_json(): @@ -28,14 +37,14 @@ def test_rshift(): def test_build_params_with_override(): params = utils.build_params( - client='', - query='', - src='', - dest='', - token='', + client="", + query="", + src="", + dest="", + token="", override={ - 'otf': '3', + "otf": "3", }, ) - assert params['otf'] == '3' + assert params["otf"] == "3" From 1e3715e503eba0aa29f5a06b14be0493f4f0d046 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Wed, 20 Nov 2024 21:28:09 +0900 Subject: [PATCH 02/16] feat(ci): uv --- .github/workflows/ci.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7e8ce8..9e1a0f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,22 +6,18 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox tox-gh-actions + run: uv python install ${{ matrix.python-version }} + - name: Install the project + run: uv sync --all-extras --dev - name: Test with tox run: tox - - From 83cee7c3ea50854a5da6ba8d1949ac497f5340f6 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Wed, 20 Nov 2024 21:29:26 +0900 Subject: [PATCH 03/16] fix(ci): use uv to run command --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e1a0f4..315f793 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,4 +20,4 @@ jobs: - name: Install the project run: uv sync --all-extras --dev - name: Test with tox - run: tox + run: uv run tox From d5ea8985dfb71350d90736327e310eb953f26da0 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Wed, 20 Nov 2024 22:06:54 +0900 Subject: [PATCH 04/16] refactor: typing --- googletrans/constants.py | 2 +- googletrans/gtoken.py | 72 +++++++++++++++++++++++----------------- googletrans/models.py | 15 +++++++-- googletrans/utils.py | 42 +++++++++++++---------- 4 files changed, 79 insertions(+), 52 deletions(-) diff --git a/googletrans/constants.py b/googletrans/constants.py index c11eef1..2e81281 100644 --- a/googletrans/constants.py +++ b/googletrans/constants.py @@ -457,7 +457,7 @@ "zu": "zulu", } -LANGCODES = dict(map(reversed, LANGUAGES.items())) +LANGCODES = {v: k for k, v in LANGUAGES.items()} DEFAULT_RAISE_EXCEPTION = False DUMMY_DATA = [ [["", None, None, 0]], diff --git a/googletrans/gtoken.py b/googletrans/gtoken.py index 46da0cd..d7ae2f4 100644 --- a/googletrans/gtoken.py +++ b/googletrans/gtoken.py @@ -3,6 +3,7 @@ import math import re import time +from typing import Any, Callable, Dict, List import httpx @@ -38,19 +39,24 @@ class TokenAcquirer: RE_TKK = re.compile(r"tkk:\'(.+?)\'", re.DOTALL) RE_RAWTKK = re.compile(r"tkk:\'(.+?)\'", re.DOTALL) - def __init__(self, client: httpx.Client, tkk="0", host="translate.google.com"): + def __init__( + self, + client: httpx.AsyncClient, + tkk: str = "0", + host: str = "translate.google.com", + ) -> None: self.client = client self.tkk = tkk self.host = host if "http" in host else "https://" + host - def _update(self): + async def _update(self) -> None: """update tkk""" # we don't need to update the base TKK value when it is still valid now = math.floor(int(time.time() * 1000) / 3600000.0) if self.tkk and int(self.tkk.split(".")[0]) == now: return - r = self.client.get(self.host) + r = await self.client.get(self.host) raw_tkk = self.RE_TKK.search(r.text) if raw_tkk: @@ -69,24 +75,28 @@ def _update(self): tree = ast.parse(code) visit_return = False operator = "+" - n, keys = 0, dict(a=0, b=0) + n: int = 0 + keys: Dict[str, int] = dict(a=0, b=0) for node in ast.walk(tree): if isinstance(node, ast.Assign): - name = node.targets[0].id + name = None + if isinstance(node.targets[0], ast.Name): + name = node.targets[0].id if name in keys: - if isinstance(node.value, ast.Num): - keys[name] = node.value.n + if isinstance(node.value, ast.Constant): + keys[name] = int(node.value.value) # the value can sometimes be negative elif isinstance(node.value, ast.UnaryOp) and isinstance( node.value.op, ast.USub ): # pragma: nocover - keys[name] = -node.value.operand.n + if isinstance(node.value.operand, ast.Constant): + keys[name] = -int(node.value.operand.value) elif isinstance(node, ast.Return): # parameters should be set after this point visit_return = True - elif visit_return and isinstance(node, ast.Num): - n = node.n - elif visit_return and n > 0: + elif visit_return and isinstance(node, ast.Constant): + n = int(node.value) + elif visit_return and isinstance(n, int) and n > 0: # the default operator is '+' but implement some more for # all possible scenarios if isinstance(node, ast.Add): # pragma: nocover @@ -108,7 +118,7 @@ def _update(self): self.tkk = result - def _lazy(self, value): + def _lazy(self, value: Any) -> Callable[[], Any]: """like lazy evaluation, this method returns a lambda function that returns value given. We won't be needing this because this seems to have been built for @@ -126,7 +136,7 @@ def _lazy(self, value): """ return lambda: value - def _xr(self, a, b): + def _xr(self, a: int, b: str) -> int: size_b = len(b) c = 0 while c < size_b - 2: @@ -138,8 +148,8 @@ def _xr(self, a, b): c += 3 return a - def acquire(self, text): - a = [] + def acquire(self, text: str) -> str: + a: List[int] = [] # Convert text to ints for i in text: val = ord(i) @@ -154,10 +164,10 @@ def acquire(self, text): b = self.tkk if self.tkk != "0" else "" d = b.split(".") - b = int(d[0]) if len(d) > 1 else 0 + b_val = int(d[0]) if len(d) > 1 else 0 # assume e means char code array - e = [] + e: List[int] = [] g = 0 size = len(a) while g < size: @@ -187,19 +197,19 @@ def acquire(self, text): e.append(l >> 6 & 63 | 128) e.append(l & 63 | 128) g += 1 - a = b - for i, value in enumerate(e): - a += value - a = self._xr(a, "+-a^+6") - a = self._xr(a, "+-3^+b+-f") - a ^= int(d[1]) if len(d) > 1 else 0 - if a < 0: # pragma: nocover - a = (a & 2147483647) + 2147483648 - a %= 1000000 # int(1E6) - - return "{}.{}".format(a, a ^ b) - - def do(self, text): - self._update() + a_val = b_val + for value in e: + a_val += value + a_val = self._xr(a_val, "+-a^+6") + a_val = self._xr(a_val, "+-3^+b+-f") + a_val ^= int(d[1]) if len(d) > 1 else 0 + if a_val < 0: # pragma: nocover + a_val = (a_val & 2147483647) + 2147483648 + a_val %= 1000000 # int(1E6) + + return "{}.{}".format(a_val, a_val ^ b_val) + + async def do(self, text: str) -> str: + await self._update() tk = self.acquire(text) return tk diff --git a/googletrans/models.py b/googletrans/models.py index 818400d..c9c8e4c 100644 --- a/googletrans/models.py +++ b/googletrans/models.py @@ -1,8 +1,10 @@ +from typing import Optional + from httpx import Response class Base: - def __init__(self, response: Response = None): + def __init__(self, response: Optional[Response] = None): self._response = response @@ -17,7 +19,14 @@ class Translated(Base): """ def __init__( - self, src, dest, origin, text, pronunciation, extra_data=None, **kwargs + self, + src: str, + dest: str, + origin: str, + text: str, + pronunciation: str, + extra_data: Optional[dict] = None, + **kwargs, ): super().__init__(**kwargs) self.src = src @@ -50,7 +59,7 @@ class Detected(Base): :param confidence: the confidence of detection result (0.00 to 1.00) """ - def __init__(self, lang, confidence, **kwargs): + def __init__(self, lang: str, confidence: float, **kwargs): super().__init__(**kwargs) self.lang = lang self.confidence = confidence diff --git a/googletrans/utils.py b/googletrans/utils.py index f17c9bb..6861598 100644 --- a/googletrans/utils.py +++ b/googletrans/utils.py @@ -2,10 +2,18 @@ import json import re - - -def build_params(client, query, src, dest, token, override): - params = { +from typing import Any, Dict, Iterator, List, Optional, Tuple + + +def build_params( + client: str, + query: str, + src: str, + dest: str, + token: str, + override: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + params: Dict[str, Any] = { "client": client, "sl": src, "tl": dest, @@ -27,17 +35,17 @@ def build_params(client, query, src, dest, token, override): return params -def legacy_format_json(original): +def legacy_format_json(original: str) -> Dict[str, Any]: # save state - states = [] - text = original + states: List[Tuple[int, str]] = [] + text: str = original # save position for double-quoted texts for i, pos in enumerate(re.finditer('"', text)): # pos.start() is a double-quote - p = pos.start() + 1 + p: int = pos.start() + 1 if i % 2 == 0: - nxt = text.find('"', p) + nxt: int = text.find('"', p) states.append((p, text[p:nxt])) # replace all wiered characters in text @@ -48,32 +56,32 @@ def legacy_format_json(original): # recover state for i, pos in enumerate(re.finditer('"', text)): - p = pos.start() + 1 + p: int = pos.start() + 1 if i % 2 == 0: - j = int(i / 2) - nxt = text.find('"', p) + j: int = int(i / 2) + nxt: int = text.find('"', p) # replacing a portion of a string # use slicing to extract those parts of the original string to be kept text = text[:p] + states[j][1] + text[nxt:] - converted = json.loads(text) + converted: Dict[str, Any] = json.loads(text) return converted -def get_items(dict_object): +def get_items(dict_object: Dict[str, Any]) -> Iterator[Tuple[Any, Any]]: for key in dict_object: yield key, dict_object[key] -def format_json(original): +def format_json(original: str) -> Dict[str, Any]: try: - converted = json.loads(original) + converted: Dict[str, Any] = json.loads(original) except ValueError: converted = legacy_format_json(original) return converted -def rshift(val, n): +def rshift(val: int, n: int) -> int: """python port for '>>>'(right shift with padding)""" return (val % 0x100000000) >> n From d2ce9801804847122e3509670f747e768cc1fb61 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Wed, 20 Nov 2024 22:07:09 +0900 Subject: [PATCH 05/16] feat(client)!: use httpx.AsyncClient by default --- googletrans/client.py | 89 ++++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/googletrans/client.py b/googletrans/client.py index 6eaf7be..8516f12 100644 --- a/googletrans/client.py +++ b/googletrans/client.py @@ -10,7 +10,8 @@ import typing import httpx -from httpx import Timeout +from httpx import Response, Timeout +from httpx._types import ProxiesTypes from googletrans import urls, utils from googletrans.constants import ( @@ -58,21 +59,19 @@ class Translator: def __init__( self, - service_urls=DEFAULT_CLIENT_SERVICE_URLS, - user_agent=DEFAULT_USER_AGENT, - raise_exception=DEFAULT_RAISE_EXCEPTION, - proxies: typing.Optional[ - typing.Dict[typing.Union[str, httpx.URL], typing.Union[str, httpx.Proxy]] - ] = None, + service_urls: typing.Sequence[str] = DEFAULT_CLIENT_SERVICE_URLS, + user_agent: str = DEFAULT_USER_AGENT, + raise_exception: bool = DEFAULT_RAISE_EXCEPTION, + proxies: typing.Optional[ProxiesTypes] = None, timeout: typing.Optional[Timeout] = None, - http2=True, + http2: bool = True, ): - self.client = httpx.Client(http2=http2, proxies=proxies) - - self.client.headers.update( - { + self.client = httpx.AsyncClient( + http2=http2, + proxies=proxies, + headers={ "User-Agent": user_agent, - } + }, ) self.service_urls = ["translate.google.com"] @@ -102,15 +101,21 @@ def __init__( self.raise_exception = raise_exception - def _pick_service_url(self): + def _pick_service_url(self) -> str: if len(self.service_urls) == 1: return self.service_urls[0] return random.choice(self.service_urls) - def _translate(self, text, dest, src, override): + async def _translate( + self, + text: str, + dest: str, + src: str, + override: typing.Dict[str, typing.Any] + ) -> typing.Tuple[typing.List[typing.Any], Response]: token = "xxxx" # dummy default value here as it is not used by api client if self.client_type == "webapp": - token = self.token_acquirer.do(text) + token = await self.token_acquirer.do(text) params = utils.build_params( client=self.client_type, @@ -122,7 +127,7 @@ def _translate(self, text, dest, src, override): ) url = urls.TRANSLATE.format(host=self._pick_service_url()) - r = self.client.get(url, params=params) + r = await self.client.get(url, params=params) if r.status_code == 200: data = utils.format_json(r.text) @@ -138,7 +143,35 @@ def _translate(self, text, dest, src, override): DUMMY_DATA[0][0][0] = text return DUMMY_DATA, r - def _parse_extra_data(self, data): + def build_request( + self, + text: str, + dest: str, + src: str, + override: typing.Dict[str, typing.Any] + ) -> httpx.Request: + """Async helper for making the translation request""" + token = "xxxx" # dummy default value here as it is not used by api client + if self.client_type == "webapp": + token = self.token_acquirer.do(text) + + params = utils.build_params( + client=self.client_type, + query=text, + src=src, + dest=dest, + token=token, + override=override, + ) + + url = urls.TRANSLATE.format(host=self._pick_service_url()) + + return self.client.build_request("GET", url, params=params) + + def _parse_extra_data( + self, + data: typing.List[typing.Any] + ) -> typing.Dict[str, typing.Any]: response_parts_name_mapping = { 0: "translation", 1: "all-translations", @@ -162,7 +195,13 @@ def _parse_extra_data(self, data): return extra - def translate(self, text, dest="en", src="auto", **kwargs): + async def translate( + self, + text: typing.Union[str, typing.List[str]], + dest: str = "en", + src: str = "auto", + **kwargs: typing.Any, + ) -> typing.Union[Translated, typing.List[Translated]]: """Translate text from source language to destination language :param text: The source text(s) to be translated. Batch translation is supported via sequence input. @@ -223,12 +262,12 @@ def translate(self, text, dest="en", src="auto", **kwargs): if isinstance(text, list): result = [] for item in text: - translated = self.translate(item, dest=dest, src=src, **kwargs) + translated = await self.translate(item, dest=dest, src=src, **kwargs) result.append(translated) return result origin = text - data, response = self._translate(text, dest, src, kwargs) + data, response = await self._translate(text, dest, src, kwargs) # this code will be updated when the format is changed. translated = "".join([d[0] if d[0] else "" for d in data[0]]) @@ -270,7 +309,11 @@ def translate(self, text, dest="en", src="auto", **kwargs): return result - def detect(self, text, **kwargs): + async def detect( + self, + text: typing.Union[str, typing.List[str]], + **kwargs: typing.Any + ) -> typing.Union[Detected, typing.List[Detected]]: """Detect language of the input text :param text: The source text(s) whose language you want to identify. @@ -308,7 +351,7 @@ def detect(self, text, **kwargs): result.append(lang) return result - data, response = self._translate(text, "en", "auto", kwargs) + data, response = await self._translate(text, "en", "auto", kwargs) # actual source language that will be recognized by Google Translator when the # src passed is equal to auto. From e197495fec9ed96f3f2f0d6eab4ec43ab01560f8 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Wed, 20 Nov 2024 23:04:21 +0900 Subject: [PATCH 06/16] fix: correctly typed --- googletrans/client.py | 42 ++++++++------ tests/__init__.py | 0 tests/test_client.py | 131 +++++++++++++++++++++++------------------- tests/test_gtoken.py | 65 +++++++++++---------- 4 files changed, 134 insertions(+), 104 deletions(-) create mode 100644 tests/__init__.py diff --git a/googletrans/client.py b/googletrans/client.py index 8516f12..ae50637 100644 --- a/googletrans/client.py +++ b/googletrans/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ A Translation module. @@ -107,11 +106,7 @@ def _pick_service_url(self) -> str: return random.choice(self.service_urls) async def _translate( - self, - text: str, - dest: str, - src: str, - override: typing.Dict[str, typing.Any] + self, text: str, dest: str, src: str, override: typing.Dict[str, typing.Any] ) -> typing.Tuple[typing.List[typing.Any], Response]: token = "xxxx" # dummy default value here as it is not used by api client if self.client_type == "webapp": @@ -144,11 +139,7 @@ async def _translate( return DUMMY_DATA, r def build_request( - self, - text: str, - dest: str, - src: str, - override: typing.Dict[str, typing.Any] + self, text: str, dest: str, src: str, override: typing.Dict[str, typing.Any] ) -> httpx.Request: """Async helper for making the translation request""" token = "xxxx" # dummy default value here as it is not used by api client @@ -169,8 +160,7 @@ def build_request( return self.client.build_request("GET", url, params=params) def _parse_extra_data( - self, - data: typing.List[typing.Any] + self, data: typing.List[typing.Any] ) -> typing.Dict[str, typing.Any]: response_parts_name_mapping = { 0: "translation", @@ -195,6 +185,20 @@ def _parse_extra_data( return extra + @typing.overload + async def translate( + self, text: str, dest: str = ..., src: str = ..., **kwargs: typing.Any + ) -> Translated: ... + + @typing.overload + async def translate( + self, + text: typing.List[str], + dest: str = ..., + src: str = ..., + **kwargs: typing.Any, + ) -> typing.List[Translated]: ... + async def translate( self, text: typing.Union[str, typing.List[str]], @@ -309,10 +313,16 @@ async def translate( return result + @typing.overload + async def detect(self, text: str, **kwargs: typing.Any) -> Detected: ... + + @typing.overload async def detect( - self, - text: typing.Union[str, typing.List[str]], - **kwargs: typing.Any + self, text: typing.List[str], **kwargs: typing.Any + ) -> typing.List[Detected]: ... + + async def detect( + self, text: typing.Union[str, typing.List[str]], **kwargs: typing.Any ) -> typing.Union[Detected, typing.List[Detected]]: """Detect language of the input text diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_client.py b/tests/test_client.py index 035c609..9fde14f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,13 +1,16 @@ +from unittest.mock import patch + +import pytest from httpcore import TimeoutException from httpcore._exceptions import ConnectError -from httpx import Timeout, Client, ConnectTimeout -from unittest.mock import patch +from httpx import Client, ConnectTimeout, Timeout from pytest import raises from googletrans import Translator -def test_bind_multiple_service_urls(): +@pytest.mark.asyncio +async def test_bind_multiple_service_urls(): service_urls = [ "translate.google.com", "translate.google.co.kr", @@ -16,87 +19,97 @@ def test_bind_multiple_service_urls(): translator = Translator(service_urls=service_urls) assert translator.service_urls == service_urls - assert translator.translate("test", dest="ko") - assert translator.detect("Hello") + assert await translator.translate("test", dest="ko") + assert await translator.detect("Hello") -def test_api_service_urls(): +@pytest.mark.asyncio +async def test_api_service_urls(): service_urls = ["translate.googleapis.com"] translator = Translator(service_urls=service_urls) assert translator.service_urls == service_urls - assert translator.translate("test", dest="ko") - assert translator.detect("Hello") + assert await translator.translate("test", dest="ko") + assert await translator.detect("Hello") -def test_source_language(translator): - result = translator.translate("안녕하세요.") +@pytest.mark.asyncio +async def test_source_language(translator: Translator): + result = await translator.translate("안녕하세요.") assert result.src == "ko" -def test_pronunciation(translator): - result = translator.translate("안녕하세요.", dest="ja") +@pytest.mark.asyncio +async def test_pronunciation(translator: Translator): + result = await translator.translate("안녕하세요.", dest="ja") assert result.pronunciation == "Kon'nichiwa." -def test_pronunciation_issue_175(translator): - result = translator.translate("Hello", src="en", dest="ru") - +@pytest.mark.asyncio +async def test_pronunciation_issue_175(translator: Translator): + result = await translator.translate("Hello", src="en", dest="ru") assert result.pronunciation is not None -def test_latin_to_english(translator): - result = translator.translate("veritas lux mea", src="la", dest="en") +@pytest.mark.asyncio +async def test_latin_to_english(translator: Translator): + result = await translator.translate("veritas lux mea", src="la", dest="en") assert result.text == "truth is my light" -def test_unicode(translator): - result = translator.translate("안녕하세요.", src="ko", dest="ja") +@pytest.mark.asyncio +async def test_unicode(translator: Translator): + result = await translator.translate("안녕하세요.", src="ko", dest="ja") assert result.text == "こんにちは。" -def test_emoji(translator): - result = translator.translate("😀") +@pytest.mark.asyncio +async def test_emoji(translator: Translator): + result = await translator.translate("😀") assert result.text == "😀" -def test_language_name(translator): - result = translator.translate("Hello", src="ENGLISH", dest="iRiSh") +@pytest.mark.asyncio +async def test_language_name(translator: Translator): + result = await translator.translate("Hello", src="ENGLISH", dest="iRiSh") assert result.text == "Dia duit" -def test_language_name_with_space(translator): - result = translator.translate("Hello", src="en", dest="chinese (simplified)") +@pytest.mark.asyncio +async def test_language_name_with_space(translator: Translator): + result = await translator.translate("Hello", src="en", dest="chinese (simplified)") assert result.dest == "zh-cn" -def test_language_rfc1766(translator): - result = translator.translate("luna", src="it_ch@euro", dest="en") +@pytest.mark.asyncio +async def test_language_rfc1766(translator: Translator): + result = await translator.translate("luna", src="it_ch@euro", dest="en") assert result.text == "moon" -def test_special_chars(translator): +@pytest.mark.asyncio +async def test_special_chars(translator: Translator): text = "©×《》" - - result = translator.translate(text, src="en", dest="en") + result = await translator.translate(text, src="en", dest="en") assert result.text == text -def test_translate_list(translator): +@pytest.mark.asyncio +async def test_translate_list(translator: Translator): args = (["test", "exam", "exam paper"], "ko", "en") - translations = translator.translate(*args) - + translations = await translator.translate(*args) assert translations[0].text == "시험" assert translations[1].text == "시험" assert translations[2].text == "시험지" -def test_detect_language(translator): - ko = translator.detect("한국어") - en = translator.detect("English") - rubg = translator.detect("тест") - russ = translator.detect("привет") +@pytest.mark.asyncio +async def test_detect_language(translator: Translator): + ko = await translator.detect("한국어") + en = await translator.detect("English") + rubg = await translator.detect("тест") + russ = await translator.detect("привет") assert ko.lang == "ko" assert en.lang == "en" @@ -105,10 +118,10 @@ def test_detect_language(translator): #'bg'] -def test_detect_list(translator): +@pytest.mark.asyncio +async def test_detect_list(translator: Translator): items = ["한국어", " English", "тест", "привет"] - - result = translator.detect(items) + result = await translator.detect(items) assert result[0].lang == "ko" assert result[1].lang == "en" @@ -116,41 +129,40 @@ def test_detect_list(translator): assert result[3].lang == "ru" -def test_src_in_special_cases(translator): +@pytest.mark.asyncio +async def test_src_in_special_cases(translator: Translator): args = ("tere", "en", "ee") - - result = translator.translate(*args) - + result = await translator.translate(*args) assert result.text in ("hello", "hi,") -def test_src_not_in_supported_languages(translator): +@pytest.mark.asyncio +async def test_src_not_in_supported_languages(translator: Translator): args = ("Hello", "en", "zzz") - with raises(ValueError): - translator.translate(*args) + await translator.translate(*args) -def test_dest_in_special_cases(translator): +@pytest.mark.asyncio +async def test_dest_in_special_cases(translator: Translator): args = ("hello", "ee", "en") - - result = translator.translate(*args) - + result = await translator.translate(*args) assert result.text == "tere" -def test_dest_not_in_supported_languages(translator): +@pytest.mark.asyncio +async def test_dest_not_in_supported_languages(translator: Translator): args = ("Hello", "zzz", "en") - with raises(ValueError): - translator.translate(*args) + await translator.translate(*args) -def test_timeout(): +@pytest.mark.asyncio +async def test_timeout(): # httpx will raise ConnectError in some conditions with raises((TimeoutException, ConnectError, ConnectTimeout)): translator = Translator(timeout=Timeout(0.0001)) - translator.translate("안녕하세요.") + await translator.translate("안녕하세요.") class MockResponse: @@ -159,7 +171,8 @@ def __init__(self, status_code): self.text = "tkk:'translation'" +@pytest.mark.asyncio @patch.object(Client, "get", return_value=MockResponse("403")) -def test_403_error(session_mock): +async def test_403_error(session_mock): translator = Translator() - assert translator.translate("test", dest="ko") + assert await translator.translate("test", dest="ko") diff --git a/tests/test_gtoken.py b/tests/test_gtoken.py index 258a400..37fd3b4 100644 --- a/tests/test_gtoken.py +++ b/tests/test_gtoken.py @@ -1,67 +1,74 @@ -# -*- coding: utf-8 -*- +from typing import Any, Callable + import httpx +import pytest from googletrans import gtoken -from pytest import fixture -@fixture(scope="session") -def acquirer(): - client = httpx.Client(http2=True) +@pytest.fixture(scope="session") +def acquirer() -> gtoken.TokenAcquirer: + client = httpx.AsyncClient(http2=True) return gtoken.TokenAcquirer(client=client) -def test_acquire_token(acquirer): - text = "test" +@pytest.mark.asyncio +async def test_acquire_token(acquirer: gtoken.TokenAcquirer) -> None: + text: str = "test" - result = acquirer.do(text) + result: str = await acquirer.do(text) assert result -def test_acquire_token_ascii_less_than_2048(acquirer): - text = "Ѐ" +@pytest.mark.asyncio +async def test_acquire_token_ascii_less_than_2048( + acquirer: gtoken.TokenAcquirer, +) -> None: + text: str = "Ѐ" - result = acquirer.do(text) + result: str = await acquirer.do(text) assert result -def test_acquire_token_ascii_matches_special_condition(acquirer): - def unichar(i): - try: - return unichr(i) - except NameError: - return chr(i) +@pytest.mark.asyncio +async def test_acquire_token_ascii_matches_special_condition( + acquirer: gtoken.TokenAcquirer, +) -> None: + def unichar(i: int) -> str: + return chr(i) - text = unichar(55296) + unichar(56320) + text: str = unichar(55296) + unichar(56320) - result = acquirer.do(text) + result: str = await acquirer.do(text) assert result -def test_acquire_token_ascii_else(acquirer): - text = "가" +@pytest.mark.asyncio +async def test_acquire_token_ascii_else(acquirer: gtoken.TokenAcquirer) -> None: + text: str = "가" - result = acquirer.do(text) + result: str = await acquirer.do(text) assert result -def test_reuse_valid_token(acquirer): - text = "test" +@pytest.mark.asyncio +async def test_reuse_valid_token(acquirer: gtoken.TokenAcquirer) -> None: + text: str = "test" - first = acquirer.do(text) - second = acquirer.do(text) + first: str = await acquirer.do(text) + second: str = await acquirer.do(text) assert first == second -def test_map_lazy_return(acquirer): - value = True +def test_map_lazy_return(acquirer: gtoken.TokenAcquirer) -> None: + value: bool = True - func = acquirer._lazy(value) + func: Callable[[], Any] = acquirer._lazy(value) assert callable(func) assert func() == value From 780aab1ae8221042abbe7f1c7b2bf5154dbf4675 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Wed, 20 Nov 2024 23:04:34 +0900 Subject: [PATCH 07/16] chore: add pytest-asyncio as dev dep --- pyproject.toml | 4 +- uv.lock | 501 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index d49558b..19158b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,10 +24,10 @@ classifiers = [ ] [project.optional-dependencies] -dev = ["pytest", "coveralls", "ruff>=0.7"] +dev = ["pytest", "pytest-asyncio", "coveralls", "ruff>=0.7"] [tool.uv] -dev-dependencies = ["ruff>=0.7"] +dev-dependencies = ["pytest", "pytest-asyncio", "ruff>=0.7"] [project.scripts] translate = "googletrans:translate" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8689fc0 --- /dev/null +++ b/uv.lock @@ -0,0 +1,501 @@ +version = 1 +requires-python = ">=3.8" + +[[package]] +name = "anyio" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, + { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, + { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, + { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, + { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, + { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, + { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, + { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, + { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, + { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, + { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, + { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, + { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, + { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/86/f4/ccab93e631e7293cca82f9f7ba39783c967f823a0000df2d8dd743cad74f/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", size = 193961 }, + { url = "https://files.pythonhosted.org/packages/94/d4/2b21cb277bac9605026d2d91a4a8872bc82199ed11072d035dc674c27223/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", size = 124507 }, + { url = "https://files.pythonhosted.org/packages/9a/e0/a7c1fcdff20d9c667342e0391cfeb33ab01468d7d276b2c7914b371667cc/charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", size = 119298 }, + { url = "https://files.pythonhosted.org/packages/70/de/1538bb2f84ac9940f7fa39945a5dd1d22b295a89c98240b262fc4b9fcfe0/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", size = 139328 }, + { url = "https://files.pythonhosted.org/packages/e9/ca/288bb1a6bc2b74fb3990bdc515012b47c4bc5925c8304fc915d03f94b027/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", size = 149368 }, + { url = "https://files.pythonhosted.org/packages/aa/75/58374fdaaf8406f373e508dab3486a31091f760f99f832d3951ee93313e8/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", size = 141944 }, + { url = "https://files.pythonhosted.org/packages/32/c8/0bc558f7260db6ffca991ed7166494a7da4fda5983ee0b0bfc8ed2ac6ff9/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", size = 143326 }, + { url = "https://files.pythonhosted.org/packages/0e/dd/7f6fec09a1686446cee713f38cf7d5e0669e0bcc8288c8e2924e998cf87d/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", size = 146171 }, + { url = "https://files.pythonhosted.org/packages/4c/a8/440f1926d6d8740c34d3ca388fbd718191ec97d3d457a0677eb3aa718fce/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", size = 139711 }, + { url = "https://files.pythonhosted.org/packages/e9/7f/4b71e350a3377ddd70b980bea1e2cc0983faf45ba43032b24b2578c14314/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", size = 148348 }, + { url = "https://files.pythonhosted.org/packages/1e/70/17b1b9202531a33ed7ef41885f0d2575ae42a1e330c67fddda5d99ad1208/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", size = 151290 }, + { url = "https://files.pythonhosted.org/packages/44/30/574b5b5933d77ecb015550aafe1c7d14a8cd41e7e6c4dcea5ae9e8d496c3/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", size = 149114 }, + { url = "https://files.pythonhosted.org/packages/0b/11/ca7786f7e13708687443082af20d8341c02e01024275a28bc75032c5ce5d/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", size = 143856 }, + { url = "https://files.pythonhosted.org/packages/f9/c2/1727c1438256c71ed32753b23ec2e6fe7b6dff66a598f6566cfe8139305e/charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", size = 94333 }, + { url = "https://files.pythonhosted.org/packages/09/c8/0e17270496a05839f8b500c1166e3261d1226e39b698a735805ec206967b/charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", size = 101454 }, + { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, + { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, + { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, + { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, + { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, + { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, + { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, + { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, + { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, + { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, + { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, + { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, + { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, + { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, + { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "coveralls" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "docopt" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/75/a454fb443eb6a053833f61603a432ffbd7dd6ae53a11159bacfadb9d6219/coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69", size = 12419 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/e5/6708c75e2a4cfca929302d4d9b53b862c6dc65bd75e6933ea3d20016d41d/coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809", size = 13599 }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901 } + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "h2" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/32/fec683ddd10629ea4ea46d206752a95a2d8a48c22521edd70b142488efe1/h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb", size = 2145593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e5/db6d438da759efbb488c4f3fbdab7764492ff3c3f953132efa6b9f0e9e53/h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d", size = 57488 }, +] + +[[package]] +name = "hpack" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/fda93fb4d957db19b0f6b370e79d586b3e8528b20252c729c476a2c02954/hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095", size = 49117 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "hyperframe" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/2a/4747bff0a17f7281abe73e955d60d80aae537a5d203f417fa1c2e7578ebb/hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914", size = 25008 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/de/85a784bcc4a3779d1753a7ec2dee5de90e18c7bcf402e71b51fcf150b129/hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", size = 12389 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "py-googletrans" +version = "4.0.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx", extra = ["http2"] }, +] + +[package.optional-dependencies] +dev = [ + { name = "coveralls" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "coveralls", marker = "extra == 'dev'" }, + { name = "httpx", extras = ["http2"], specifier = ">=0.27.2" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-asyncio", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff", specifier = ">=0.7" }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "ruff" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, + { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, + { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, + { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, + { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, + { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, + { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, + { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, + { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, + { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, + { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, + { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, + { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, + { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, + { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, + { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "tomli" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/e4/1b6cbcc82d8832dd0ce34767d5c560df8a3547ad8cbc427f34601415930a/tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", size = 16622 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f7/4da0ffe1892122c9ea096c57f64c2753ae5dd3ce85488802d11b0992cc6d/tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391", size = 13750 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] From 9d7fea0adaf501894c81706b033f11b5106f2f1f Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Wed, 20 Nov 2024 23:18:18 +0900 Subject: [PATCH 08/16] refactor: convert to f-string --- googletrans/client.py | 6 ++++-- googletrans/models.py | 15 ++++--------- translate | 49 ++++++++++++++++++++++++------------------- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/googletrans/client.py b/googletrans/client.py index ae50637..abab067 100644 --- a/googletrans/client.py +++ b/googletrans/client.py @@ -126,6 +126,8 @@ async def _translate( if r.status_code == 200: data = utils.format_json(r.text) + if not isinstance(data, list): + data = [data] # Convert dict to list to match return type return data, r if self.raise_exception: @@ -138,13 +140,13 @@ async def _translate( DUMMY_DATA[0][0][0] = text return DUMMY_DATA, r - def build_request( + async def build_request( self, text: str, dest: str, src: str, override: typing.Dict[str, typing.Any] ) -> httpx.Request: """Async helper for making the translation request""" token = "xxxx" # dummy default value here as it is not used by api client if self.client_type == "webapp": - token = self.token_acquirer.do(text) + token = await self.token_acquirer.do(text) params = utils.build_params( client=self.client_type, diff --git a/googletrans/models.py b/googletrans/models.py index c9c8e4c..b9baed4 100644 --- a/googletrans/models.py +++ b/googletrans/models.py @@ -41,14 +41,9 @@ def __str__(self): # pragma: nocover def __unicode__(self): # pragma: nocover return ( - "Translated(src={src}, dest={dest}, text={text}, pronunciation={pronunciation}, " - "extra_data={extra_data})".format( - src=self.src, - dest=self.dest, - text=self.text, - pronunciation=self.pronunciation, - extra_data='"' + repr(self.extra_data)[:10] + '..."', - ) + f"Translated(src={self.src}, dest={self.dest}, text={self.text}, " + f"pronunciation={self.pronunciation}, " + f'extra_data="{repr(self.extra_data)[:10]}...")' ) @@ -68,6 +63,4 @@ def __str__(self): # pragma: nocover return self.__unicode__() def __unicode__(self): # pragma: nocover - return "Detected(lang={lang}, confidence={confidence})".format( - lang=self.lang, confidence=self.confidence - ) + return f"Detected(lang={self.lang}, confidence={self.confidence})" diff --git a/translate b/translate index 7561584..62308b8 100755 --- a/translate +++ b/translate @@ -1,40 +1,47 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import argparse -import sys + from googletrans import Translator + def main(): parser = argparse.ArgumentParser( - description='Python Google Translator as a command-line tool') - parser.add_argument('text', help='The text you want to translate.') - parser.add_argument('-d', '--dest', default='en', - help='The destination language you want to translate. (Default: en)') - parser.add_argument('-s', '--src', default='auto', - help='The source language you want to translate. (Default: auto)') - parser.add_argument('-c', '--detect', action='store_true', default=False, - help='') + description="Python Google Translator as a command-line tool" + ) + parser.add_argument("text", help="The text you want to translate.") + parser.add_argument( + "-d", + "--dest", + default="en", + help="The destination language you want to translate. (Default: en)", + ) + parser.add_argument( + "-s", + "--src", + default="auto", + help="The source language you want to translate. (Default: auto)", + ) + parser.add_argument("-c", "--detect", action="store_true", default=False, help="") args = parser.parse_args() translator = Translator() if args.detect: result = translator.detect(args.text) - result = """ -[{lang}, {confidence}] {text} - """.strip().format(text=args.text, - lang=result.lang, confidence=result.confidence) + result = f""" +[{result.lang}, {result.confidence}] {args.text} + """.strip() print(result) return result = translator.translate(args.text, dest=args.dest, src=args.src) - result = u""" -[{src}] {original} + result = f""" +[{result.src}] {result.origin} -> -[{dest}] {text} -[pron.] {pronunciation} - """.strip().format(src=result.src, dest=result.dest, original=result.origin, - text=result.text, pronunciation=result.pronunciation) +[{result.dest}] {result.text} +[pron.] {result.pronunciation} + """.strip() print(result) -if __name__ == '__main__': + +if __name__ == "__main__": main() From 2c8c16529652359c6f7722f18e695c5aff8af974 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Sat, 23 Nov 2024 02:58:10 +0900 Subject: [PATCH 09/16] fix: pronunciation --- googletrans/client.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/googletrans/client.py b/googletrans/client.py index abab067..3284acc 100644 --- a/googletrans/client.py +++ b/googletrans/client.py @@ -289,16 +289,17 @@ async def translate( pron = origin try: - pron = data[0][1][-2] + # Get pronunciation from [0][1][3] which contains romanized pronunciation + if data[0][1] and len(data[0][1]) > 3: + pron = data[0][1][3] + # Fallback to previous methods if not found + elif data[0][1] and len(data[0][1]) > 2: + pron = data[0][1][2] + elif data[0][1] and len(data[0][1]) >= 2: + pron = data[0][1][-2] except Exception: # pragma: nocover pass - if pron is None: - try: - pron = data[0][1][2] - except: # pragma: nocover # noqa: E722 - pass - if dest in EXCLUDES and pron == origin: pron = translated From 9880a8046f8ec5ed2c1ef73929fe1aaa87fe7e41 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Sat, 23 Nov 2024 13:53:20 +0900 Subject: [PATCH 10/16] chore: remove Pipfile --- Pipfile | 16 -- Pipfile.lock | 646 --------------------------------------------------- 2 files changed, 662 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 96b45f2..0000000 --- a/Pipfile +++ /dev/null @@ -1,16 +0,0 @@ -[[source]] -url = "https://pypi.python.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -httpx = {extras = ["http2"], version = "~=0.23"} - -[dev-packages] -coveralls = "*" -"pytest-watch" = "*" -"pytest-testmon" = "*" -sphinx = "*" - -[requires] -python_version = "^3.6" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 2680f98..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,646 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "d68ab844e5920866556b2fd9dc32456520ce2fc110115b0ebbeb136a6038b5e1" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "^3.6" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "anyio": { - "hashes": [ - "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9", - "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78" - ], - "markers": "python_version >= '3.8'", - "version": "==4.5.0" - }, - "certifi": { - "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.8.30" - }, - "exceptiongroup": { - "hashes": [ - "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", - "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" - ], - "markers": "python_version < '3.11'", - "version": "==1.2.2" - }, - "h11": { - "hashes": [ - "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", - "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" - ], - "markers": "python_version >= '3.7'", - "version": "==0.14.0" - }, - "h2": { - "hashes": [ - "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d", - "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb" - ], - "version": "==4.1.0" - }, - "hpack": { - "hashes": [ - "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", - "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==4.0.0" - }, - "httpcore": { - "hashes": [ - "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", - "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" - ], - "markers": "python_version >= '3.8'", - "version": "==1.0.5" - }, - "httpx": { - "extras": [ - "http2" - ], - "hashes": [ - "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", - "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2" - ], - "markers": "python_version >= '3.8'", - "version": "==0.27.2" - }, - "hyperframe": { - "hashes": [ - "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", - "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==6.0.1" - }, - "idna": { - "hashes": [ - "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", - "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" - ], - "markers": "python_version >= '3.6'", - "version": "==3.10" - }, - "sniffio": { - "hashes": [ - "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", - "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" - ], - "markers": "python_version < '3.11'", - "version": "==4.12.2" - } - }, - "develop": { - "alabaster": { - "hashes": [ - "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", - "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92" - ], - "markers": "python_version >= '3.9'", - "version": "==0.7.16" - }, - "babel": { - "hashes": [ - "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", - "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316" - ], - "markers": "python_version >= '3.8'", - "version": "==2.16.0" - }, - "certifi": { - "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.8.30" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.4.6" - }, - "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", - "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", - "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", - "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", - "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", - "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", - "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", - "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", - "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", - "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", - "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", - "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", - "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", - "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", - "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", - "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", - "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", - "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", - "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", - "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", - "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", - "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", - "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", - "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", - "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", - "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", - "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", - "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", - "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", - "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", - "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", - "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", - "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", - "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", - "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", - "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", - "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", - "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", - "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", - "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", - "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", - "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", - "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", - "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", - "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", - "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", - "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", - "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", - "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", - "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", - "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", - "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", - "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", - "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", - "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", - "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", - "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", - "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", - "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", - "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", - "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", - "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", - "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", - "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", - "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", - "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", - "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", - "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", - "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", - "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", - "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", - "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc" - ], - "markers": "python_version >= '3.8'", - "version": "==7.6.1" - }, - "coveralls": { - "hashes": [ - "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809", - "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69" - ], - "index": "pypi", - "markers": "python_version < '3.13' and python_version >= '3.8'", - "version": "==4.0.1" - }, - "docopt": { - "hashes": [ - "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" - ], - "version": "==0.6.2" - }, - "docutils": { - "hashes": [ - "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", - "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" - ], - "markers": "python_version >= '3.9'", - "version": "==0.21.2" - }, - "exceptiongroup": { - "hashes": [ - "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", - "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" - ], - "markers": "python_version < '3.11'", - "version": "==1.2.2" - }, - "idna": { - "hashes": [ - "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", - "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" - ], - "markers": "python_version >= '3.6'", - "version": "==3.10" - }, - "imagesize": { - "hashes": [ - "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", - "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.1" - }, - "importlib-metadata": { - "hashes": [ - "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", - "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7" - ], - "markers": "python_version < '3.10'", - "version": "==8.5.0" - }, - "iniconfig": { - "hashes": [ - "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", - "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "jinja2": { - "hashes": [ - "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", - "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.4" - }, - "markupsafe": { - "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" - }, - "packaging": { - "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" - ], - "markers": "python_version >= '3.8'", - "version": "==24.1" - }, - "pluggy": { - "hashes": [ - "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", - "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" - ], - "markers": "python_version >= '3.8'", - "version": "==1.5.0" - }, - "pygments": { - "hashes": [ - "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", - "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" - ], - "markers": "python_version >= '3.8'", - "version": "==2.18.0" - }, - "pytest": { - "hashes": [ - "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", - "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" - ], - "markers": "python_version >= '3.8'", - "version": "==8.3.3" - }, - "pytest-testmon": { - "hashes": [ - "sha256:8271ca47bc8c80760c4fc7fd7895ea786b111bbb31f13eeea879a6fd11fe2226", - "sha256:8ebe2c3de42d99306ee54cd4536fed0fc48346a954420da904b18e8d59b5da98" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.1.1" - }, - "pytest-watch": { - "hashes": [ - "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9" - ], - "index": "pypi", - "version": "==4.2.0" - }, - "requests": { - "hashes": [ - "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" - ], - "markers": "python_version >= '3.8'", - "version": "==2.32.3" - }, - "snowballstemmer": { - "hashes": [ - "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", - "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" - ], - "version": "==2.2.0" - }, - "sphinx": { - "hashes": [ - "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", - "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==7.4.7" - }, - "sphinxcontrib-applehelp": { - "hashes": [ - "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", - "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.0" - }, - "sphinxcontrib-devhelp": { - "hashes": [ - "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", - "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.0" - }, - "sphinxcontrib-htmlhelp": { - "hashes": [ - "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", - "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9" - ], - "markers": "python_version >= '3.9'", - "version": "==2.1.0" - }, - "sphinxcontrib-jsmath": { - "hashes": [ - "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", - "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.1" - }, - "sphinxcontrib-qthelp": { - "hashes": [ - "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", - "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.0" - }, - "sphinxcontrib-serializinghtml": { - "hashes": [ - "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", - "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.0" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "urllib3": { - "hashes": [ - "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", - "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.3" - }, - "watchdog": { - "hashes": [ - "sha256:14dd4ed023d79d1f670aa659f449bcd2733c33a35c8ffd88689d9d243885198b", - "sha256:29e4a2607bd407d9552c502d38b45a05ec26a8e40cc7e94db9bb48f861fa5abc", - "sha256:3960136b2b619510569b90f0cd96408591d6c251a75c97690f4553ca88889769", - "sha256:3e8d5ff39f0a9968952cce548e8e08f849141a4fcc1290b1c17c032ba697b9d7", - "sha256:53ed1bf71fcb8475dd0ef4912ab139c294c87b903724b6f4a8bd98e026862e6d", - "sha256:5597c051587f8757798216f2485e85eac583c3b343e9aa09127a3a6f82c65ee8", - "sha256:638bcca3d5b1885c6ec47be67bf712b00a9ab3d4b22ec0881f4889ad870bc7e8", - "sha256:6bec703ad90b35a848e05e1b40bf0050da7ca28ead7ac4be724ae5ac2653a1a0", - "sha256:726eef8f8c634ac6584f86c9c53353a010d9f311f6c15a034f3800a7a891d941", - "sha256:72990192cb63872c47d5e5fefe230a401b87fd59d257ee577d61c9e5564c62e5", - "sha256:7d1aa7e4bb0f0c65a1a91ba37c10e19dabf7eaaa282c5787e51371f090748f4b", - "sha256:8c47150aa12f775e22efff1eee9f0f6beee542a7aa1a985c271b1997d340184f", - "sha256:901ee48c23f70193d1a7bc2d9ee297df66081dd5f46f0ca011be4f70dec80dab", - "sha256:963f7c4c91e3f51c998eeff1b3fb24a52a8a34da4f956e470f4b068bb47b78ee", - "sha256:9814adb768c23727a27792c77812cf4e2fd9853cd280eafa2bcfa62a99e8bd6e", - "sha256:aa9cd6e24126d4afb3752a3e70fce39f92d0e1a58a236ddf6ee823ff7dba28ee", - "sha256:b6dc8f1d770a8280997e4beae7b9a75a33b268c59e033e72c8a10990097e5fde", - "sha256:b84bff0391ad4abe25c2740c7aec0e3de316fdf7764007f41e248422a7760a7f", - "sha256:ba32efcccfe2c58f4d01115440d1672b4eb26cdd6fc5b5818f1fb41f7c3e1889", - "sha256:bda40c57115684d0216556671875e008279dea2dc00fcd3dde126ac8e0d7a2fb", - "sha256:c4a440f725f3b99133de610bfec93d570b13826f89616377715b9cd60424db6e", - "sha256:d010be060c996db725fbce7e3ef14687cdcc76f4ca0e4339a68cc4532c382a73", - "sha256:d2ab34adc9bf1489452965cdb16a924e97d4452fcf88a50b21859068b50b5c3b", - "sha256:d7594a6d32cda2b49df3fd9abf9b37c8d2f3eab5df45c24056b4a671ac661619", - "sha256:d961f4123bb3c447d9fcdcb67e1530c366f10ab3a0c7d1c0c9943050936d4877", - "sha256:dae7a1879918f6544201d33666909b040a46421054a50e0f773e0d870ed7438d", - "sha256:dcebf7e475001d2cdeb020be630dc5b687e9acdd60d16fea6bb4508e7b94cf76", - "sha256:f627c5bf5759fdd90195b0c0431f99cff4867d212a67b384442c51136a098ed7", - "sha256:f8b2918c19e0d48f5f20df458c84692e2a054f02d9df25e6c3c930063eca64c1", - "sha256:fb223456db6e5f7bd9bbd5cd969f05aae82ae21acc00643b60d81c770abd402b" - ], - "markers": "python_version >= '3.9'", - "version": "==5.0.2" - }, - "zipp": { - "hashes": [ - "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", - "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29" - ], - "markers": "python_version >= '3.8'", - "version": "==3.20.2" - } - } -} From a0eb8582c83a4127dd11e13f3e94ebef384670f9 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Sat, 23 Nov 2024 13:53:33 +0900 Subject: [PATCH 11/16] fix: test --- pytest.ini | 3 ++- tests/conftest.py | 18 +++++++++++++----- tests/test_client.py | 9 ++++----- tests/test_gtoken.py | 7 ------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/pytest.ini b/pytest.ini index 3f22e41..c8aa016 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,8 @@ [pytest] -addopts = -v +addopts = -v --asyncio-mode=auto omit = tests/* +asyncio_default_fixture_loop_scope = function [run] include = googletrans/* diff --git a/tests/conftest.py b/tests/conftest.py index 4042b17..c2b2637 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,16 @@ -from pytest import fixture +import httpx +import pytest +from googletrans import Translator, gtoken -@fixture(scope="session") -def translator(): - from googletrans import Translator - return Translator() +@pytest.fixture(scope="function") +async def translator(): + async with Translator() as t: + yield t + + +@pytest.fixture(scope="function") +async def acquirer(): + async with httpx.AsyncClient(http2=True) as client: + yield gtoken.TokenAcquirer(client=client) diff --git a/tests/test_client.py b/tests/test_client.py index 9fde14f..309af1a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -108,24 +108,23 @@ async def test_translate_list(translator: Translator): async def test_detect_language(translator: Translator): ko = await translator.detect("한국어") en = await translator.detect("English") - rubg = await translator.detect("тест") + rubg = await translator.detect("летóво") russ = await translator.detect("привет") assert ko.lang == "ko" assert en.lang == "en" - assert rubg.lang == "mk" + assert rubg.lang == "bg" assert russ.lang == "ru" - #'bg'] @pytest.mark.asyncio async def test_detect_list(translator: Translator): - items = ["한국어", " English", "тест", "привет"] + items = ["한국어", " English", "летóво", "привет"] result = await translator.detect(items) assert result[0].lang == "ko" assert result[1].lang == "en" - assert result[2].lang == "mk" + assert result[2].lang == "bg" assert result[3].lang == "ru" diff --git a/tests/test_gtoken.py b/tests/test_gtoken.py index 37fd3b4..6efd8a7 100644 --- a/tests/test_gtoken.py +++ b/tests/test_gtoken.py @@ -1,17 +1,10 @@ from typing import Any, Callable -import httpx import pytest from googletrans import gtoken -@pytest.fixture(scope="session") -def acquirer() -> gtoken.TokenAcquirer: - client = httpx.AsyncClient(http2=True) - return gtoken.TokenAcquirer(client=client) - - @pytest.mark.asyncio async def test_acquire_token(acquirer: gtoken.TokenAcquirer) -> None: text: str = "test" From 15ecbc07173cbcf51157d9adf27dbc53a6ddf68f Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Sat, 23 Nov 2024 13:54:10 +0900 Subject: [PATCH 12/16] fix: support async context, fix list ops, and fix pronunciation extraction --- googletrans/client.py | 56 ++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/googletrans/client.py b/googletrans/client.py index 3284acc..31d9735 100644 --- a/googletrans/client.py +++ b/googletrans/client.py @@ -4,6 +4,7 @@ You can translate text using this module. """ +import asyncio import random import re import typing @@ -64,6 +65,7 @@ def __init__( proxies: typing.Optional[ProxiesTypes] = None, timeout: typing.Optional[Timeout] = None, http2: bool = True, + list_operation_max_concurrency: int = 2, ): self.client = httpx.AsyncClient( http2=http2, @@ -86,7 +88,7 @@ def __init__( # default way of working: use the defined values from user app self.service_urls = service_urls self.client_type = "webapp" - self.tok1en_acquirer = TokenAcquirer( + self.token_acquirer = TokenAcquirer( client=self.client, host=self.service_urls[0] ) @@ -99,12 +101,19 @@ def __init__( break self.raise_exception = raise_exception + self.list_operation_max_concurrency = list_operation_max_concurrency def _pick_service_url(self) -> str: if len(self.service_urls) == 1: return self.service_urls[0] return random.choice(self.service_urls) + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.client.aclose() + async def _translate( self, text: str, dest: str, src: str, override: typing.Dict[str, typing.Any] ) -> typing.Tuple[typing.List[typing.Any], Response]: @@ -266,10 +275,17 @@ async def translate( raise ValueError("invalid destination language") if isinstance(text, list): - result = [] - for item in text: - translated = await self.translate(item, dest=dest, src=src, **kwargs) - result.append(translated) + concurrency_limit = kwargs.pop( + "list_operation_max_concurrency", self.list_operation_max_concurrency + ) + semaphore = asyncio.Semaphore(concurrency_limit) + + async def translate_with_semaphore(item): + async with semaphore: + return await self.translate(item, dest=dest, src=src, **kwargs) + + tasks = [translate_with_semaphore(item) for item in text] + result = await asyncio.gather(*tasks) return result origin = text @@ -289,17 +305,16 @@ async def translate( pron = origin try: - # Get pronunciation from [0][1][3] which contains romanized pronunciation - if data[0][1] and len(data[0][1]) > 3: - pron = data[0][1][3] - # Fallback to previous methods if not found - elif data[0][1] and len(data[0][1]) > 2: - pron = data[0][1][2] - elif data[0][1] and len(data[0][1]) >= 2: - pron = data[0][1][-2] + pron = data[0][1][-2] except Exception: # pragma: nocover pass + if pron is None: + try: + pron = data[0][1][2] + except: # pragma: nocover # noqa: E722 + pass + if dest in EXCLUDES and pron == origin: pron = translated @@ -358,10 +373,17 @@ async def detect( fr 0.043500196 """ if isinstance(text, list): - result = [] - for item in text: - lang = self.detect(item) - result.append(lang) + concurrency_limit = kwargs.pop( + "list_operation_max_concurrency", self.list_operation_max_concurrency + ) + semaphore = asyncio.Semaphore(concurrency_limit) + + async def detect_with_semaphore(item): + async with semaphore: + return await self.detect(item, **kwargs) + + tasks = [detect_with_semaphore(item) for item in text] + result = await asyncio.gather(*tasks) return result data, response = await self._translate(text, "en", "auto", kwargs) From 17b0ffdc3abde8002ce050a9bbee622ce40fc774 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Sat, 23 Nov 2024 13:56:50 +0900 Subject: [PATCH 13/16] fix(ci): test --- .github/workflows/ci.yml | 31 ++++++++++++++++++++----------- pyproject.toml | 4 ++-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 315f793..13f1398 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,19 +5,28 @@ on: - pull_request jobs: - build: + test: runs-on: ubuntu-24.04 strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v3 - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - - name: Install the project - run: uv sync --all-extras --dev - - name: Test with tox - run: uv run tox + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "requirements**.txt" + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + - name: Install dependencies + run: uv sync --all-extras --dev + - name: Run tests with coverage + run: | + uv run python -m pytest --cov=googletrans --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml + fail_ci_if_error: true diff --git a/pyproject.toml b/pyproject.toml index 19158b6..6e4efa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,10 +24,10 @@ classifiers = [ ] [project.optional-dependencies] -dev = ["pytest", "pytest-asyncio", "coveralls", "ruff>=0.7"] +dev = ["pytest", "pytest-asyncio", "pytest-cov", "ruff>=0.7"] [tool.uv] -dev-dependencies = ["pytest", "pytest-asyncio", "ruff>=0.7"] +dev-dependencies = ["pytest", "pytest-asyncio", "pytest-cov", "ruff>=0.7"] [project.scripts] translate = "googletrans:translate" From 0053cc9e7a809279b304cc7adac7eaf1e40308f2 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Sat, 23 Nov 2024 13:59:32 +0900 Subject: [PATCH 14/16] fix: codecov --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13f1398..37b7b8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: run: | uv run python -m pytest --cov=googletrans --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: ./coverage.xml - fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} From 7ce4ed4d61b791ec27bab701beb4f4b86db7232f Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Sat, 23 Nov 2024 14:11:47 +0900 Subject: [PATCH 15/16] chore: update readme --- README.rst | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index aaf2038..f953110 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ implemented Google Translate API. This uses the `Google Translate Ajax API `__ to make calls to such methods as detect and translate. -Compatible with Python 3.6+. +Compatible with Python 3.8+. For details refer to the `API Documentation `__. @@ -24,15 +24,10 @@ Features - Auto language detection - Bulk translations - Customizable service URL +- Async support - HTTP/2 support - -TODO -~~~~ - -more features are coming soon. - - Proxy support -- Internal session management (for better bulk translations) +- Complete type hints HTTP/2 support ~~~~~~~~~~~~~~ @@ -109,7 +104,7 @@ URLs are provided, it then randomly chooses a domain. Customize service URL to point to standard api ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Considering translate.google. url services use the webapp requiring a token, +Considering translate.google. url services use the webapp requiring a token, you can prefer to use the direct api than does not need any token to process. It can solve your problems of unstable token providing processes (refer to issue #234) From 574da597b7600df0f400b41e74cd7e029d37a205 Mon Sep 17 00:00:00 2001 From: Suhun Han Date: Mon, 9 Dec 2024 10:52:52 +0900 Subject: [PATCH 16/16] chore(ci): test python 3.13 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37b7b8a..fac4528 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4