From 5385af660d4f3bb894d277f8e3e05b6d0e72b36e Mon Sep 17 00:00:00 2001 From: Ricardo Date: Fri, 11 Sep 2026 17:22:41 -0300 Subject: [PATCH] Commit inicial --- .env.example | 13 ++++++++ .gitignore | 6 ++++ app/core/configs.py | 33 +++++++++++++++++++ app/core/database.py | 47 +++++++++++++++++++++++++++ app/v1/api.py | 7 ++++ app/v1/modules/escrituracao/router.py | 0 app/v1/modules/veiculo/model.py | 5 +++ app/v1/modules/veiculo/router.py | 30 +++++++++++++++++ app/v1/modules/veiculo/services.py | 17 ++++++++++ main.py | 15 +++++++++ 10 files changed, 173 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 app/core/configs.py create mode 100644 app/core/database.py create mode 100644 app/v1/api.py create mode 100644 app/v1/modules/escrituracao/router.py create mode 100644 app/v1/modules/veiculo/model.py create mode 100644 app/v1/modules/veiculo/router.py create mode 100644 app/v1/modules/veiculo/services.py create mode 100644 main.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..78c513c --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Copie estas variáveis para o .env e preencha os valores + +# SQL Server +DB_HOST= +DB_PORTA=1433 +DB_DATABASE= +DB_LOGIN= +DB_SENHA= + +# Opcionais (já têm valor padrão) +# DB_DRIVER=ODBC Driver 17 for SQL Server +# DB_POOL_SIZE=5 +# DB_POOL_MAX_OVERFLOW=10 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c204ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +.venv/ +__pycache__/ +*.pyc +*.pyo +*.log \ No newline at end of file diff --git a/app/core/configs.py b/app/core/configs.py new file mode 100644 index 0000000..28703e7 --- /dev/null +++ b/app/core/configs.py @@ -0,0 +1,33 @@ +from pathlib import Path + +from pydantic import SecretStr +from pydantic_settings import BaseSettings, SettingsConfigDict + +# Raiz do projeto (api-podium/), pra achar o .env independente de onde a API for iniciada +RAIZ_PROJETO = Path(__file__).resolve().parents[2] + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=RAIZ_PROJETO / '.env', + env_file_encoding='utf-8', + env_ignore_empty=True, # variável vazia no .env conta como não configurada + extra='ignore', # outras variáveis no .env não geram erro + ) + + # SQL Server + db_host: str + db_porta: int = 1433 + db_database: str + db_login: str + db_senha: SecretStr # SecretStr evita que a senha apareça em print/log + db_driver: str = 'ODBC Driver 17 for SQL Server' + + # Pool de conexões + db_pool_size: int = 5 + db_pool_max_overflow: int = 10 + + # Configs da api + port: int + +settings = Settings() \ No newline at end of file diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..7a5d5a4 --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,47 @@ +from collections.abc import Iterator +from typing import Annotated + +import pyodbc +from fastapi import Depends +from sqlalchemy import create_engine +from sqlalchemy.engine import URL +from sqlalchemy.pool import PoolProxiedConnection + +from app.core.configs import settings + +# O pool do SQLAlchemy substitui o pool interno do ODBC. Com os dois ligados, +# conexões que o SQLAlchemy descarta continuam abertas no driver. +pyodbc.pooling = False + + +def _escapar(valor: str) -> str: + # Envolve o valor em chaves pra que ; ou } na senha não quebrem a connection string + return '{' + valor.replace('}', '}}') + '}' + + +connection_string = ( + f"DRIVER={_escapar(settings.db_driver)};" + f"SERVER={settings.db_host},{settings.db_porta};" + f"DATABASE={_escapar(settings.db_database)};" + f"UID={_escapar(settings.db_login)};" + f"PWD={_escapar(settings.db_senha.get_secret_value())};" +) + +engine = create_engine( + URL.create('mssql+pyodbc', query={'odbc_connect': connection_string}), + pool_size=settings.db_pool_size, # conexões mantidas abertas + max_overflow=settings.db_pool_max_overflow, # extras permitidas em pico de acesso + pool_pre_ping=True, # testa a conexão antes de entregar (o servidor pode ter derrubado) + pool_recycle=1800, # renova conexões com mais de 30 min +) + + +def get_conexao() -> Iterator[PoolProxiedConnection]: + """Pega uma conexão do pool e devolve ao final da requisição.""" + conexao = engine.raw_connection() + try: + yield conexao + finally: + conexao.close() # devolve ao pool, não fecha de verdade + +Conexao = Annotated[PoolProxiedConnection, Depends(get_conexao)] diff --git a/app/v1/api.py b/app/v1/api.py new file mode 100644 index 0000000..137ec10 --- /dev/null +++ b/app/v1/api.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +from app.v1.modules.veiculo.router import veiculo_router + +api_router = APIRouter() + +api_router.include_router(veiculo_router) \ No newline at end of file diff --git a/app/v1/modules/escrituracao/router.py b/app/v1/modules/escrituracao/router.py new file mode 100644 index 0000000..e69de29 diff --git a/app/v1/modules/veiculo/model.py b/app/v1/modules/veiculo/model.py new file mode 100644 index 0000000..599188d --- /dev/null +++ b/app/v1/modules/veiculo/model.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class CancelamentoPropostaInput(BaseModel): + id: str \ No newline at end of file diff --git a/app/v1/modules/veiculo/router.py b/app/v1/modules/veiculo/router.py new file mode 100644 index 0000000..00114a7 --- /dev/null +++ b/app/v1/modules/veiculo/router.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter, HTTPException, status + +from app.core.database import Conexao +from app.v1.modules.veiculo.model import CancelamentoPropostaInput +from app.v1.modules.veiculo.services import CancelamentoProposta + +veiculo_router = APIRouter( + prefix='/veiculos' +) + +# Rota de cancelamento (logica pra informar que o processo foi cancelado, esses vendedores que ficam crinando proposta pra dps cancelar é foda) +@veiculo_router.post( + path='/cancelar_proposta' +) +def cancelar_proposta( + dados_proposta_holmes: CancelamentoPropostaInput, + conexao: Conexao, +): + repo = CancelamentoProposta(conexao) + + cancelou = repo.cancelar_no_banco(dados_proposta_holmes.id) + + if not cancelou: + raise HTTPException( + status_code= status.HTTP_404_NOT_FOUND, + detail= "Proposta não encontrada no banco de dados" + ) + return { + 'mensagem': 'Proposta cancelada' + } \ No newline at end of file diff --git a/app/v1/modules/veiculo/services.py b/app/v1/modules/veiculo/services.py new file mode 100644 index 0000000..0db0bb5 --- /dev/null +++ b/app/v1/modules/veiculo/services.py @@ -0,0 +1,17 @@ +from sqlalchemy.pool import PoolProxiedConnection + + +class CancelamentoProposta: + def __init__(self, conexao: PoolProxiedConnection) -> None: + self.conexao = conexao + + def cancelar_no_banco(self, id_processo: str) -> bool: + cursor = self.conexao.cursor() + cursor.execute(""" + UPDATE HOLMES_FAT_ESTOQUE + SET STATUS = 'CANCELADO', DTA_ALTERACAO_STATUS = GETDATE() + WHERE ID_PROCESSO = ? + """, id_processo) + + self.conexao.commit() + return cursor.rowcount > 0 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..65c3906 --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +import uvicorn +from fastapi import FastAPI + +from app.core.configs import settings +from app.v1.api import api_router + +app = FastAPI( + title = 'API - Podium', + version= '0.0.1' +) + +app.include_router(api_router, prefix='/v1') + +if __name__ == '__main__': + uvicorn.run(app, host='127.0.0.1', port=settings.port) \ No newline at end of file