Skip to content

Commit 9069a1f

Browse files
authored
Code improvement and words' cache. (#7)
* 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
1 parent 99d33cc commit 9069a1f

9 files changed

Lines changed: 161 additions & 59 deletions

File tree

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,17 @@ The following tools were used in the construction of the project:
5656

5757
``` bash
5858
.
59-
├── api_dictionary
59+
├── mldictionary_api
60+
│   ├── models
61+
│   │   ├── __init__.py
62+
│   │   ├── base.py
63+
│   │   ├── const.py
64+
│   │   ├── meanings.py
65+
│   │   └── requests.py
6066
│   ├── resources
6167
│   │   ├── __init__.py
68+
│   │   ├── const.py
69+
│   │   ├── response.py
6270
│   │   └── translator.py
6371
│   ├── routes
6472
│   │   ├── __init__.py
@@ -82,7 +90,8 @@ The following tools were used in the construction of the project:
8290
├── docker-compose.yml
8391
└── requirements.txt
8492

85-
7 directories, 18 files
93+
8 directories, 25 files
94+
8695
```
8796

8897
---

mldictionary_api/const.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
from random import randint as random
22

3-
from mldictionary import English, Portuguese, Spanish
4-
5-
from mldictionary_api.resources import Translator
6-
73

84
VIEWS_PREFIX = '/'
95

