diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..618899bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Arquivos e pastas gerados pelo Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.db +*.sqlite3 + +# Ambiente virtual +venv/ +env/ +ENV/ + +# Arquivos de log e saída +*.log +*.log.* +logs/ +log.txt +log/ + +# Cache de dependências +__pycache__/ +.cache/ + +# Arquivos de configuração +*.ini +*.conf + +# Arquivos de ambiente +.env +.env.* \ No newline at end of file diff --git a/README.md b/README.md index 22af01577..492f4e5fc 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,61 @@ -# Hurb Bravo Challenge +# Conversor de Moedas -[[English](README.md) | [Portuguese](README.pt.md)] +![Redis](https://img.shields.io/badge/redis-%23DD0031.svg?style=for-the-badge&logo=redis&logoColor=white) +![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi) +![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54) -Build an API, which responds to JSON, for currency conversion. It must have a backing currency (USD) and make conversions between different currencies with **real and live values**. +Esse projeto é um sistema de conversão de moedas reais e ficticias. -The API must convert between the following currencies: +## Pré-requisitos -- USD -- BRL -- EUR -- BTC -- ETH +Antes de começar, certifique-se de que você tenha o seguinte software instalado em seu ambiente: -Other coins could be added as usage. +- Python 3.x +- FastApi +- Redis -Ex: USD to BRL, USD to BTC, ETH to BRL, etc... +## Instalação -The request must receive as parameters: The source currency, the amount to be converted and the final currency. +1. **Clonando o repositório** -Ex: `?from=BTC&to=EUR&amount=123.45` + Clone este repositório para o seu ambiente local: -Also build an endpoint to add and remove API supported currencies using HTTP verbs. + ``` + git clone https://github.com/gabrielgimenez98/challenge-bravo.git + cd challenge-bravo -The API must support conversion between FIAT, crypto and fictitious. Example: BRL->HURB, HURB->ETH +2. **Criando Ambiente Virtual** -"Currency is the means by which monetary transactions are effected." (Wikipedia, 2021). + Clone este repositório para o seu ambiente local: -Therefore, it is possible to imagine that new coins come into existence or cease to exist, it is also possible to imagine fictitious coins such as Dungeons & Dragons coins being used in these transactions, such as how much is a Gold Piece (Dungeons & Dragons) in Real or how much is the GTA$1 in Real. + ``` + python3 -m venv venv + source venv/bin/activate # No Windows, use "venv\Scripts\activate" -Let's consider the PSN quote where GTA$1,250,000.00 cost R$83.50 we clearly have a relationship between the currencies, so it is possible to create a quote. (Playstation Store, 2021). +3. **Instalando dependências** -Ref: -Wikipedia [Institutional Website]. Available at: . Accessed on: 28 April 2021. -Playstation Store [Virtual Store]. Available at: . Accessed on: 28 April 2021. + Instale as dependências usando -You can use any programming language for the challenge. Below is the list of languages ​​that we here at Hurb have more affinity: + ``` + pip install -r requirements.txt + +4. **Rodando o projeto localmente** -- JavaScript (NodeJS) -- Python -- Go -- Ruby -- C++ -- PHP + ``` + uvicorn views:app --reload + ``` -## Requirements + Após isso é possível encontrar o swagger na rota ```\docs``` -- Fork this challenge and create your project (or workspace) using your version of that repository, as soon as you finish the challenge, submit a _pull request_. - - If you have any reason not to submit a _pull request_, create a private repository on Github, do every challenge on the **main** branch and don't forget to fill in the `pull-request.txt` file. As soon as you finish your development, add the user `automator-hurb` to your repository as a contributor and make it available for at least 30 days. **Do not add the `automator-hurb` until development is complete.** - - If you have any problem creating the private repository, at the end of the challenge fill in the file called `pull-request.txt`, compress the project folder - including the `.git` folder - and send it to us by email. -- The code needs to run on macOS or Ubuntu (preferably as a Docker container) -- To run your code, all you need to do is run the following commands: - - git clone \$your-fork - - cd \$your-fork - - command to install dependencies - - command to run the application -- The API can be written with or without the help of _frameworks_ - - If you choose to use a _framework_ that results in _boilerplate code_, mark in the README which piece of code was written by you. The more code you make, the more content we will have to rate. -- The API needs to support a volume of 1000 requests per second in a stress test. -- The API needs to include real and current quotes through integration with public currency quote APIs +5. **Rodando os testes unitários** -## Evaluation criteria + ``` + pytest -s tests/ + ``` -- **Organization of code**: Separation of modules, view and model, back-end and front-end -- **Clarity**: Does the README explain briefly what the problem is and how can I run the application? -- **Assertiveness**: Is the application doing what is expected? If something is missing, does the README explain why? -- **Code readability** (including comments) -- **Security**: Are there any clear vulnerabilities? -- **Test coverage** (We don't expect full coverage) -- **History of commits** (structure and quality) -- **UX**: Is the interface user-friendly and self-explanatory? Is the API intuitive? -- **Technical choices**: Is the choice of libraries, database, architecture, etc. the best choice for the application? +6. **Rodando os testes de carga** -## Doubts - -Any questions you may have, check the [_issues_](https://github.com/HurbCom/challenge-bravo/issues) to see if someone hasn't already and if you can't find your answer, open one yourself. new issue! - -Godspeed! ;) - -

