diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..e0cc78f0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ad9ad867 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.vscode/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f4846e8f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# For more information, please refer to https://aka.ms/vscode-docker-python +FROM python:3.10-slim + +EXPOSE 8000 + +# Keeps Python from generating .pyc files in the container +ENV PYTHONDONTWRITEBYTECODE=1 + +# Turns off buffering for easier container logging +ENV PYTHONUNBUFFERED=1 + +# Install pip requirements +COPY requirements.txt . +RUN python -m pip install -r requirements.txt + +WORKDIR /app +COPY . /app + +# Creates a non-root user with an explicit UID and adds permission to access the /app folder +# For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers +RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app +USER appuser + +# During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "-k", "uvicorn.workers.UvicornWorker", "main:app"] diff --git a/README.md b/README.md index 5c3393a9..038c1aec 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,54 @@ -![WATTIO](http://wattio.com.br/web/image/1204-212f47c3/Logo%20Wattio.png) +## Features -#### Descrição +- é possivel realizar todas as operações CRUD na API -O desafio consiste em implementar um CRUD de filmes, utilizando [python](https://www.python.org/ "python") integrando com uma API REST e uma possível persistência de dados. +## Installation guide -Rotas da API: +- clone esse repositório +- instale o [Docker](https://www.docker.com/) / [Docker-compose](https://docs.docker.com/compose/install/) - - `/filmes` - [GET] deve retornar todos os filmes cadastrados. - - `/filmes` - [POST] deve cadastrar um novo filme. - - `/filmes/{id}` - [GET] deve retornar o filme com ID especificado. -O Objetivo é te desafiar e reconhecer seu esforço para aprender e se adaptar. Qualquer código enviado, ficaremos muito felizes e avaliaremos com toda atenção! +## How to use -#### Sugestão de Ferramentas -Não é obrigatório utilizar todas as as tecnologias sugeridas, mas será um diferencial =] +- utilize `docker-compose up` +- é possivel ver todos os endpoints em localhost:8000/docs +- pode-se utilizar [Postmand](EventsLogger.postman_collection.json) para a testagem, no repositório há um backup para importar -- Orientação a objetos (utilizar objetos, classes para manipular os filmes) -- [FastAPI](https://fastapi.tiangolo.com/) (API com documentação auto gerada) -- [Docker](https://www.docker.com/) / [Docker-compose](https://docs.docker.com/compose/install/) (Aplicação deverá ficar em um container docker, e o start deverá seer com o comando ``` docker-compose up ``` -- Integração com banco de dados (persistir as informações em json (iniciante) /[SqLite](https://www.sqlite.org/index.html) / [SQLAlchemy](https://fastapi.tiangolo.com/tutorial/sql-databases/#sql-relational-databases) / outros DB) +## API endpoints + -#### Como começar? +### filmes -- Fork do repositório -- Criar branch com seu nome ``` git checkout -b feature/ana ``` -- Faça os commits de suas alterações ``` git commit -m "[ADD] Funcionalidade" ``` -- Envie a branch para seu repositório ``` git push origin feature/ana ``` -- Navegue até o [Github](https://github.com/), crie seu Pull Request apontando para a branch **```main```** -- Atualize o README.md descrevendo como subir sua aplicação +| HTTP Verbs | Endpoints | Action | +| ---------- | ------------------ | --------------------------------- | +| GET | /filmes | listar todos os filmes | +| GET | /filmes | buscar apenas um filme | +| POST | /filmes/{id} | criar um novo filme | +| PUT | /filmes/{id} | atualizar um filme | +| DELETE | /filmes/{id} | deletar um filme | -#### Dúvidas? +## Technologies used -Qualquer dúvida / sugestão / melhoria / orientação adicional só enviar email para hendrix@wattio.com.br + -Salve! +- [python](https://www.python.org/ "python") +- [SQLAlchemy](https://www.sqlalchemy.org/) +- [FastAPI](https://fastapi.tiangolo.com/) + + +## Authors + + + +- [Lucas Oliveira](https://github.com/LordSouza) + +## License + +This project is available for use under the MIT License. diff --git a/backend.postman_collection.json b/backend.postman_collection.json new file mode 100644 index 00000000..78906620 --- /dev/null +++ b/backend.postman_collection.json @@ -0,0 +1,255 @@ +{ + "info": { + "_postman_id": "79acb18e-76f2-46d0-838b-21661eed7d45", + "name": "backend", + "description": "# 🚀 Get started here\n\nThis template guides you through CRUD operations (GET, POST, PUT, DELETE), variables, and tests.\n\n## 🔖 **How to use this template**\n\n#### **Step 1: Send requests**\n\nRESTful APIs allow you to perform CRUD operations using the POST, GET, PUT, and DELETE HTTP methods.\n\nThis collection contains each of these [request](https://learning.postman.com/docs/sending-requests/requests/) types. Open each request and click \"Send\" to see what happens.\n\n#### **Step 2: View responses**\n\nObserve the response tab for status code (200 OK), response time, and size.\n\n#### **Step 3: Send new Body data**\n\nUpdate or add new data in \"Body\" in the POST request. Typically, Body data is also used in PUT request.\n\n```\n{\n \"name\": \"Add your name in the body\"\n}\n\n ```\n\n#### **Step 4: Update the variable**\n\nVariables enable you to store and reuse values in Postman. We have created a [variable](https://learning.postman.com/docs/sending-requests/variables/) called `base_url` with the sample request [https://postman-api-learner.glitch.me](https://postman-api-learner.glitch.me). Replace it with your API endpoint to customize this collection.\n\n#### **Step 5: Add tests in the \"Tests\" tab**\n\nTests help you confirm that your API is working as expected. You can write test scripts in JavaScript and view the output in the \"Test Results\" tab.\n\n\n\n## 💪 Pro tips\n\n- Use folders to group related requests and organize the collection.\n- Add more [scripts](https://learning.postman.com/docs/writing-scripts/intro-to-scripts/) in \"Tests\" to verify if the API works as expected and execute workflows.\n \n\n## 💡Related templates\n\n[API testing basics](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9a37a28-055b-49cd-8c7e-97494a21eb54&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[API documentation](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9c28f47-1253-44af-a2f3-20dce4da1f18&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[Authorization methods](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=31a9a6ed-4cdf-4ced-984c-d12c9aec1c27&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "29771180" + }, + "item": [ + { + "name": "Get data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/filmes", + "host": [ + "{{base_url}}" + ], + "path": [ + "filmes" + ], + "query": [ + { + "key": "id", + "value": "1", + "disabled": true + } + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Get data id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/filmes/2", + "host": [ + "{{base_url}}" + ], + "path": [ + "filmes", + "2" + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Post data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful POST request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 201]);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"nome\":\"The Shawshank Redemption\",\n \"genero\":\"Drama\",\n \"ano\":1994,\n \"descricao\":\"Over the course of several years, two convicts form a friendship, seeking consolation and, eventually, redemption through basic compassion.\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/filmes", + "host": [ + "{{base_url}}" + ], + "path": [ + "filmes" + ] + }, + "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." + }, + "response": [] + }, + { + "name": "Update data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful PUT request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"nome\":\"\",\n \"genero\":\"\",\n \"ano\":2000,\n \"descricao\":\"\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/filmes/1", + "host": [ + "{{base_url}}" + ], + "path": [ + "filmes", + "1" + ], + "query": [ + { + "key": "id", + "value": "1", + "disabled": true + } + ] + }, + "description": "This is a PUT request and it is used to overwrite an existing piece of data. For instance, after you create an entity with a POST request, you may want to modify that later. You can do that using a PUT request. You typically identify the entity being updated by including an identifier in the URL (eg. `id=1`).\n\nA successful PUT request typically returns a `200 OK`, `201 Created`, or `204 No Content` response code." + }, + "response": [] + }, + { + "name": "Delete data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful DELETE request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 202, 204]);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/filmes/1", + "host": [ + "{{base_url}}" + ], + "path": [ + "filmes", + "1" + ] + }, + "description": "This is a DELETE request, and it is used to delete data that was previously created via a POST request. You typically identify the entity being updated by including an identifier in the URL (eg. `id=1`).\n\nA successful DELETE request typically returns a `200 OK`, `202 Accepted`, or `204 No Content` response code." + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "base_url", + "value": "http://127.0.0.1:8000" + } + ] +} \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 00000000..fa01f47a --- /dev/null +++ b/database.py @@ -0,0 +1,19 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + + +SQLALCHEMY_DATABASE_URL = "sqlite:///./filmes.db" +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml new file mode 100644 index 00000000..7465bb9f --- /dev/null +++ b/docker-compose.debug.yml @@ -0,0 +1,12 @@ +version: '3.4' + +services: + backend: + image: backend + build: + context: . + dockerfile: ./Dockerfile + command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn main:app --host 0.0.0.0 --port 8000"] + ports: + - 8000:8000 + - 5678:5678 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..e722a2e9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.4' + +services: + backend: + image: backend + build: + context: . + dockerfile: ./Dockerfile + ports: + - 8000:8000 diff --git a/filmes.db b/filmes.db new file mode 100644 index 00000000..7aaa0d31 Binary files /dev/null and b/filmes.db differ diff --git a/main.py b/main.py new file mode 100644 index 00000000..16ccc5a9 --- /dev/null +++ b/main.py @@ -0,0 +1,110 @@ +from typing import Optional +from fastapi import Depends, FastAPI +from pydantic import BaseModel +import models +from database import engine, get_db +from sqlalchemy.orm import Session + + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +class Filme(BaseModel): + nome: str + ano: Optional[int] + genero: Optional[str] + descricao: Optional[str] + + +@app.get("/filmes") +async def root(db: Session = Depends(get_db)): + """Retorna todos os filmes cadastrados no banco de dados + + Returns: + [dict]: {"data": [Filme]} + """ + try: + filmes = models.Filme.listar(db=db) + return {"data": filmes} + except Exception as e: + breakpoint() + return {"message": "Erro ao buscar filmes"} + + +@app.get("/filmes/{id}") +async def get_movie(id: int, db: Session = Depends(get_db)): + """Retorna um filme específico + + Args: + id (int): id do filme + + Returns: + filme: Filme + """ + try: + filme = models.Filme.buscar(filme_id=id, db=db) + return {"data": filme} + except Exception as e: + return {"message": "Erro ao buscar filme"} + + +@app.post("/filmes", status_code=201) +async def create_movie(filme: Filme, db: Session = Depends(get_db)): + """cria um novo filme + + Args: + filme (Filme): Novo filme + + Returns: + str: mensagem de sucesso ou erro + """ + try: + new_filme = models.Filme( + nome=filme.nome, + ano=filme.ano, + genero=filme.genero, + descricao=filme.descricao, + ) + models.Filme.criar(filme=new_filme, db=db) + + return {"message": "Filme criado com sucesso!"} + except Exception as e: + return {"message": "Erro ao criar filme"} + + +@app.put("/filmes/{id}") +async def update_movie(id: int, filme_modificado: Filme, db: Session = Depends(get_db)): + """atualiza um filme + + Args: + id (int): atualiza o filme com o id + filme_modificado (Filme): novos valores para colocar no fime + + Returns: + _type_: _description_ + """ + try: + models.Filme.atualizar(filme_id=id, filme_modificado=filme_modificado, db=db) + return {"message": "Filme atualizado com sucesso!"} + except Exception as e: + return {"message": "Erro ao atualizar filme"} + + +@app.delete("/filmes/{id}") +async def delete_movie(id: int, db: Session = Depends(get_db)): + """deleta um filme + + Args: + id (int): id do filme a ser deletado + + Returns: + str: mensagem de sucesso ou erro + """ + try: + models.Filme.deletar(filme_id=id, db=db) + + return {"message": "Filme deletado com sucesso!"} + except Exception as e: + return {"message": "Erro ao deletar filme"} diff --git a/models.py b/models.py new file mode 100644 index 00000000..ff89c448 --- /dev/null +++ b/models.py @@ -0,0 +1,49 @@ +from sqlalchemy import Column, Integer, String +from database import Base, get_db + + +class Filme(Base): + __tablename__ = "filmes" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + nome = Column(String, index=True) + genero = Column(String, index=True, nullable=True) + ano = Column(Integer, index=True, nullable=True) + descricao = Column(String, index=True, nullable=True) + + def criar(filme, db): + db.add(filme) + db.commit() + db.refresh(filme) + + def listar(db): + filmes = db.query(Filme).all() + return filmes + + def buscar(filme_id: int, db): + filme = db.query(Filme).filter(Filme.id == filme_id).first() + if not filme: + return "Filme não encontrado!" + return filme + + def atualizar(filme_id: int, filme_modificado, db): + filme = db.query(Filme).filter(Filme.id == filme_id).first() + if not filme: + return {"error": "Filme não encontrado!"} + if filme_modificado.nome: + filme.nome = filme_modificado.nome + if filme_modificado.ano: + filme.ano = filme_modificado.ano + if filme_modificado.genero: + filme.genero = filme_modificado.genero + if filme_modificado.descricao: + filme.descricao = filme_modificado.descricao + db.commit() + + def deletar(filme_id: int, db): + filme = db.query(Filme).filter(Filme.id == filme_id).first() + db.delete(filme) + db.commit() + + def __repr__(self): + return f"" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..79124c34 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi[all]==0.109.0 +uvicorn[standard]==0.26.0 +gunicorn==20.1.0 +SQLAlchemy==2.0.25 \ No newline at end of file