Skip to content

Commit

Permalink
Code improvement and words' cache. (#7)
Browse files Browse the repository at this point in the history
* Defined local consts to avoid conflict

* Using a class to process response

* Improving translator class results

* Take resources's __init__ off to avoid import conflict

* Create response class to treat api requests and create a cache to limited dictionary

* Update README.md
  • Loading branch information
PabloEmidio committed Jan 18, 2022
1 parent 99d33cc commit 9069a1f
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 59 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,17 @@ The following tools were used in the construction of the project:

``` bash
.
├── api_dictionary
├── mldictionary_api
│   ├── models
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── const.py
│   │   ├── meanings.py
│   │   └── requests.py
│   ├── resources
│   │   ├── __init__.py
│   │   ├── const.py
│   │   ├── response.py
│   │   └── translator.py
│   ├── routes
│   │   ├── __init__.py
Expand All @@ -82,7 +90,8 @@ The following tools were used in the construction of the project:
├── docker-compose.yml
└── requirements.txt

7 directories, 18 files
8 directories, 25 files

```

---
Expand Down
17 changes: 0 additions & 17 deletions mldictionary_api/const.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
from random import randint as random

from mldictionary import English, Portuguese, Spanish

from mldictionary_api.resources import Translator


VIEWS_PREFIX = '/'

Expand All @@ -26,16 +22,3 @@
API_ROUTES_EXAMPLES[random(0, 1)] + 'crazy',
API_ROUTES_EXAMPLES[random(0, 1)] + 'mad',
]

DICTIONARIES = {
'en': English(),
'pt': Portuguese(),
'es': Spanish(),
'en-pt': Translator(),
'pt-en': Translator(),
}


LOCAL_ADDR = "127.0.0.1"
TOTAL_REQUESTS_ALLOW = 50
TTL_REQUEST = 60 * 60
1 change: 1 addition & 0 deletions mldictionary_api/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .meanings import RedisMeaningsCache
from .requests import RedisRequests
17 changes: 17 additions & 0 deletions mldictionary_api/models/meanings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
__all__ = ['RedisMeaningsCache']

from mldictionary_api.models.base import RedisBaseModel


class RedisMeaningsCache(RedisBaseModel):
def __init__(self):
super().__init__()

def get(self, match: str) -> list:
meanings = self.db.smembers(match) or set()
return list(meanings)

def set(self, key, values, ttl: int):
for value in values:
self.db.sadd(key, value)
self.db.expire(key, ttl)
1 change: 0 additions & 1 deletion mldictionary_api/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
from .translator import Translator
25 changes: 25 additions & 0 deletions mldictionary_api/resources/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from mldictionary import English, Portuguese, Spanish

from mldictionary_api.resources.translator import Translator

LIMITED_REQUESTS_DICTIONARIES = [Translator]
LOCAL_ADDR = '127.0.0.1'
TOTAL_REQUESTS_ALLOW = 50
TTL_REQUEST = 60 * 60
TTL_MEANINGS_CACHE = 24 * 60 * 60


ENGLISH_REPR = 'en'
PORTUGUESE_REPR = 'pt'
SPANISH_REPR = 'es'
ENGLISH_TO_PORTUGUESE_REPR = 'en-pt'
PORTUGUESE_TO_ENGLISH = 'pt-en'


DICTIONARIES = {
ENGLISH_REPR: English(),
PORTUGUESE_REPR: Portuguese(),
SPANISH_REPR: Spanish(),
ENGLISH_TO_PORTUGUESE_REPR: Translator(),
PORTUGUESE_TO_ENGLISH: Translator(),
}
74 changes: 74 additions & 0 deletions mldictionary_api/resources/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import traceback
from typing import Type

from flask import jsonify, request
from mldictionary import Dictionary
from werkzeug.exceptions import NotFound, TooManyRequests

from mldictionary_api.models import RedisRequests, RedisMeaningsCache
from mldictionary_api.resources.const import (
DICTIONARIES,
LIMITED_REQUESTS_DICTIONARIES,
LOCAL_ADDR,
TOTAL_REQUESTS_ALLOW,
TTL_MEANINGS_CACHE,
TTL_REQUEST,
)


class ResponseAPI:
def get_meanings(self, lang, word):
dictionary = DICTIONARIES[lang]
request_ip = self.__get_request_ip(request.headers.getlist("X-Forwarded-For"))

if not (ilimited_dictionary := self.__valid_request(request_ip, dictionary)):
total_requests = RedisRequests().get(f'requests:{request_ip}')
if total_requests > TOTAL_REQUESTS_ALLOW:
raise TooManyRequests(
f'The address {request_ip} is allow to make only "{TOTAL_REQUESTS_ALLOW}" requests '
f'wait until {int(TTL_REQUEST / 60)} minutes and try again'
)

if not (meanings := self.__get_meanings(word, dictionary)):
raise NotFound(f'"{word}" not found, check the spelling and try again')

if not ilimited_dictionary:
self.__make_cache(request_ip, total_requests, word, meanings)
return (
jsonify({'source': dictionary.URL.format(word), 'meanings': meanings}),
200,
)

def handle_error(self, err):
traceback.print_tb(err.__traceback__)
return (
jsonify({'message': err.description, 'http_status': err.code}),
err.code,
)