- Challange accepted -

+ ``` + 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 @@ -# Hurb 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: . Acesso em: 28 abril 2021. -Playstation Store [Loja Virtual]. Disponível em: . Acesso em: 28 abril 2021. - -Você pode usar qualquer linguagem de programação para o desafio. Abaixo a lista de linguagens que nós aqui do Hurb temos mais afinidade: - -- JavaScript (NodeJS) -- Python -- Go -- Ruby -- C++ -- PHP - -## Requisitos - -- Forkar esse desafio e criar o seu projeto (ou workspace) usando a sua versão desse repositório, tão logo acabe o desafio, submeta um _pull request_. - - Caso você tenha algum motivo para não submeter um _pull request_, crie um repositório privado no Github, faça todo desafio na branch **main** e não se esqueça de preencher o arquivo `pull-request.txt`. Tão logo termine seu desenvolvimento, adicione como colaborador o usuário `automator-hurb` no seu repositório e o deixe disponível por pelo menos 30 dias. **Não adicione o `automator-hurb` antes do término do desenvolvimento.** - - Caso você tenha algum problema para criar o repositório privado, ao término do desafio preencha o arquivo chamado `pull-request.txt`, comprima a pasta do projeto - incluindo a pasta `.git` - e nos envie por email. -- O código precisa rodar em macOS ou Ubuntu (preferencialmente como container Docker) -- Para executar seu código, deve ser preciso apenas rodar os seguintes comandos: - - git clone \$seu-fork - - cd \$seu-fork - - comando para instalar dependências - - comando para executar a aplicação -- A API pode ser escrita com ou sem a ajuda de _frameworks_ - - Se optar por usar um _framework_ que resulte em _boilerplate code_, assinale no README qual pedaço de código foi escrito por você. Quanto mais código feito por você, mais conteúdo teremos para avaliar. -- A API precisa suportar um volume de 1000 requisições por segundo em um teste de estresse. -- A API precisa contemplar cotações de verdade e atuais através de integração com APIs públicas de cotação de moedas - -## Critério de avaliação - -- **Organização do código**: Separação de módulos, view e model, back-end e front-end -- **Clareza**: O README explica de forma resumida qual é o problema e como pode rodar a aplicação? -- **Assertividade**: A aplicação está fazendo o que é esperado? Se tem algo faltando, o README explica o porquê? -- **Legibilidade do código** (incluindo comentários) -- **Segurança**: Existe alguma vulnerabilidade clara? -- **Cobertura de testes** (Não esperamos cobertura completa) -- **Histórico de commits** (estrutura e qualidade) -- **UX**: A interface é de fácil uso e auto-explicativa? A API é intuitiva? -- **Escolhas técnicas**: A escolha das bibliotecas, banco de dados, arquitetura, etc, é a melhor escolha para a aplicação? - -## Dúvidas - -Quaisquer dúvidas que você venha a ter, consulte as [_issues_](https://github.com/HurbCom/challenge-bravo/issues) para ver se alguém já não a fez e caso você não ache sua resposta, abra você mesmo uma nova issue! - -Boa sorte e boa viagem! ;) - -

- Challange accepted -

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