- -
+ ``` + locust -f tests/stress_test.py --headless -u 1000 -r 100 + ``` + Os testes de carga são úteis para avaliar o desempenho da API sob carga simulada. Os parâmetros `-u` e `-r` definem o número de usuários e a taxa de aumento de usuários, respectivamente. diff --git a/README.pt.md b/README.pt.md deleted file mode 100644 index 0159db9f0..000000000 --- a/README.pt.md +++ /dev/null @@ -1,82 +0,0 @@ -# Desafio Bravo - -[[English](README.md) | [Português](README.pt.md)] - -Construa uma API, que responda JSON, para conversão monetária. Ela deve ter uma moeda de lastro (USD) e fazer conversões entre diferentes moedas com **cotações de verdade e atuais**. - -A API precisa converter entre as seguintes moedas: - -- USD -- BRL -- EUR -- BTC -- ETH - -Outras moedas podem ser adicionadas conforme o uso. - -Ex: USD para BRL, USD para BTC, ETH para BRL, etc... - -A requisição deve receber como parâmetros: A moeda de origem, o valor a ser convertido e a moeda final. - -Ex: `?from=BTC&to=EUR&amount=123.45` - -Construa também um endpoint para adicionar e remover moedas suportadas pela API, usando os verbos HTTP. - -A API deve suportar conversão entre moedas fiduciárias, crypto e fictícias. Exemplo: BRL->HURB, HURB->ETH - -"Moeda é o meio pelo qual são efetuadas as transações monetárias." (Wikipedia, 2021). - -Sendo assim, é possível imaginar que novas moedas passem a existir ou deixem de existir, é possível também imaginar moedas fictícias como as de Dungeons & Dragons sendo utilizadas nestas transações, como por exemplo quanto vale uma Peça de Ouro (D&D) em Real ou quanto vale a GTA$ 1 em Real. - -Vamos considerar a cotação da PSN onde GTA$ 1.250.000,00 custam R$ 83,50 claramente temos uma relação entre as moedas, logo é possível criar uma cotação. (Playstation Store, 2021). - -Ref: -Wikipedia [Site Institucional]. Disponível em:- -
diff --git a/__pycache__/views.cpython-310.pyc b/__pycache__/views.cpython-310.pyc new file mode 100644 index 000000000..f97c8be0f Binary files /dev/null and b/__pycache__/views.cpython-310.pyc differ diff --git a/inputs.py b/inputs.py new file mode 100644 index 000000000..13916fd92 --- /dev/null +++ b/inputs.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel +from typing import Optional + +class Currency(BaseModel): + currency_name: str + is_fictional: Optional[bool] + backing: Optional[str] + backing_amount: Optional[float] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..26aefa280 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,45 @@ +annotated-types==0.6.0 +anyio==4.3.0 +async-timeout==4.0.3 +blinker==1.7.0 +Brotli==1.1.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +ConfigArgParse==1.7 +exceptiongroup==1.2.0 +fastapi==0.109.2 +Flask==3.0.2 +Flask-Cors==4.0.0 +Flask-Login==0.6.3 +gevent==24.2.1 +geventhttpclient==2.0.11 +greenlet==3.0.3 +h11==0.14.0 +idna==3.6 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.3 +locust==2.23.1 +MarkupSafe==2.1.5 +msgpack==1.0.7 +packaging==23.2 +pluggy==1.4.0 +psutil==5.9.8 +pydantic==2.6.1 +pydantic_core==2.16.2 +pytest==8.0.1 +pyzmq==25.1.2 +redis==5.0.1 +requests==2.31.0 +roundrobin==0.0.4 +six==1.16.0 +sniffio==1.3.0 +starlette==0.36.3 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==2.2.1 +uvicorn==0.27.1 +Werkzeug==3.0.1 +zope.event==5.0 +zope.interface==6.2 diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/__pycache__/__init__.cpython-310.pyc b/services/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 000000000..6d3d953e0 Binary files /dev/null and b/services/__pycache__/__init__.cpython-310.pyc differ diff --git a/services/__pycache__/awesome_api.cpython-310.pyc b/services/__pycache__/awesome_api.cpython-310.pyc new file mode 100644 index 000000000..d6be9f135 Binary files /dev/null and b/services/__pycache__/awesome_api.cpython-310.pyc differ diff --git a/services/awesome_api.py b/services/awesome_api.py new file mode 100644 index 000000000..b558cc1df --- /dev/null +++ b/services/awesome_api.py @@ -0,0 +1,12 @@ +import requests + +class AwesomeApiService(): + def __init__(self): + self.url = "https://economia.awesomeapi.com.br/last/" + def get_bid_value_from_api(self, from_currency: str, to_currency: str): + path = from_currency.upper() + "-" + to_currency.upper() + response = requests.get(self.url + path) + json_response = response.json() + return json_response[from_currency.upper()+ to_currency.upper()]["bid"] + + diff --git a/services/redis.py b/services/redis.py new file mode 100644 index 000000000..5530486e9 --- /dev/null +++ b/services/redis.py @@ -0,0 +1,47 @@ +import redis + + +r = redis.Redis(host='localhost', port=6379, db=7, decode_responses=True) + +class Redis(): + def add_currency(self, currency: dict): + if currency.get("is_fictional", False): + mounted_currency = { + "currency_name": currency["currency_name"].upper(), + "is_fictional": "True", + "backing": currency["backing"].upper(), + "backing_amount": currency["backing_amount"], + } + + r.hmset(currency["currency_name"].upper(), mounted_currency) + + currency_name_upper = currency["currency_name"].upper() + r.rpush("available_currencies", currency_name_upper) + return r.lrange("available_currencies", 0, -1) + + def update_currency(self, currency: dict): + currency_name = currency.get("currency_name") + is_fictional = "True" if currency.get("is_fictional") else "False" + mounted_currency = { + "currency_name": currency["currency_name"].upper(), + "is_fictional": is_fictional, + "backing": currency["backing"].upper(), + "backing_amount": currency["backing_amount"], + } + r.hmset(currency_name.upper(), mounted_currency) + + + return self.get_currency(currency_name) + + def get_currency(self, currency_name: str): + + return r.hgetall(currency_name) + + def get_avaliable_currencies(self): + + return r.lrange("available_currencies", 0, -1) + + def remove_currency_from_list(self, currency_name: str): + r.lrem("available_currencies", 0, currency_name.upper()) + + return r.lrange("available_currencies", 0, -1) diff --git a/services/utils.py b/services/utils.py new file mode 100644 index 000000000..71482307f --- /dev/null +++ b/services/utils.py @@ -0,0 +1,50 @@ +from services.awesome_api import AwesomeApiService +from services.redis import Redis +def is_currency_fictional(currency_name: str): + currency = Redis().get_currency(currency_name=currency_name) + return currency.get("is_fictional", "false") == "True" + +def is_currency_avaliable (currency_name: str): + avaliable_currencies = Redis().get_avaliable_currencies() + + return currency_name in avaliable_currencies + +def has_fictional_currency_flow(from_currency: str, to_currency: str, amount: int): + from_currency_backing,from_currency_backing_amount = _extract_backing_and_backing_amount(from_currency) + to_currency_backing,to_currency_backing_amount = _extract_backing_and_backing_amount(to_currency) + bid_value = _get_bid_value(from_currency=from_currency_backing, to_currency=to_currency_backing) + converted_value = ((float(bid_value) * float(from_currency_backing_amount))/float(to_currency_backing_amount)) * amount + output = _mount_output(float(bid_value), converted_value) + return output + +def not_fictional_currency_flow(from_currency: str, to_currency: str, amount: int): + bid_value = _get_bid_value(from_currency,to_currency) + converted_value = float(bid_value) * amount + output = _mount_output(float(bid_value), converted_value) + return output + +def _get_bid_value(from_currency: str, to_currency: str): + try: + awesome_api_service = AwesomeApiService() + bid_value = awesome_api_service.get_bid_value_from_api(from_currency=from_currency, to_currency=to_currency) + return bid_value + except Exception as e: + raise e + +def _extract_backing_and_backing_amount(currency_name): + currency = Redis().get_currency(currency_name=currency_name) + + if not currency.get("is_fictional", "false") == "True": + return currency_name,1 + + return currency.get("backing"), currency.get("backing_amount") + +def _mount_output(bid_value:float, converted_value: float): + return { + "bid_value": bid_value, + "converted_value": converted_value, + } + + + + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/stress_test.py b/tests/stress_test.py new file mode 100644 index 000000000..df1b86c8f --- /dev/null +++ b/tests/stress_test.py @@ -0,0 +1,9 @@ +from locust import HttpUser, between, task + +class MyUser(HttpUser): + wait_time = between(0.1, 0.5) + host = "http://localhost:8000" + + @task + def my_task(self): + self.client.get("/convert?from_currency=USD&to_currency=BRL&amount=2") \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..5fd5a4658 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,55 @@ +from unittest import TestCase +from unittest.mock import patch +from services.utils import is_currency_avaliable, is_currency_fictional, has_fictional_currency_flow, not_fictional_currency_flow + +class TestTasks(TestCase): + + @patch("services.utils.Redis") + def test_is_currency_fictional(self,mock_redis): + mock_redis.return_value.get_currency.return_value = {'currency_name': 'TESTE', 'is_fictional': 'True', 'backing': 'NOT', 'backing_amount': '1.0'} + response = is_currency_fictional("TESTE") + self.assertEqual(response,True) + + @patch("services.utils.Redis") + def test_is_currency_fictional_false(self,mock_redis): + mock_redis.return_value.get_currency.return_value = {'currency_name': 'TESTE', 'is_fictional': 'false', 'backing': 'NOT', 'backing_amount': '1.0'} + response = is_currency_fictional("TESTE") + self.assertEqual(response,False) + + @patch("services.utils.Redis") + def test_is_currency_avaliable(self,mock_redis): + mock_redis.return_value.get_avaliable_currencies.return_value = ["TESTE"] + response = is_currency_avaliable("TESTE") + self.assertEqual(response,True) + + @patch("services.utils.Redis") + def test_is_currency_avaliable_false(self,mock_redis): + mock_redis.return_value.get_avaliable_currencies.return_value = ["TESTE222"] + response = is_currency_avaliable("TESTE") + self.assertEqual(response,False) + + @patch("services.utils.Redis") + def test_is_currency_avaliable_false(self,mock_redis): + mock_redis.return_value.get_avaliable_currencies.return_value = ["TESTE222"] + response = is_currency_avaliable("TESTE") + self.assertEqual(response,False) + + @patch("services.utils.AwesomeApiService") + def test_not_fictional_flow(self,mock_api): + expected_response = { + "bid_value": 1, + "converted_value": 1, + } + mock_api.return_value.get_bid_value_from_api.return_value = 1 + response = not_fictional_currency_flow("TESTE","TESTE",1) + self.assertEqual(response,expected_response) + + @patch("services.utils.AwesomeApiService") + def test_fictional_flow(self,mock_api): + expected_response = { + "bid_value": 1, + "converted_value": 1, + } + mock_api.return_value.get_bid_value_from_api.return_value = 1 + response = has_fictional_currency_flow("TESTE","TESTE",1) + self.assertEqual(response,expected_response) diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 000000000..21fb0e688 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,43 @@ +from unittest import TestCase +from unittest.mock import patch +from inputs import Currency +from views import create_currency, convert, delete_currency + +class TestTasks(TestCase): + + @patch("views.Redis") + def test_create_currency(self,mock_redis): + currency = Currency(currency_name="TESTE",is_fictional="True", backing="NOT", backing_amount="1.0") + mock_redis.return_value.add_currency.return_value = ["TESTE"] + response = create_currency(currency) + self.assertEqual(response[0],["TESTE"]) + + @patch("views.is_currency_avaliable") + @patch("views.is_currency_fictional") + @patch("views.has_fictional_currency_flow") + def test_convert(self,mock_has_fictional_currency_flow,mock_is_currency_fictional, mock_is_currency_avaliable): + mock_has_fictional_currency_flow.return_value = { + "bid_value": 1, + "converted_value": 1, + } + mock_is_currency_fictional.return_value = True + mock_is_currency_avaliable.return_value = True + response = convert(from_currency="TESTE", to_currency="TESTE", amount=1) + expected_response = { + "bid_value": 1, + "converted_value": 1, + } + self.assertEqual(response[0],expected_response) + + @patch("views.Redis") + def test_edit_currency(self,mock_redis): + currency = Currency(currency_name="TESTE",is_fictional="True", backing="NOT", backing_amount="1.0") + mock_redis.return_value.add_currency.return_value = {'currency_name': 'TESTE', 'is_fictional': 'True', 'backing': 'NOT', 'backing_amount': '1.0'} + response = create_currency(currency) + self.assertEqual(response[0],{'currency_name': 'TESTE', 'is_fictional': 'True', 'backing': 'NOT', 'backing_amount': '1.0'}) + + @patch("views.Redis") + def test_delete_currency(self,mock_redis): + mock_redis.return_value.remove_currency_from_list.return_value = ["TESTE2"] + response = delete_currency(currency_name="TESTE") + self.assertEqual(response[0],["TESTE2"]) diff --git a/views.py b/views.py new file mode 100644 index 000000000..61974b620 --- /dev/null +++ b/views.py @@ -0,0 +1,42 @@ +import json +from fastapi import FastAPI +from inputs import Currency + +from services.redis import Redis +from services.utils import is_currency_avaliable, is_currency_fictional, has_fictional_currency_flow, not_fictional_currency_flow + +app = FastAPI() + +@app.get("/convert") +def convert(from_currency: str, to_currency:str, amount: float): + if not is_currency_avaliable(from_currency) or not is_currency_avaliable(to_currency): + return {"message": "Moedas não disponíveis, por favor insira no banco de dados"}, 404 + if is_currency_fictional(from_currency) or is_currency_fictional(to_currency): + output = has_fictional_currency_flow(from_currency=from_currency, to_currency=to_currency, amount=amount) + else: + output = not_fictional_currency_flow(from_currency=from_currency, to_currency=to_currency, amount=amount) + + + return output, 200 + +@app.post("/currency") +def create_currency(request: Currency): + + payload = json.loads(request.json()) + redis_service = Redis() + response = redis_service.add_currency(payload) + return response, 200 + +@app.put("/currency") +def edit_currency(request: Currency): + + payload = json.loads(request.json()) + redis_service = Redis() + response = redis_service.update_currency(payload) + return response, 200 + +@app.delete("/currency") +def delete_currency(currency_name: str): + redis_service = Redis() + response = redis_service.remove_currency_from_list(currency_name) + return response, 200 \ No newline at end of file