def __get_request_ip(self, heroku_proxy_header: list[str]):
return (
request.remote_addr if not heroku_proxy_header else heroku_proxy_header[0]
)

def __valid_request(self, request_ip: str, dictionary: Type[Dictionary]):
if request_ip in LOCAL_ADDR:
return True

for limited_dictionary in LIMITED_REQUESTS_DICTIONARIES:
if isinstance(dictionary, limited_dictionary):
return False
return True

def __get_meanings(self, word: str, dictionary: Type[Dictionary]) -> list[str]:
meanings = RedisMeaningsCache().get(f'meanings:{word}')
return meanings if meanings else dictionary.get_meanings(word)

def __make_cache(
self, request_ip: str, total_requests: int, word: str, meanings: list[str]
):

RedisRequests().set(
f'requests:{request_ip}', str(total_requests + 1), TTL_REQUEST
)
RedisMeaningsCache().set(f'meanings:{word}', meanings, TTL_MEANINGS_CACHE)
4 changes: 2 additions & 2 deletions mldictionary_api/resources/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
class Translator(Dictionary):
URL = 'https://www.linguee.com/english-portuguese/search?source=auto&query={}'
LANGUAGE = 'Translator(en-pt)'
TARGET_TAG = 'span'
TARGET_ATTR = {'class': 'tag_trans'}
TARGET_TAG = 'a'
TARGET_ATTR = {'class': 'featured'}
REPLACES = {}
68 changes: 31 additions & 37 deletions mldictionary_api/routes/api.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,61 @@
import traceback

from flask import Blueprint, jsonify, request
from flask import Blueprint
from werkzeug.exceptions import NotFound, InternalServerError, TooManyRequests

from mldictionary_api.models import RedisRequests
from mldictionary_api.const import (
API_PREFIX,
DICTIONARIES,
LOCAL_ADDR,
TOTAL_REQUESTS_ALLOW,
TTL_REQUEST,
from mldictionary_api.const import API_PREFIX
from mldictionary_api.resources.response import ResponseAPI
from mldictionary_api.resources.const import (
ENGLISH_REPR,
ENGLISH_TO_PORTUGUESE_REPR,
PORTUGUESE_REPR,
PORTUGUESE_TO_ENGLISH,
SPANISH_REPR,
)


api = Blueprint('mldictionary_api', __name__, url_prefix=API_PREFIX)


@api.route('/dictionary/en/<word>/')
def english(word: str):
return ResponseAPI().get_meanings(ENGLISH_REPR, word)


@api.route('/dictionary/pt/<word>/')
def portuguese(word: str):
return ResponseAPI().get_meanings(PORTUGUESE_REPR, word)


@api.route('/dictionary/es/<word>/')
@api.route('/translator/en-pt/<word>/')
@api.route('/translator/pt-en/<word>/')
def dictionary(word: str):
def spanish(word: str):
return ResponseAPI().get_meanings(SPANISH_REPR, word)

requests_db = RedisRequests()

choice = request.url.split('/')[5]
dictionary = DICTIONARIES[choice]
request_ip = request.remote_addr if not request.headers.getlist("X-Forwarded-For") else request.headers.getlist("X-Forwarded-For")[0]
total_requests = requests_db.get(f'requests:{request_ip}')
if not (meanings := dictionary.get_meanings(word)):
raise NotFound(f'"{word}" not found, check the spelling and try again')
if request_ip != LOCAL_ADDR:
if total_requests > TOTAL_REQUESTS_ALLOW:
raise TooManyRequests(
f'The address {request_ip} is allow to make only "{TOTAL_REQUESTS_ALLOW}" requests '
f'wait until {int(TTL_REQUEST / 60)} minutes and try again'
)
@api.route('/translator/en-pt/<word>/')
def english_to_portuguese(word: str):
return ResponseAPI().get_meanings(ENGLISH_TO_PORTUGUESE_REPR, word)

requests_db.set(f'requests:{request_ip}', str(total_requests + 1), TTL_REQUEST)

return jsonify({'source': dictionary.URL.format(word), 'meanings': meanings}), 200
@api.route('/translator/pt-en/<word>/')
def portuguese_to_english(word: str):
return ResponseAPI().get_meanings(PORTUGUESE_TO_ENGLISH, word)


@api.app_errorhandler(NotFound)
def not_found(err):
traceback.print_tb(err.__traceback__)
return jsonify({'message': err.description}), err.code
return ResponseAPI().handle_error(err)


@api.app_errorhandler(TooManyRequests)
def too_many_requests(err):
traceback.print_tb(err.__traceback__)
return jsonify({'message': err.description}), err.code
return ResponseAPI().handle_error(err)


@api.app_errorhandler(InternalServerError)
def internal_error(err):
traceback.print_tb(err.__traceback__)
return jsonify({'message': err.description}), err.code
return ResponseAPI().handle_error(err)


@api.app_errorhandler(Exception)
def general_exception(err):
traceback.print_tb(err.__traceback__)
return jsonify({'message': 'Don\'t recognize erro'}), 500
err.description = 'Don\'t recognize erro'
err.code = 500
return ResponseAPI().handle_error(err)

0 comments on commit 9069a1f

Please sign in to comment.