From 9069a1f1b84b307f32be0127021ecb5d8119074c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Em=C3=ADdio=20S=2ES?=
Date: Mon, 17 Jan 2022 21:53:21 -0300
Subject: [PATCH] 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
---
README.md | 13 ++++-
mldictionary_api/const.py | 17 ------
mldictionary_api/models/__init__.py | 1 +
mldictionary_api/models/meanings.py | 17 ++++++
mldictionary_api/resources/__init__.py | 1 -
mldictionary_api/resources/const.py | 25 ++++++++
mldictionary_api/resources/response.py | 74 ++++++++++++++++++++++++
mldictionary_api/resources/translator.py | 4 +-
mldictionary_api/routes/api.py | 68 ++++++++++------------
9 files changed, 161 insertions(+), 59 deletions(-)
create mode 100644 mldictionary_api/models/meanings.py
create mode 100644 mldictionary_api/resources/const.py
create mode 100644 mldictionary_api/resources/response.py
diff --git a/README.md b/README.md
index 3236c91..6b858e2 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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
+
```
---
diff --git a/mldictionary_api/const.py b/mldictionary_api/const.py
index 52de459..ef068c8 100644
--- a/mldictionary_api/const.py
+++ b/mldictionary_api/const.py
@@ -1,9 +1,5 @@
from random import randint as random
-from mldictionary import English, Portuguese, Spanish
-
-from mldictionary_api.resources import Translator
-
VIEWS_PREFIX = '/'
@@ -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
diff --git a/mldictionary_api/models/__init__.py b/mldictionary_api/models/__init__.py
index 848da7b..85ed42d 100644
--- a/mldictionary_api/models/__init__.py
+++ b/mldictionary_api/models/__init__.py
@@ -1 +1,2 @@
+from .meanings import RedisMeaningsCache
from .requests import RedisRequests
diff --git a/mldictionary_api/models/meanings.py b/mldictionary_api/models/meanings.py
new file mode 100644
index 0000000..6eed91c
--- /dev/null
+++ b/mldictionary_api/models/meanings.py
@@ -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)
diff --git a/mldictionary_api/resources/__init__.py b/mldictionary_api/resources/__init__.py
index 546c41b..e69de29 100644
--- a/mldictionary_api/resources/__init__.py
+++ b/mldictionary_api/resources/__init__.py
@@ -1 +0,0 @@
-from .translator import Translator
diff --git a/mldictionary_api/resources/const.py b/mldictionary_api/resources/const.py
new file mode 100644
index 0000000..fee02c5
--- /dev/null
+++ b/mldictionary_api/resources/const.py
@@ -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(),
+}
diff --git a/mldictionary_api/resources/response.py b/mldictionary_api/resources/response.py
new file mode 100644
index 0000000..5a7ff0e
--- /dev/null
+++ b/mldictionary_api/resources/response.py
@@ -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)
diff --git a/mldictionary_api/resources/translator.py b/mldictionary_api/resources/translator.py
index b253730..aa550c8 100644
--- a/mldictionary_api/resources/translator.py
+++ b/mldictionary_api/resources/translator.py
@@ -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 = {}
diff --git a/mldictionary_api/routes/api.py b/mldictionary_api/routes/api.py
index 962bef2..f70dfb3 100644
--- a/mldictionary_api/routes/api.py
+++ b/mldictionary_api/routes/api.py
@@ -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//')
+def english(word: str):
+ return ResponseAPI().get_meanings(ENGLISH_REPR, word)
+
+
@api.route('/dictionary/pt//')
+def portuguese(word: str):
+ return ResponseAPI().get_meanings(PORTUGUESE_REPR, word)
+
+
@api.route('/dictionary/es//')
-@api.route('/translator/en-pt//')
-@api.route('/translator/pt-en//')
-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//')
+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//')
+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)