@@ -26,16 +22,3 @@
2622
API_ROUTES_EXAMPLES[random(0, 1)] + 'crazy',
2723
API_ROUTES_EXAMPLES[random(0, 1)] + 'mad',
2824
]
29-
30-
DICTIONARIES = {
31-
'en': English(),
32-
'pt': Portuguese(),
33-
'es': Spanish(),
34-
'en-pt': Translator(),
35-
'pt-en': Translator(),
36-
}
37-
38-
39-
LOCAL_ADDR = "127.0.0.1"
40-
TOTAL_REQUESTS_ALLOW = 50
41-
TTL_REQUEST = 60 * 60
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from .meanings import RedisMeaningsCache
12
from .requests import RedisRequests
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
__all__ = ['RedisMeaningsCache']
2+
3+
from mldictionary_api.models.base import RedisBaseModel
4+
5+
6+
class RedisMeaningsCache(RedisBaseModel):
7+
def __init__(self):
8+
super().__init__()
9+
10+
def get(self, match: str) -> list:
11+
meanings = self.db.smembers(match) or set()
12+
return list(meanings)
13+
14+
def set(self, key, values, ttl: int):
15+
for value in values:
16+
self.db.sadd(key, value)
17+
self.db.expire(key, ttl)
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
from .translator import Translator
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from mldictionary import English, Portuguese, Spanish
2+
3+
from mldictionary_api.resources.translator import Translator
4+
5+
LIMITED_REQUESTS_DICTIONARIES = [Translator]
6+
LOCAL_ADDR = '127.0.0.1'
7+
TOTAL_REQUESTS_ALLOW = 50
8+
TTL_REQUEST = 60 * 60
9+
TTL_MEANINGS_CACHE = 24 * 60 * 60
10+
11+
12+
ENGLISH_REPR = 'en'
13+
PORTUGUESE_REPR = 'pt'
14+
SPANISH_REPR = 'es'
15+
ENGLISH_TO_PORTUGUESE_REPR = 'en-pt'
16+
PORTUGUESE_TO_ENGLISH = 'pt-en'
17+
18+
19+
DICTIONARIES = {
20+
ENGLISH_REPR: English(),
21+
PORTUGUESE_REPR: Portuguese(),
22+
SPANISH_REPR: Spanish(),
23+
ENGLISH_TO_PORTUGUESE_REPR: Translator(),
24+
PORTUGUESE_TO_ENGLISH: Translator(),
25+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import traceback
2+
from typing import Type
3+
4+
from flask import jsonify, request
5+
from mldictionary import Dictionary
6+
from werkzeug.exceptions import NotFound, TooManyRequests
7+
8+
from mldictionary_api.models import RedisRequests, RedisMeaningsCache
9+
from mldictionary_api.resources.const import (
10+
DICTIONARIES,
11+
LIMITED_REQUESTS_DICTIONARIES,
12+
LOCAL_ADDR,
13+
TOTAL_REQUESTS_ALLOW,
14+
TTL_MEANINGS_CACHE,
15+
TTL_REQUEST,
16+
)
17+
18+
19+
class ResponseAPI:
20+
def get_meanings(self, lang, word):
21+
dictionary = DICTIONARIES[lang]
22+
request_ip = self.__get_request_ip(request.headers.getlist("X-Forwarded-For"))
23+
24+
if not (ilimited_dictionary := self.__valid_request(request_ip, dictionary)):
25+
total_requests = RedisRequests().get(f'requests:{request_ip}')
26+
if total_requests > TOTAL_REQUESTS_ALLOW:
27+
raise TooManyRequests(
28+
f'The address {request_ip} is allow to make only "{TOTAL_REQUESTS_ALLOW}" requests '
29+
f'wait until {int(TTL_REQUEST / 60)} minutes and try again'
30+
)
31+
32+
if not (meanings := self.__get_meanings(word, dictionary)):
33+
raise NotFound(f'"{word}" not found, check the spelling and try again')
34+
35+
if not ilimited_dictionary:
36+
self.__make_cache(request_ip, total_requests, word, meanings)
37+
return (
38+
jsonify({'source': dictionary.URL.format(word), 'meanings': meanings}),
39+
200,
40+
)
41+
42+
def handle_error(self, err):
43+
traceback.print_tb(err.__traceback__)
44+
return (
45+
jsonify({'message': err.description, 'http_status': err.code}),
46+
err.code,
47+
)
48+
49+
def __get_request_ip(self, heroku_proxy_header: list[str]):
50+
return (
51+
request.remote_addr if not heroku_proxy_header else heroku_proxy_header[0]
52+
)
53+
54+
def __valid_request(self, request_ip: str, dictionary: Type[Dictionary]):
55+
if request_ip in LOCAL_ADDR:
56+
return True
57+
58+
for limited_dictionary in LIMITED_REQUESTS_DICTIONARIES:
59+
if isinstance(dictionary, limited_dictionary):
60+
return False
61+
return True
62+
63+
def __get_meanings(self, word: str, dictionary: Type[Dictionary]) -> list[str]:
64+
meanings = RedisMeaningsCache().get(f'meanings:{word}')
65+
return meanings if meanings else dictionary.get_meanings(word)
66+
67+
def __make_cache(
68+
self, request_ip: str, total_requests: int, word: str, meanings: list[str]
69+
):
70+
71+
RedisRequests().set(
72+
f'requests:{request_ip}', str(total_requests + 1), TTL_REQUEST
73+
)
74+
RedisMeaningsCache().set(f'meanings:{word}', meanings, TTL_MEANINGS_CACHE)

mldictionary_api/resources/translator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
class Translator(Dictionary):
55
URL = 'https://www.linguee.com/english-portuguese/search?source=auto&query={}'
66
LANGUAGE = 'Translator(en-pt)'
7-
TARGET_TAG = 'span'
8-
TARGET_ATTR = {'class': 'tag_trans'}
7+
TARGET_TAG = 'a'
8+
TARGET_ATTR = {'class': 'featured'}
99
REPLACES = {}

mldictionary_api/routes/api.py

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,61 @@
1-
import traceback
2-
3-
from flask import Blueprint, jsonify, request
1+
from flask import Blueprint
42
from werkzeug.exceptions import NotFound, InternalServerError, TooManyRequests
53

6-
from mldictionary_api.models import RedisRequests
7-
from mldictionary_api.const import (
8-
API_PREFIX,
9-
DICTIONARIES,
10-
LOCAL_ADDR,
11-
TOTAL_REQUESTS_ALLOW,
12-
TTL_REQUEST,
4+
from mldictionary_api.const import API_PREFIX
5+
from mldictionary_api.resources.response import ResponseAPI
6+
from mldictionary_api.resources.const import (
7+
ENGLISH_REPR,
8+
ENGLISH_TO_PORTUGUESE_REPR,
9+
PORTUGUESE_REPR,
10+
PORTUGUESE_TO_ENGLISH,
11+
SPANISH_REPR,
1312
)
1413

15-
1614
api = Blueprint('mldictionary_api', __name__, url_prefix=API_PREFIX)
1715

1816

1917
@api.route('/dictionary/en/<word>/')
18+
def english(word: str):
19+
return ResponseAPI().get_meanings(ENGLISH_REPR, word)
20+
21+
2022
@api.route('/dictionary/pt/<word>/')
23+
def portuguese(word: str):
24+
return ResponseAPI().get_meanings(PORTUGUESE_REPR, word)
25+
26+
2127
@api.route('/dictionary/es/<word>/')
22-
@api.route('/translator/en-pt/<word>/')
23-
@api.route('/translator/pt-en/<word>/')
24-
def dictionary(word: str):
28+
def spanish(word: str):
29+
return ResponseAPI().get_meanings(SPANISH_REPR, word)
2530

26-
requests_db = RedisRequests()
2731

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

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

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

4541

4642
@api.app_errorhandler(NotFound)
4743
def not_found(err):
48-
traceback.print_tb(err.__traceback__)
49-
return jsonify({'message': err.description}), err.code
44+
return ResponseAPI().handle_error(err)
5045

5146

5247
@api.app_errorhandler(TooManyRequests)
5348
def too_many_requests(err):
54-
traceback.print_tb(err.__traceback__)
55-
return jsonify({'message': err.description}), err.code
49+
return ResponseAPI().handle_error(err)
5650

5751

5852
@api.app_errorhandler(InternalServerError)
5953
def internal_error(err):
60-
traceback.print_tb(err.__traceback__)
61-
return jsonify({'message': err.description}), err.code
54+
return ResponseAPI().handle_error(err)
6255

6356

6457
@api.app_errorhandler(Exception)
6558
def general_exception(err):
66-
traceback.print_tb(err.__traceback__)
67-
return jsonify({'message': 'Don\'t recognize erro'}), 500
59+
err.description = 'Don\'t recognize erro'
60+
err.code = 500
61+
return ResponseAPI().handle_error(err)

0 commit comments

Comments
 (0)