Merge pull request #462 from vemonet/use-standard-build-system

Update the project configuration to use modern PEP standards, and replace `flake8` by `ruff` for linting
This commit is contained in:
Piero Toffanin 2023-07-10 21:35:09 +02:00 committed by GitHub
commit e93f22276b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 328 additions and 222 deletions

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1

View file

@ -9,35 +9,8 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest uses: LibreTranslate/LibreTranslate/.github/workflows/run-tests.yml@main
strategy: secrets: inherit
matrix:
python-version: ['3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest flake8
pip install .
python scripts/compile_locales.py
- name: Check code style with flake8 (lint)
run: |
# warnings if there are Python syntax errors or undefined names
# (remove --exit-zero to fail when syntax error)
flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: pytest
publish: publish:
@ -45,23 +18,23 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: '3.8' python-version: '3.8'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip pip install build
pip install setuptools wheel twine
python setup.py sdist bdist_wheel
- name: Build and publish to PyPI - name: Build
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: | run: |
pip install Babel==2.11.0 pip install Babel==2.11.0
python scripts/compile_locales.py python scripts/compile_locales.py
python setup.py sdist bdist_wheel python -m build
twine upload dist/*
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: ${{ secrets.PYPI_USERNAME }}
password: ${{ secrets.PYPI_PASSWORD }}

View file

@ -2,6 +2,7 @@ name: Run tests
# Run test at each push to main, if changes to package or tests files # Run test at each push to main, if changes to package or tests files
on: on:
workflow_dispatch: workflow_dispatch:
workflow_call:
pull_request: pull_request:
branches: [ main ] branches: [ main ]
push: push:
@ -21,35 +22,26 @@ jobs:
python-version: ['3.8', '3.9', '3.10'] python-version: ['3.8', '3.9', '3.10']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip pipx install hatch
pip install pytest flake8 hatch run locales
pip install .
- name: Check code style with flake8 (lint)
run: |
# warnings if there are Python syntax errors or undefined names
# (remove --exit-zero to fail when syntax error)
flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest - name: Test with pytest
run: pytest -v run: hatch run test
test_docker_build: test_docker_build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Docker build - name: Docker build
run: docker build -f docker/Dockerfile -t libretranslate . run: docker build -f docker/Dockerfile -t libretranslate .

View file

@ -20,17 +20,57 @@ sudo dnf install cmake
## Getting Started ## Getting Started
Install [`hatch`](https://hatch.pypa.io) to manage the projects dependencies and run dev scripts:
```bash
pipx install hatch
```
Clone the repository:
```bash ```bash
git clone https://github.com/LibreTranslate/LibreTranslate.git git clone https://github.com/LibreTranslate/LibreTranslate.git
cd LibreTranslate cd LibreTranslate
pip install -e . ```
libretranslate [args]
Run in development:
```bash
hatch run dev
```
Then open a web browser to <http://localhost:5000>
You can also start a new shell in a virtual environment with libretranslate installed:
```bash
hatch shell
libretranslate [args]
# Or # Or
python main.py [args] python main.py [args]
``` ```
Then open a web browser to <http://localhost:5000> > You can still use `pip install -e ".[test]"` directly if you don't want to use hatch.
## Run the tests
Run the test suite and linting checks:
```bash
hatch run test
```
To display all `print()` when debugging:
```bash
hatch run test -s
```
You can also run the tests on multiple python versions:
```bash
hatch run all:test
```
## Run with Docker ## Run with Docker

View file

@ -1,8 +1,10 @@
import os import os
import sqlite3 import sqlite3
import uuid import uuid
import requests import requests
from expiringdict import ExpiringDict from expiringdict import ExpiringDict
from libretranslate.default_values import DEFAULT_ARGUMENTS as DEFARGS from libretranslate.default_values import DEFAULT_ARGUMENTS as DEFARGS
DEFAULT_DB_PATH = DEFARGS['API_KEYS_DB_PATH'] DEFAULT_DB_PATH = DEFARGS['API_KEYS_DB_PATH']
@ -12,14 +14,14 @@ class Database:
def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30): def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30):
# Legacy check - this can be removed at some point in the near future # Legacy check - this can be removed at some point in the near future
if os.path.isfile("api_keys.db") and not os.path.isfile("db/api_keys.db"): if os.path.isfile("api_keys.db") and not os.path.isfile("db/api_keys.db"):
print("Migrating %s to %s" % ("api_keys.db", "db/api_keys.db")) print("Migrating {} to {}".format("api_keys.db", "db/api_keys.db"))
try: try:
os.rename("api_keys.db", "db/api_keys.db") os.rename("api_keys.db", "db/api_keys.db")
except Exception as e: except Exception as e:
print(str(e)) print(str(e))
db_dir = os.path.dirname(db_path) db_dir = os.path.dirname(db_path)
if not db_dir == "" and not os.path.exists(db_dir): if db_dir != '' and not os.path.exists(db_dir):
os.makedirs(db_dir) os.makedirs(db_dir)
self.db_path = db_path self.db_path = db_path
self.cache = ExpiringDict(max_len=max_cache_len, max_age_seconds=max_cache_age) self.cache = ExpiringDict(max_len=max_cache_len, max_age_seconds=max_cache_age)
@ -85,16 +87,13 @@ class RemoteDatabase:
req_limit = self.cache.get(api_key) req_limit = self.cache.get(api_key)
if req_limit is None: if req_limit is None:
try: try:
r = requests.post(self.url, data={'api_key': api_key}) r = requests.post(self.url, data={'api_key': api_key}, timeout=60)
res = r.json() res = r.json()
except Exception as e: except Exception as e:
print("Cannot authenticate API key: " + str(e)) print("Cannot authenticate API key: " + str(e))
return None return None
if res.get('error', None) is None: req_limit = res.get('req_limit', None) if res.get('error', None) is None else None
req_limit = res.get('req_limit', None)
else:
req_limit = None
self.cache[api_key] = req_limit self.cache[api_key] = req_limit
return req_limit return req_limit

View file

@ -1,30 +1,37 @@
import io import io
import os import os
import tempfile
import re import re
import tempfile
import uuid import uuid
from datetime import datetime
from functools import wraps from functools import wraps
from html import unescape from html import unescape
from timeit import default_timer from timeit import default_timer
from datetime import datetime
import argostranslatefiles import argostranslatefiles
from argostranslatefiles import get_supported_formats from argostranslatefiles import get_supported_formats
from flask import (abort, Blueprint, Flask, jsonify, render_template, request, from flask import Blueprint, Flask, Response, abort, jsonify, render_template, request, send_file, session, url_for
Response, send_file, url_for, session) from flask_babel import Babel
from flask_session import Session
from flask_swagger import swagger from flask_swagger import swagger
from flask_swagger_ui import get_swaggerui_blueprint from flask_swagger_ui import get_swaggerui_blueprint
from flask_session import Session
from translatehtml import translate_html from translatehtml import translate_html
from werkzeug.utils import secure_filename
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from werkzeug.http import http_date from werkzeug.http import http_date
from flask_babel import Babel from werkzeug.utils import secure_filename
from libretranslate import scheduler, flood, secret, remove_translated_files, security, storage from libretranslate import flood, remove_translated_files, scheduler, secret, security, storage
from libretranslate.language import detect_languages, improve_translation_formatting from libretranslate.language import detect_languages, improve_translation_formatting
from libretranslate.locales import (_, _lazy, get_available_locales, get_available_locale_codes, gettext_escaped, from libretranslate.locales import (
gettext_html, lazy_swag, get_alternate_locale_links) _,
_lazy,
get_alternate_locale_links,
get_available_locale_codes,
get_available_locales,
gettext_escaped,
gettext_html,
lazy_swag,
)
from .api_keys import Database, RemoteDatabase from .api_keys import Database, RemoteDatabase
from .suggestions import Database as SuggestionsDatabase from .suggestions import Database as SuggestionsDatabase
@ -122,8 +129,8 @@ def create_app(args):
from libretranslate.language import load_languages from libretranslate.language import load_languages
SWAGGER_URL = args.url_prefix + "/docs" # Swagger UI (w/o trailing '/') swagger_url = args.url_prefix + "/docs" # Swagger UI (w/o trailing '/')
API_URL = args.url_prefix + "/spec" api_url = args.url_prefix + "/spec"
bp = Blueprint('Main app', __name__) bp = Blueprint('Main app', __name__)
@ -150,10 +157,7 @@ def create_app(args):
frontend_argos_language_source = languages[0] frontend_argos_language_source = languages[0]
if len(languages) >= 2: language_target_fallback = languages[1] if len(languages) >= 2 else languages[0]
language_target_fallback = languages[1]
else:
language_target_fallback = languages[0]
if args.frontend_language_target == "locale": if args.frontend_language_target == "locale":
def resolve_language_locale(): def resolve_language_locale():
@ -185,10 +189,7 @@ def create_app(args):
if args.req_limit > 0 or args.api_keys or args.daily_req_limit > 0: if args.req_limit > 0 or args.api_keys or args.daily_req_limit > 0:
api_keys_db = None api_keys_db = None
if args.api_keys: if args.api_keys:
if args.api_keys_remote: api_keys_db = RemoteDatabase(args.api_keys_remote) if args.api_keys_remote else Database(args.api_keys_db_path)
api_keys_db = RemoteDatabase(args.api_keys_remote)
else:
api_keys_db = Database(args.api_keys_db_path)
from flask_limiter import Limiter from flask_limiter import Limiter
@ -220,7 +221,7 @@ def create_app(args):
os.mkdir(default_mp_dir) os.mkdir(default_mp_dir)
os.environ["PROMETHEUS_MULTIPROC_DIR"] = default_mp_dir os.environ["PROMETHEUS_MULTIPROC_DIR"] = default_mp_dir
from prometheus_client import CONTENT_TYPE_LATEST, Summary, Gauge, CollectorRegistry, multiprocess, generate_latest from prometheus_client import CONTENT_TYPE_LATEST, CollectorRegistry, Gauge, Summary, generate_latest, multiprocess
@bp.route("/metrics") @bp.route("/metrics")
@limiter.exempt @limiter.exempt
@ -338,7 +339,7 @@ def create_app(args):
get_api_key_link=args.get_api_key_link, get_api_key_link=args.get_api_key_link,
web_version=os.environ.get("LT_WEB") is not None, web_version=os.environ.get("LT_WEB") is not None,
version=get_version(), version=get_version(),
swagger_url=SWAGGER_URL, swagger_url=swagger_url,
available_locales=[{'code': l['code'], 'name': _lazy(l['name'])} for l in get_available_locales(not args.debug)], available_locales=[{'code': l['code'], 'name': _lazy(l['name'])} for l in get_available_locales(not args.debug)],
current_locale=get_locale(), current_locale=get_locale(),
alternate_locales=get_alternate_locale_links() alternate_locales=get_alternate_locale_links()
@ -544,10 +545,7 @@ def create_app(args):
) )
if args.char_limit != -1: if args.char_limit != -1:
if batch: chars = sum([len(text) for text in q]) if batch else len(q)
chars = sum([len(text) for text in q])
else:
chars = len(q)
if args.char_limit < chars: if args.char_limit < chars:
abort( abort(
@ -557,10 +555,7 @@ def create_app(args):
if source_lang == "auto": if source_lang == "auto":
source_langs = [] source_langs = []
if batch: auto_detect_texts = q if batch else [q]
auto_detect_texts = q
else:
auto_detect_texts = [q]
overall_candidates = detect_languages(q) overall_candidates = detect_languages(q)
@ -797,7 +792,7 @@ def create_app(args):
checked_filepath = security.path_traversal_check(filepath, get_upload_dir()) checked_filepath = security.path_traversal_check(filepath, get_upload_dir())
if os.path.isfile(checked_filepath): if os.path.isfile(checked_filepath):
filepath = checked_filepath filepath = checked_filepath
except security.SuspiciousFileOperation: except security.SuspiciousFileOperationError:
abort(400, description=_("Invalid filename")) abort(400, description=_("Invalid filename"))
return_data = io.BytesIO() return_data = io.BytesIO()
@ -1080,7 +1075,7 @@ def create_app(args):
swag["info"]["version"] = get_version() swag["info"]["version"] = get_version()
swag["info"]["title"] = "LibreTranslate" swag["info"]["title"] = "LibreTranslate"
@app.route(API_URL) @app.route(api_url)
@limiter.exempt @limiter.exempt
def spec(): def spec():
return jsonify(lazy_swag(swag)) return jsonify(lazy_swag(swag))
@ -1093,14 +1088,14 @@ def create_app(args):
return override_lang return override_lang
return session.get('preferred_lang', request.accept_languages.best_match(get_available_locale_codes())) return session.get('preferred_lang', request.accept_languages.best_match(get_available_locale_codes()))
babel = Babel(app, locale_selector=get_locale) Babel(app, locale_selector=get_locale)
app.jinja_env.globals.update(_e=gettext_escaped, _h=gettext_html) app.jinja_env.globals.update(_e=gettext_escaped, _h=gettext_html)
# Call factory function to create our blueprint # Call factory function to create our blueprint
swaggerui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL) swaggerui_blueprint = get_swaggerui_blueprint(swagger_url, api_url)
if args.url_prefix: if args.url_prefix:
app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) app.register_blueprint(swaggerui_blueprint, url_prefix=swagger_url)
else: else:
app.register_blueprint(swaggerui_blueprint) app.register_blueprint(swaggerui_blueprint)

View file

@ -2,10 +2,11 @@
import pycld2 as cld2 import pycld2 as cld2
class UnknownLanguage(Exception):
class UnknownLanguageError(Exception):
pass pass
class Language(object): class Language:
def __init__(self, choice): def __init__(self, choice):
name, code, confidence, bytesize = choice name, code, confidence, bytesize = choice
self.code = code self.code = code
@ -23,7 +24,7 @@ class Language(object):
return Language(("", code, 100, 0)) return Language(("", code, 100, 0))
class Detector(object): class Detector:
""" Detect the language used in a snippet of text.""" """ Detect the language used in a snippet of text."""
def __init__(self, text, quiet=False): def __init__(self, text, quiet=False):
@ -56,17 +57,16 @@ class Detector(object):
if not reliable: if not reliable:
self.reliable = False self.reliable = False
reliable, index, top_3_choices = cld2.detect(text, bestEffort=True) reliable, index, top_3_choices = cld2.detect(text, bestEffort=True)
if not self.quiet: if not self.quiet and not reliable:
if not reliable: raise UnknownLanguageError("Try passing a longer snippet of text")
raise UnknownLanguage("Try passing a longer snippet of text")
self.languages = [Language(x) for x in top_3_choices] self.languages = [Language(x) for x in top_3_choices]
self.language = self.languages[0] self.language = self.languages[0]
return self.language return self.language
def __str__(self): def __str__(self):
text = "Prediction is reliable: {}\n".format(self.reliable) text = f"Prediction is reliable: {self.reliable}\n"
text += u"\n".join(["Language {}: {}".format(i+1, str(l)) text += "\n".join([f"Language {i+1}: {str(l)}"
for i,l in enumerate(self.languages)]) for i,l in enumerate(self.languages)])
return text return text

View file

@ -1,4 +1,3 @@
from pathlib import Path
from argostranslate import package, translate from argostranslate import package, translate
@ -46,14 +45,12 @@ def check_and_install_models(force=False, load_only_lang_codes=None):
# Download and install all available packages # Download and install all available packages
for available_package in available_packages: for available_package in available_packages:
print( print(
"Downloading %s (%s) ..." f"Downloading {available_package} ({available_package.package_version}) ..."
% (available_package, available_package.package_version)
) )
available_package.install() available_package.install()
# reload installed languages # reload installed languages
libretranslate.language.languages = translate.get_installed_languages() libretranslate.language.languages = translate.get_installed_languages()
print( print(
"Loaded support for %s languages (%s models total)!" f"Loaded support for {len(translate.get_installed_languages())} languages ({len(available_packages)} models total)!"
% (len(translate.get_installed_languages()), len(available_packages))
) )

View file

@ -1,7 +1,7 @@
import string
from argostranslate import translate from argostranslate import translate
from libretranslate.detect import Detector, UnknownLanguage
from libretranslate.detect import Detector, UnknownLanguageError
__languages = None __languages = None
@ -29,7 +29,7 @@ def detect_languages(text):
for i in range(len(d)): for i in range(len(d)):
d[i].text_length = len(t) d[i].text_length = len(t)
candidates.extend(d) candidates.extend(d)
except UnknownLanguage: except UnknownLanguageError:
pass pass
# total read bytes of the provided text # total read bytes of the provided text
@ -83,10 +83,10 @@ def improve_translation_formatting(source, translation, improve_punctuation=True
if not len(source): if not len(source):
return "" return ""
if not len(translation): if not len(translation):
return source return source
if improve_punctuation: if improve_punctuation:
source_last_char = source[len(source) - 1] source_last_char = source[len(source) - 1]
translation_last_char = translation[len(translation) - 1] translation_last_char = translation[len(translation) - 1]

View file

@ -1,10 +1,11 @@
import os
import json import json
import os
from functools import lru_cache from functools import lru_cache
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_babel import lazy_gettext as _lazy from flask_babel import lazy_gettext as _lazy
from markupsafe import Markup, escape
from markupsafe import escape, Markup
@lru_cache(maxsize=None) @lru_cache(maxsize=None)
def get_available_locales(only_reviewed=True, sort_by_name=False): def get_available_locales(only_reviewed=True, sort_by_name=False):

View file

@ -197,7 +197,7 @@ def main():
from waitress import serve from waitress import serve
url_scheme = "https" if args.ssl else "http" url_scheme = "https" if args.ssl else "http"
print("Running on %s://%s:%s%s" % (url_scheme, args.host, args.port, args.url_prefix)) print(f"Running on {url_scheme}://{args.host}:{args.port}{args.url_prefix}")
serve( serve(
app, app,

View file

@ -49,7 +49,7 @@ def manage():
print("There are no API keys") print("There are no API keys")
else: else:
for item in keys: for item in keys:
print("%s: %s" % item) print("{}: {}".format(*item))
elif args.sub_command == "add": elif args.sub_command == "add":
print(db.add(args.req_limit, args.key)[0]) print(db.add(args.req_limit, args.key)[0])

View file

@ -1,5 +1,7 @@
import atexit import atexit
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
scheduler = None scheduler = None
def setup(args): def setup(args):

View file

@ -1,9 +1,9 @@
import atexit
import random import random
import string import string
from libretranslate.storage import get_storage from libretranslate.storage import get_storage
def generate_secret(): def generate_secret():
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=7)) return ''.join(random.choices(string.ascii_uppercase + string.digits, k=7))

View file

@ -1,7 +1,7 @@
import os import os
class SuspiciousFileOperation(Exception): class SuspiciousFileOperationError(Exception):
pass pass
@ -10,7 +10,7 @@ def path_traversal_check(unsafe_path, known_safe_path):
unsafe_path = os.path.abspath(unsafe_path) unsafe_path = os.path.abspath(unsafe_path)
if (os.path.commonprefix([known_safe_path, unsafe_path]) != known_safe_path): if (os.path.commonprefix([known_safe_path, unsafe_path]) != known_safe_path):
raise SuspiciousFileOperation("{} is not safe".format(unsafe_path)) raise SuspiciousFileOperationError(f"{unsafe_path} is not safe")
# Passes the check # Passes the check
return unsafe_path return unsafe_path

View file

@ -1,5 +1,5 @@
import sqlite3
import os import os
import sqlite3
from expiringdict import ExpiringDict from expiringdict import ExpiringDict
@ -10,7 +10,7 @@ class Database:
def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30): def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30):
# Legacy check - this can be removed at some point in the near future # Legacy check - this can be removed at some point in the near future
if os.path.isfile("suggestions.db") and not os.path.isfile("db/suggestions.db"): if os.path.isfile("suggestions.db") and not os.path.isfile("db/suggestions.db"):
print("Migrating %s to %s" % ("suggestions.db", "db/suggestions.db")) print("Migrating {} to {}".format("suggestions.db", "db/suggestions.db"))
try: try:
os.rename("suggestions.db", "db/suggestions.db") os.rename("suggestions.db", "db/suggestions.db")
except Exception as e: except Exception as e:

View file

@ -1,4 +1,5 @@
import sys import sys
import pytest import pytest
from libretranslate.app import create_app from libretranslate.app import create_app

View file

@ -43,7 +43,7 @@ def test_api_translate_unsupported_language(client):
response_json = json.loads(response.data) response_json = json.loads(response.data)
assert "error" in response_json assert "error" in response_json
assert "zz is not supported" == response_json["error"] assert response_json["error"] == "zz is not supported"
assert response.status_code == 400 assert response.status_code == 400
@ -57,5 +57,5 @@ def test_api_translate_missing_parameter(client):
response_json = json.loads(response.data) response_json = json.loads(response.data)
assert "error" in response_json assert "error" in response_json
assert "Invalid request: missing q parameter" == response_json["error"] assert response_json["error"] == "Invalid request: missing q parameter"
assert response.status_code == 400 assert response.status_code == 400

View file

@ -1,6 +1,7 @@
from libretranslate.init import boot
from argostranslate import package from argostranslate import package
from libretranslate.init import boot
def test_boot_argos(): def test_boot_argos():
"""Test Argos translate models initialization""" """Test Argos translate models initialization"""

167
pyproject.toml Normal file
View file

@ -0,0 +1,167 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
requires-python = ">=3.8"
name = "libretranslate"
description = "Free and Open Source Machine Translation API. Self-hosted, no limits, no ties to proprietary services."
readme = "README.md"
license = { file = "LICENSE" }
authors = [
{ name = "Piero Toffanin", email = "pt@uav4geo.com" },
{ name = "LibreTranslate Authors" },
]
maintainers = [
{ name = "Piero Toffanin", email = "pt@uav4geo.com" },
{ name = "LibreTranslate Authors" },
]
keywords = [
"Python",
"Translate",
"Translation",
"API",
]
classifiers = [
"Operating System :: OS Independent",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10"
]
dynamic = ["version"]
dependencies = [
"argostranslate ==1.8.0",
"Flask ==2.2.2",
"flask-swagger ==0.2.14",
"flask-swagger-ui ==4.11.1",
"Flask-Limiter ==2.6.3",
"Flask-Babel ==3.1.0",
"Flask-Session ==0.4.0",
"waitress ==2.1.2",
"expiringdict ==1.2.2",
" LTpycld2==0.42",
"morfessor ==2.0.6",
"appdirs ==1.4.4",
"APScheduler ==3.9.1",
"translatehtml ==1.5.2",
"argos-translate-files ==1.1.1",
"itsdangerous ==2.1.2",
"Werkzeug ==2.2.2",
"requests ==2.28.1",
"redis ==4.3.4",
"prometheus-client ==0.15.0",
"polib ==1.1.1",
]
[project.scripts]
libretranslate = "libretranslate.main:main"
ltmanage = "libretranslate.manage:manage"
[project.optional-dependencies]
test = [
"pytest >=7.2.0",
"pytest-cov",
"ruff ==0.0.277",
"types-requests",
]
[project.urls]
Homepage = "https://libretranslate.com"
Source = "https://github.com/LibreTranslate/LibreTranslate"
Documentation = "https://github.com/LibreTranslate/LibreTranslate"
Tracker = "https://github.com/LibreTranslate/LibreTranslate/issues"
History = "https://github.com/LibreTranslate/LibreTranslate/releases"
# ENVIRONMENTS AND SCRIPTS
[tool.hatch.envs.default]
features = [
"test",
]
[tool.hatch.envs.default.scripts]
dev = "python main.py {args}"
locales = "python scripts/compile_locales.py"
fmt = [
"ruff libretranslate scripts --fix",
]
test = [
"fmt",
"pytest {args}",
]
cov = [
"pytest --cov-report html {args}",
"python -c 'import webbrowser; webbrowser.open(\"http://0.0.0.0:3000\")'",
"python -m http.server 3000 --directory ./htmlcov",
]
[[tool.hatch.envs.all.matrix]]
python = ["3.8", "3.9", "3.10", "3.11"]
# TOOLS
[tool.hatch.version]
path = "VERSION"
pattern = "^(?P<version>[0-9]*.[0-9]*.[0-9]*)$"
[tool.pytest.ini_options]
addopts = [
"-v",
"--cov=libretranslate",
"--color=yes",
"--cov-report=term-missing",
]
# https://beta.ruff.rs/docs/rules
[tool.ruff]
src = ["libretranslate", "scripts"]
target-version = "py38"
line-length = 136
select = [
"I", # isort
"N", # pep8-naming
"S", # bandit
"A", # flake8-builtins
"YTT", # flake8-2020
"B", # flake8-bugbear
# "C", # flake8-comprehensions
"ICN", # flake8-import-conventions
# "SIM", # flake8-simplify
"TID", # flake8-tidy-imports
# "Q", # flake8-quotes
# "FBT", # flake8-boolean-trap
"F", # pyflakes
"UP", # pyupgrade
# "E", # pycodestyle errors
# "W", # pycodestyle warnings
# "PLC", # pylint convention
"PLE", # pylint error
# "PLR", # pylint refactor
# "PLW", # pylint warning
# "RUF", # ruff specific
]
ignore = [
"E501", # line too long
"A003", # Class attribute is shadowing a python builtin
"S101", # Use of `assert` detected
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
"T201", "T203", # remove print and pprint
"E402", # Module level import not at top of file
]
[tool.ruff.per-file-ignores]
"__init__.py" = ["I", "F401"] # module imported but unused
[tool.ruff.mccabe]
max-complexity = 12

View file

@ -1,21 +0,0 @@
argostranslate==1.8.0
Flask==2.2.2
flask-swagger==0.2.14
flask-swagger-ui==4.11.1
Flask-Limiter==2.6.3
Flask-Babel==3.1.0
Flask-Session==0.4.0
waitress==2.1.2
expiringdict==1.2.2
LTpycld2==0.42
morfessor==2.0.6
appdirs==1.4.4
APScheduler==3.9.1
translatehtml==1.5.2
argos-translate-files==1.1.1
itsdangerous==2.1.2
Werkzeug==2.2.2
requests==2.28.1
redis==4.3.4
prometheus-client==0.15.0
polib==1.1.1

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys
import os import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from babel.messages.frontend import main as pybabel from babel.messages.frontend import main as pybabel
@ -15,7 +16,7 @@ if __name__ == "__main__":
link = "https://hosted.weblate.org/translate/libretranslate/app/%s/" % l['code'] link = "https://hosted.weblate.org/translate/libretranslate/app/%s/" % l['code']
if l['code'] == 'en': if l['code'] == 'en':
link = "https://hosted.weblate.org/projects/libretranslate/app/" link = "https://hosted.weblate.org/projects/libretranslate/app/"
print("%s | %s | %s" % (l['name'], ':heavy_check_mark:' if l['reviewed'] else '', "[Edit](%s)" % link)) print("{} | {} | {}".format(l['name'], ':heavy_check_mark:' if l['reviewed'] else '', "[Edit](%s)" % link))
else: else:
locales_dir = os.path.join("libretranslate", "locales") locales_dir = os.path.join("libretranslate", "locales")
if not os.path.isdir(locales_dir): if not os.path.isdir(locales_dir):

View file

@ -1,7 +1,9 @@
from prometheus_client import multiprocess
import re import re
import sys import sys
from prometheus_client import multiprocess
def child_exit(server, worker): def child_exit(server, worker):
multiprocess.mark_process_dead(worker.pid) multiprocess.mark_process_dead(worker.pid)
@ -35,7 +37,7 @@ def on_starting(server):
args = get_args() args = get_args()
from libretranslate import storage, scheduler, flood, secret from libretranslate import flood, scheduler, secret, storage
storage.setup(args.shared_storage) storage.setup(args.shared_storage)
scheduler.setup(args) scheduler.setup(args)
flood.setup(args) flood.setup(args)

View file

@ -1,4 +1,5 @@
import requests import requests
response = requests.post( response = requests.post(
url='http://0.0.0.0:5000/translate', url='http://0.0.0.0:5000/translate',
headers={'Content-Type': 'application/json'}, headers={'Content-Type': 'application/json'},
@ -6,6 +7,7 @@ response = requests.post(
'q': 'Hello World!', 'q': 'Hello World!',
'source': 'en', 'source': 'en',
'target': 'en' 'target': 'en'
} },
timeout=60
) )
# if server unavailable then requests with raise exception and healthcheck will fail # if server unavailable then requests with raise exception and healthcheck will fail

View file

@ -1,8 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys
import os import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import argparse import argparse
from libretranslate.init import check_and_install_models from libretranslate.init import check_and_install_models
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
import argparse import argparse
import time
import sqlite3
import json import json
import sqlite3
import time
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Program to generate JSONL files from a LibreTranslate's suggestions.db") parser = argparse.ArgumentParser(description="Program to generate JSONL files from a LibreTranslate's suggestions.db")

View file

@ -1,17 +1,19 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys
import os import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import re
import polib
import json import json
import re
import polib
from babel.messages.frontend import main as pybabel from babel.messages.frontend import main as pybabel
from libretranslate.language import load_languages, improve_translation_formatting
from libretranslate.locales import get_available_locale_codes, swag_eval
from translatehtml import translate_html
from libretranslate.app import get_version, create_app
from libretranslate.main import get_args
from flask_swagger import swagger from flask_swagger import swagger
from libretranslate.app import create_app, get_version
from libretranslate.language import improve_translation_formatting, load_languages
from libretranslate.locales import get_available_locale_codes, swag_eval
from libretranslate.main import get_args
from translatehtml import translate_html
# Update strings # Update strings
if __name__ == "__main__": if __name__ == "__main__":
@ -74,7 +76,7 @@ if __name__ == "__main__":
if not os.path.isfile(meta_file): if not os.path.isfile(meta_file):
with open(meta_file, 'w') as f: with open(meta_file, 'w') as f:
f.write(json.dumps({ f.write(json.dumps({
'name': next((lang.name for lang in languages if lang.code == l)), 'name': next(lang.name for lang in languages if lang.code == l),
'reviewed': False 'reviewed': False
}, indent=4)) }, indent=4))
print("Wrote %s" % meta_file) print("Wrote %s" % meta_file)

View file

@ -1,12 +0,0 @@
[flake8]
exclude = .git,
.vscode,
.gitignore,
README.md,
venv,
test,
setup.py,
libretranslate/__init__.py
max-line-length = 136
ignore = E741

View file

@ -1,38 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
setup(
version=open('VERSION').read().strip(),
name='libretranslate',
license='GNU Affero General Public License v3.0',
description='Free and Open Source Machine Translation API. Self-hosted, no limits, no ties to proprietary services.',
author='LibreTranslate Authors',
author_email='pt@uav4geo.com',
url='https://libretranslate.com',
packages=find_packages(),
# packages=find_packages(include=['openpredict']),
# package_dir={'openpredict': 'openpredict'},
package_data={'': ['static/*', 'static/**/*', 'templates/*', 'locales/**/meta.json', 'locales/**/**/*.mo']},
include_package_data=True,
entry_points={
'console_scripts': [
'libretranslate=libretranslate.main:main',
'ltmanage=libretranslate.manage:manage'
],
},
python_requires='>=3.8.0',
long_description=open('README.md').read(),
long_description_content_type="text/markdown",
install_requires=open("requirements.txt", "r").readlines(),
tests_require=['pytest==7.2.0'],
setup_requires=['pytest-runner'],
classifiers=[
"License :: OSI Approved :: GNU Affero General Public License v3 ",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10"
]
)