2025-12-28 15:55:43 +01:00
|
|
|
#!/usr/bin/env python3
|
2026-03-13 20:41:31 +01:00
|
|
|
r"""Translator - translates words/text between languages.
|
2025-12-28 15:55:43 +01:00
|
|
|
|
2026-03-17 22:47:42 +01:00
|
|
|
This module provides translation capabilities using Argos Translate (offline).
|
2026-03-13 20:41:31 +01:00
|
|
|
|
|
|
|
|
Usage::
|
|
|
|
|
|
2026-03-17 22:47:42 +01:00
|
|
|
python -m python_pkg.word_frequency.translator \
|
2026-03-13 20:41:31 +01:00
|
|
|
--text "hello" --from en --to es
|
2025-12-28 15:55:43 +01:00
|
|
|
|
2026-03-17 22:47:42 +01:00
|
|
|
Dependencies::
|
2025-12-28 15:55:43 +01:00
|
|
|
|
2026-03-13 20:41:31 +01:00
|
|
|
pip install argostranslate
|
2025-12-28 15:55:43 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-03-13 20:41:31 +01:00
|
|
|
import logging
|
2026-03-17 22:47:42 +01:00
|
|
|
from typing import TYPE_CHECKING
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from collections.abc import Sequence
|
|
|
|
|
|
2026-03-13 20:41:31 +01:00
|
|
|
try:
|
|
|
|
|
import argostranslate.package
|
|
|
|
|
import argostranslate.translate
|
|
|
|
|
except ImportError:
|
2026-03-18 22:20:05 +01:00
|
|
|
argostranslate = None
|
2026-03-13 20:41:31 +01:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from python_pkg.word_frequency.cache import (
|
|
|
|
|
get_translation_cache,
|
|
|
|
|
)
|
|
|
|
|
except ImportError:
|
|
|
|
|
get_translation_cache = None
|
|
|
|
|
|
2026-03-17 22:47:42 +01:00
|
|
|
from python_pkg.word_frequency._translator_cli import main
|
|
|
|
|
from python_pkg.word_frequency._translator_helpers import (
|
|
|
|
|
TranslationResult,
|
|
|
|
|
_check_cuda_available,
|
|
|
|
|
_ensure_argos_installed,
|
|
|
|
|
_ensure_language_pair,
|
|
|
|
|
_init_gpu_if_available,
|
|
|
|
|
detect_language,
|
|
|
|
|
format_translations,
|
|
|
|
|
read_file,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-18 22:20:05 +01:00
|
|
|
__all__ = [
|
|
|
|
|
"TranslationResult",
|
|
|
|
|
"detect_language",
|
|
|
|
|
"download_languages",
|
|
|
|
|
"format_translations",
|
|
|
|
|
"get_available_packages",
|
|
|
|
|
"get_installed_languages",
|
|
|
|
|
"main",
|
|
|
|
|
"read_file",
|
|
|
|
|
"translate_word",
|
|
|
|
|
"translate_words",
|
|
|
|
|
"translate_words_batch",
|
|
|
|
|
]
|
|
|
|
|
|
2026-03-13 20:41:31 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
_BATCH_SIZE = 100
|
|
|
|
|
|
|
|
|
|
|
2025-12-28 15:55:43 +01:00
|
|
|
def _check_argos() -> bool:
|
|
|
|
|
"""Check if argostranslate is available."""
|
2026-03-13 20:41:31 +01:00
|
|
|
return argostranslate is not None
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_installed_languages() -> list[tuple[str, str]]:
|
|
|
|
|
"""Get list of installed languages.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
List of (code, name) tuples for installed languages.
|
|
|
|
|
"""
|
|
|
|
|
if not _check_argos():
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
languages = argostranslate.translate.get_installed_languages()
|
|
|
|
|
return [(lang.code, lang.name) for lang in languages]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_available_packages() -> list[tuple[str, str, str, str]]:
|
|
|
|
|
"""Get list of available language packages for download.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
List of (from_code, from_name, to_code, to_name) tuples.
|
|
|
|
|
"""
|
|
|
|
|
if not _check_argos():
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
argostranslate.package.update_package_index()
|
|
|
|
|
available = argostranslate.package.get_available_packages()
|
|
|
|
|
return [
|
|
|
|
|
(pkg.from_code, pkg.from_name, pkg.to_code, pkg.to_name) for pkg in available
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def download_languages(lang_codes: Sequence[str]) -> dict[str, bool]:
|
|
|
|
|
"""Download language packages for the specified languages.
|
|
|
|
|
|
|
|
|
|
Downloads packages for translation between English and the specified languages,
|
|
|
|
|
and between each pair of specified languages if available.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
lang_codes: List of language codes to download (e.g., ['en', 'es', 'pl']).
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dict mapping "from->to" to success boolean.
|
|
|
|
|
"""
|
|
|
|
|
if not _check_argos():
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
results: dict[str, bool] = {}
|
|
|
|
|
|
|
|
|
|
# Update package index
|
2026-03-13 20:41:31 +01:00
|
|
|
logger.info("Updating package index...")
|
2025-12-28 15:55:43 +01:00
|
|
|
argostranslate.package.update_package_index()
|
|
|
|
|
available = argostranslate.package.get_available_packages()
|
|
|
|
|
|
|
|
|
|
# Create a lookup for available packages
|
|
|
|
|
available_lookup: dict[tuple[str, str], object] = {}
|
|
|
|
|
for pkg in available:
|
|
|
|
|
available_lookup[(pkg.from_code, pkg.to_code)] = pkg
|
|
|
|
|
|
|
|
|
|
# Download packages for all requested language pairs
|
|
|
|
|
lang_codes_set = set(lang_codes)
|
|
|
|
|
|
|
|
|
|
for from_code in lang_codes_set:
|
|
|
|
|
for to_code in lang_codes_set:
|
|
|
|
|
if from_code == to_code:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
key = f"{from_code}->{to_code}"
|
|
|
|
|
pkg_key = (from_code, to_code)
|
|
|
|
|
|
|
|
|
|
if pkg_key in available_lookup:
|
|
|
|
|
pkg = available_lookup[pkg_key]
|
|
|
|
|
try:
|
2026-03-13 20:41:31 +01:00
|
|
|
logger.info(
|
|
|
|
|
"Downloading %s -> %s...",
|
|
|
|
|
from_code,
|
|
|
|
|
to_code,
|
|
|
|
|
)
|
2025-12-28 15:55:43 +01:00
|
|
|
argostranslate.package.install_from_path(pkg.download())
|
|
|
|
|
results[key] = True
|
2026-03-13 20:41:31 +01:00
|
|
|
logger.info(
|
|
|
|
|
" Installed %s -> %s",
|
|
|
|
|
from_code,
|
|
|
|
|
to_code,
|
|
|
|
|
)
|
|
|
|
|
except (OSError, RuntimeError, ValueError) as e:
|
2025-12-28 15:55:43 +01:00
|
|
|
results[key] = False
|
2026-03-13 20:41:31 +01:00
|
|
|
logger.info(
|
|
|
|
|
" Failed %s -> %s: %s",
|
|
|
|
|
from_code,
|
|
|
|
|
to_code,
|
|
|
|
|
e,
|
|
|
|
|
)
|
2025-12-28 15:55:43 +01:00
|
|
|
else:
|
|
|
|
|
# Package not available
|
|
|
|
|
results[key] = False
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def translate_word(
|
|
|
|
|
word: str,
|
|
|
|
|
from_lang: str,
|
|
|
|
|
to_lang: str,
|
2025-12-29 14:41:56 +01:00
|
|
|
*,
|
|
|
|
|
use_cache: bool = True,
|
2025-12-28 15:55:43 +01:00
|
|
|
) -> TranslationResult:
|
2025-12-29 14:41:56 +01:00
|
|
|
"""Translate a single word using argostranslate (offline).
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
word: The word to translate.
|
|
|
|
|
from_lang: Source language code (e.g., 'en', 'pl', 'la').
|
|
|
|
|
to_lang: Target language code.
|
2025-12-29 14:41:56 +01:00
|
|
|
use_cache: Whether to use/update translation cache.
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
TranslationResult with the translation.
|
|
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
Raises:
|
|
|
|
|
ImportError: If argostranslate is not available and cannot be installed.
|
|
|
|
|
"""
|
|
|
|
|
# Check cache first
|
2026-03-13 20:41:31 +01:00
|
|
|
if use_cache and get_translation_cache is not None:
|
|
|
|
|
cache = get_translation_cache()
|
|
|
|
|
cached = cache.get(word, from_lang, to_lang)
|
|
|
|
|
if cached is not None:
|
|
|
|
|
return TranslationResult(
|
|
|
|
|
source_word=word,
|
|
|
|
|
translated_word=cached,
|
|
|
|
|
source_lang=from_lang,
|
|
|
|
|
target_lang=to_lang,
|
|
|
|
|
success=True,
|
|
|
|
|
)
|
2025-12-28 15:55:43 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
# Ensure argos is installed (will raise if it can't be)
|
|
|
|
|
_ensure_argos_installed()
|
2025-12-28 15:55:43 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
try:
|
2026-03-13 20:41:31 +01:00
|
|
|
translated = argostranslate.translate.translate(
|
2026-03-17 22:47:42 +01:00
|
|
|
word,
|
|
|
|
|
from_lang,
|
|
|
|
|
to_lang,
|
2026-03-13 20:41:31 +01:00
|
|
|
)
|
2025-12-29 14:41:56 +01:00
|
|
|
# Cache the result
|
2026-03-13 20:41:31 +01:00
|
|
|
if use_cache and get_translation_cache is not None:
|
|
|
|
|
get_translation_cache().set(
|
2026-03-17 22:47:42 +01:00
|
|
|
word,
|
|
|
|
|
from_lang,
|
|
|
|
|
to_lang,
|
|
|
|
|
translated,
|
2026-03-13 20:41:31 +01:00
|
|
|
)
|
2025-12-29 14:41:56 +01:00
|
|
|
return TranslationResult(
|
|
|
|
|
source_word=word,
|
|
|
|
|
translated_word=translated,
|
|
|
|
|
source_lang=from_lang,
|
|
|
|
|
target_lang=to_lang,
|
|
|
|
|
success=True,
|
|
|
|
|
)
|
2026-03-13 20:41:31 +01:00
|
|
|
except (OSError, RuntimeError, ValueError, TypeError) as e:
|
2025-12-29 14:41:56 +01:00
|
|
|
return TranslationResult(
|
|
|
|
|
source_word=word,
|
|
|
|
|
translated_word="",
|
|
|
|
|
source_lang=from_lang,
|
|
|
|
|
target_lang=to_lang,
|
|
|
|
|
success=False,
|
|
|
|
|
error=str(e),
|
|
|
|
|
)
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def translate_words(
|
|
|
|
|
words: Sequence[str],
|
|
|
|
|
from_lang: str,
|
|
|
|
|
to_lang: str,
|
2025-12-29 14:41:56 +01:00
|
|
|
*,
|
|
|
|
|
use_cache: bool = True,
|
2025-12-28 15:55:43 +01:00
|
|
|
) -> list[TranslationResult]:
|
|
|
|
|
"""Translate multiple words.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
words: List of words to translate.
|
|
|
|
|
from_lang: Source language code.
|
|
|
|
|
to_lang: Target language code.
|
2025-12-29 14:41:56 +01:00
|
|
|
use_cache: Whether to use translation cache.
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
List of TranslationResult for each word.
|
|
|
|
|
"""
|
2026-01-07 22:57:42 +01:00
|
|
|
return [
|
|
|
|
|
translate_word(word, from_lang, to_lang, use_cache=use_cache) for word in words
|
|
|
|
|
]
|
2025-12-29 14:41:56 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _translate_batch_worker(
|
|
|
|
|
batch_words: list[str],
|
|
|
|
|
from_lang: str,
|
|
|
|
|
to_lang: str,
|
|
|
|
|
batch_idx: int,
|
|
|
|
|
) -> tuple[int, dict[str, str]]:
|
|
|
|
|
"""Worker function to translate a batch of words.
|
2026-01-07 22:57:42 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
Args:
|
|
|
|
|
batch_words: Words to translate in this batch.
|
|
|
|
|
from_lang: Source language code.
|
|
|
|
|
to_lang: Target language code.
|
|
|
|
|
batch_idx: Index of this batch (for ordering results).
|
2026-01-07 22:57:42 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
Returns:
|
|
|
|
|
Tuple of (batch_idx, translations dict).
|
|
|
|
|
"""
|
|
|
|
|
translations: dict[str, str] = {}
|
2026-01-07 22:57:42 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
# Batch translate by joining with newlines
|
|
|
|
|
batch_text = "\n".join(batch_words)
|
|
|
|
|
translated_batch = argostranslate.translate.translate(
|
|
|
|
|
batch_text, from_lang, to_lang
|
|
|
|
|
)
|
|
|
|
|
translated_words = translated_batch.split("\n")
|
|
|
|
|
|
|
|
|
|
# If we got the same number of translations, use them
|
|
|
|
|
if len(translated_words) == len(batch_words):
|
|
|
|
|
for word, trans in zip(batch_words, translated_words, strict=True):
|
|
|
|
|
translations[word.lower()] = trans.strip()
|
|
|
|
|
else:
|
|
|
|
|
# Fall back to individual translation for this batch
|
|
|
|
|
for word in batch_words:
|
2026-01-07 22:57:42 +01:00
|
|
|
translated = argostranslate.translate.translate(word, from_lang, to_lang)
|
2025-12-29 14:41:56 +01:00
|
|
|
translations[word.lower()] = translated
|
2026-01-07 22:57:42 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
return batch_idx, translations
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
|
2026-03-13 20:41:31 +01:00
|
|
|
def _run_batch_translation(
|
|
|
|
|
words_to_translate: list[str],
|
|
|
|
|
from_lang: str,
|
|
|
|
|
to_lang: str,
|
|
|
|
|
) -> dict[str, str]:
|
|
|
|
|
"""Translate a list of words in batches with progress logging.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
words_to_translate: Words needing translation.
|
|
|
|
|
from_lang: Source language code.
|
|
|
|
|
to_lang: Target language code.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dict mapping lowercased words to translations.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
RuntimeError: If translation fails.
|
|
|
|
|
"""
|
|
|
|
|
new_translations: dict[str, str] = {}
|
|
|
|
|
num_to_translate = len(words_to_translate)
|
|
|
|
|
|
2026-03-17 22:47:42 +01:00
|
|
|
gpu_status = " (GPU)" if _check_cuda_available() else " (CPU)"
|
2026-03-13 20:41:31 +01:00
|
|
|
logger.info(
|
|
|
|
|
"Translating %d words from %s to %s%s...",
|
|
|
|
|
num_to_translate,
|
|
|
|
|
from_lang,
|
|
|
|
|
to_lang,
|
|
|
|
|
gpu_status,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
batches = [
|
|
|
|
|
words_to_translate[i : i + _BATCH_SIZE]
|
|
|
|
|
for i in range(0, num_to_translate, _BATCH_SIZE)
|
|
|
|
|
]
|
|
|
|
|
total_batches = len(batches)
|
|
|
|
|
|
|
|
|
|
for batch_idx, batch_words in enumerate(batches):
|
|
|
|
|
words_done = min(
|
|
|
|
|
(batch_idx + 1) * _BATCH_SIZE,
|
|
|
|
|
num_to_translate,
|
|
|
|
|
)
|
|
|
|
|
pct = int(words_done / num_to_translate * 100)
|
|
|
|
|
|
|
|
|
|
logger.info(
|
2026-03-17 22:47:42 +01:00
|
|
|
" [%3d%%] Translating batch %d/%d " "(%d/%d words)...",
|
2026-03-13 20:41:31 +01:00
|
|
|
pct,
|
|
|
|
|
batch_idx + 1,
|
|
|
|
|
total_batches,
|
|
|
|
|
words_done,
|
|
|
|
|
num_to_translate,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_, batch_translations = _translate_batch_worker(
|
2026-03-17 22:47:42 +01:00
|
|
|
batch_words,
|
|
|
|
|
from_lang,
|
|
|
|
|
to_lang,
|
|
|
|
|
batch_idx,
|
2026-03-13 20:41:31 +01:00
|
|
|
)
|
|
|
|
|
new_translations.update(batch_translations)
|
|
|
|
|
|
|
|
|
|
logger.info(" Translation complete.")
|
|
|
|
|
except Exception as e:
|
2026-03-17 22:47:42 +01:00
|
|
|
msg = f"Translation failed for " f"{from_lang} -> {to_lang}: {e}"
|
2026-03-13 20:41:31 +01:00
|
|
|
raise RuntimeError(msg) from e
|
|
|
|
|
|
|
|
|
|
return new_translations
|
|
|
|
|
|
|
|
|
|
|
2025-12-28 15:55:43 +01:00
|
|
|
def translate_words_batch(
|
|
|
|
|
words: Sequence[str],
|
|
|
|
|
from_lang: str,
|
|
|
|
|
to_lang: str,
|
2025-12-29 14:41:56 +01:00
|
|
|
*,
|
|
|
|
|
use_cache: bool = True,
|
2025-12-28 15:55:43 +01:00
|
|
|
) -> list[TranslationResult]:
|
2025-12-29 14:41:56 +01:00
|
|
|
"""Translate multiple words using argostranslate (offline).
|
2025-12-28 15:55:43 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
Uses small batch translation for efficiency with frequent progress updates.
|
|
|
|
|
Requires argostranslate. Will use GPU if CUDA is available.
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
words: List of words to translate.
|
|
|
|
|
from_lang: Source language code.
|
|
|
|
|
to_lang: Target language code.
|
2025-12-29 14:41:56 +01:00
|
|
|
use_cache: Whether to use translation cache.
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
List of TranslationResult for each word.
|
2025-12-29 14:41:56 +01:00
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
ImportError: If argostranslate is not available and cannot be installed.
|
|
|
|
|
RuntimeError: If CUDA is available but GPU initialization fails.
|
2025-12-28 15:55:43 +01:00
|
|
|
"""
|
|
|
|
|
if not words:
|
|
|
|
|
return []
|
|
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
_ensure_argos_installed()
|
|
|
|
|
_init_gpu_if_available()
|
|
|
|
|
_ensure_language_pair(from_lang, to_lang)
|
2025-12-28 15:55:43 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
# Check cache for already-translated words
|
|
|
|
|
cached_results: dict[str, str] = {}
|
2026-03-13 20:41:31 +01:00
|
|
|
if use_cache and get_translation_cache is not None:
|
|
|
|
|
cache = get_translation_cache()
|
|
|
|
|
cached_results = cache.get_many(
|
2026-03-17 22:47:42 +01:00
|
|
|
list(words),
|
|
|
|
|
from_lang,
|
|
|
|
|
to_lang,
|
2026-03-13 20:41:31 +01:00
|
|
|
)
|
2025-12-29 14:41:56 +01:00
|
|
|
|
|
|
|
|
# Find words that still need translation
|
2026-03-17 22:47:42 +01:00
|
|
|
words_to_translate = [word for word in words if word.lower() not in cached_results]
|
2025-12-29 14:41:56 +01:00
|
|
|
|
|
|
|
|
# Translate uncached words using argos batch
|
|
|
|
|
new_translations: dict[str, str] = {}
|
|
|
|
|
if words_to_translate:
|
2026-03-13 20:41:31 +01:00
|
|
|
new_translations = _run_batch_translation(
|
2026-03-17 22:47:42 +01:00
|
|
|
words_to_translate,
|
|
|
|
|
from_lang,
|
|
|
|
|
to_lang,
|
2025-12-28 15:55:43 +01:00
|
|
|
)
|
|
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
# Cache new translations
|
2026-03-13 20:41:31 +01:00
|
|
|
if use_cache and get_translation_cache is not None:
|
|
|
|
|
get_translation_cache().set_many(
|
2026-03-17 22:47:42 +01:00
|
|
|
new_translations,
|
|
|
|
|
from_lang,
|
|
|
|
|
to_lang,
|
2026-03-13 20:41:31 +01:00
|
|
|
)
|
2025-12-29 14:41:56 +01:00
|
|
|
|
|
|
|
|
# Merge cached and new translations
|
|
|
|
|
all_translations = {**cached_results, **new_translations}
|
|
|
|
|
|
|
|
|
|
# Build results in original order
|
|
|
|
|
results: list[TranslationResult] = []
|
|
|
|
|
for word in words:
|
|
|
|
|
translation = all_translations.get(word.lower(), "")
|
|
|
|
|
results.append(
|
|
|
|
|
TranslationResult(
|
|
|
|
|
source_word=word,
|
|
|
|
|
translated_word=translation,
|
|
|
|
|
source_lang=from_lang,
|
|
|
|
|
target_lang=to_lang,
|
|
|
|
|
success=bool(translation),
|
|
|
|
|
error=None if translation else "Translation failed",
|
|
|
|
|
)
|
|
|
|
|
)
|
2025-12-28 15:55:43 +01:00
|
|
|
|
2025-12-29 14:41:56 +01:00
|
|
|
return results
|
2025-12-28 15:55:43 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-03-17 22:47:42 +01:00
|
|
|
import sys
|
|
|
|
|
|
2025-12-28 15:55:43 +01:00
|
|
|
sys.exit(main())
|