diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..7e661988685d0384a4401b4b10aaa71748c8b02e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +mer.jpg filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..68bc17f9ff2104a9d7b6777058bb4c343ca72609 --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# 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/ + +# 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/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6cfecc16aafcbeff7dc5dd0369654263ae855b6b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.11-slim + +# Instala PostgreSQL, utilitários e Make para poder rodar migrações via Makefile +RUN apt-get update && apt-get install -y \ + postgresql postgresql-contrib postgresql-client \ + make + +# Define variáveis default do banco (ajuste as credenciais conforme sua preferência) +ENV POSTGRES_USER=workout +ENV POSTGRES_PASSWORD=workout +ENV POSTGRES_DB=workout + +# Cria pasta de trabalho +WORKDIR /app + +# Copia dependências e instala +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copia todo o código da aplicação e scripts +COPY . /app + +# Dá permissão para o script de entrypoint +RUN chmod +x /app/entrypoint.sh + +# Comando para rodar ao iniciar o container +CMD ["/app/entrypoint.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..0f06b041cde422f8f311c31416951edf62e93b66 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +run: + @uvicorn workout_api.main:app --host 0.0.0.0 --port 7860 + +create-migrations: + @PYTHONPATH=$PYTHONPATH:$(pwd) alembic revision --autogenerate -m $(d) + +run-migrations: + @PYTHONPATH=$PYTHONPATH:$(pwd) alembic upgrade head diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..cf2f65a0da2f7e0041e04b0895414928e72d22c9 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,45 @@ +[alembic] + +script_location = alembic + +prepend_sys_path = . + +version_path_separator = os + +sqlalchemy.url = postgresql+asyncpg://workout:workout@localhost/workout + +[post_write_hooks] + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000000000000000000000000000000000000..98e4f9c44effe479ed38c66ba922e7bcc672916f --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/__pycache__/env.cpython-311.pyc b/alembic/__pycache__/env.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..387314d50eaf511ae7fc62cea6bb8ce22f40d39c Binary files /dev/null and b/alembic/__pycache__/env.cpython-311.pyc differ diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000000000000000000000000000000000000..50069ef103d1dec90e8543e1e4864252aa1155fd --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,59 @@ +import os +import asyncio +from logging.config import fileConfig + +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config +from sqlalchemy import pool + +from alembic import context +from workout_api.contrib.models import BaseModel +from workout_api.contrib.repository.models import * + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = BaseModel.metadata + +def get_url(): + # Busca a DATABASE_URL do ambiente, se não encontra usa alembic.ini + return os.getenv("DATABASE_URL") or config.get_main_option("sqlalchemy.url") + +def run_migrations_offline() -> None: + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + +async def run_async_migrations() -> None: + # Injeta a URL pela configuração dinâmica + section = config.get_section(config.config_ini_section, {}) + section["sqlalchemy.url"] = get_url() + connectable = async_engine_from_config( + section, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + +def run_migrations_online() -> None: + asyncio.run(run_async_migrations()) + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000000000000000000000000000000000000..55df2863d206fa1678abb4c92e90c45d3f85c114 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/__pycache__/c006e8463eb4_init_db.cpython-311.pyc b/alembic/versions/__pycache__/c006e8463eb4_init_db.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0319cdb37614fc26b4fe4f16cbfae3f5808af212 Binary files /dev/null and b/alembic/versions/__pycache__/c006e8463eb4_init_db.cpython-311.pyc differ diff --git a/alembic/versions/c006e8463eb4_init_db.py b/alembic/versions/c006e8463eb4_init_db.py new file mode 100644 index 0000000000000000000000000000000000000000..8e1a2cbbb561b371239329ff196e48711170f83f --- /dev/null +++ b/alembic/versions/c006e8463eb4_init_db.py @@ -0,0 +1,62 @@ +"""init_db + +Revision ID: c006e8463eb4 +Revises: +Create Date: 2023-07-27 19:13:13.567144 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c006e8463eb4' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('categorias', + sa.Column('pk_id', sa.Integer(), nullable=False), + sa.Column('nome', sa.String(length=50), nullable=False), + sa.Column('id', sa.UUID(), nullable=False), + sa.PrimaryKeyConstraint('pk_id'), + sa.UniqueConstraint('nome') + ) + op.create_table('centros_treinamento', + sa.Column('pk_id', sa.Integer(), nullable=False), + sa.Column('nome', sa.String(length=50), nullable=False), + sa.Column('endereco', sa.String(length=60), nullable=False), + sa.Column('proprietario', sa.String(length=30), nullable=False), + sa.Column('id', sa.UUID(), nullable=False), + sa.PrimaryKeyConstraint('pk_id'), + sa.UniqueConstraint('nome') + ) + op.create_table('atletas', + sa.Column('pk_id', sa.Integer(), nullable=False), + sa.Column('nome', sa.String(length=50), nullable=False), + sa.Column('cpf', sa.String(length=11), nullable=False), + sa.Column('idade', sa.Integer(), nullable=False), + sa.Column('peso', sa.Float(), nullable=False), + sa.Column('altura', sa.Float(), nullable=False), + sa.Column('sexo', sa.String(length=1), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('categoria_id', sa.Integer(), nullable=False), + sa.Column('centro_treinamento_id', sa.Integer(), nullable=False), + sa.Column('id', sa.UUID(), nullable=False), + sa.ForeignKeyConstraint(['categoria_id'], ['categorias.pk_id'], ), + sa.ForeignKeyConstraint(['centro_treinamento_id'], ['centros_treinamento.pk_id'], ), + sa.PrimaryKeyConstraint('pk_id'), + sa.UniqueConstraint('cpf') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('atletas') + op.drop_table('centros_treinamento') + op.drop_table('categorias') + # ### end Alembic commands ### diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000000000000000000000000000000000000..31cb7c0fb726246a74be7165f904a695b346e4fc --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Inicia o serviço Postgres em background +service postgresql start + +# Aguarda o banco de dados subir +sleep 5 + +# Cria usuário e o banco, se não existirem (idempotente) +su - postgres -c "psql -c \"CREATE USER $POSTGRES_USER WITH PASSWORD '$POSTGRES_PASSWORD';\"" 2>/dev/null +su - postgres -c "psql -c \"CREATE DATABASE $POSTGRES_DB OWNER $POSTGRES_USER;\"" 2>/dev/null + +# Exporta a variável DATABASE_URL para Alembic/FastAPI encontrarem +export DATABASE_URL="postgresql+asyncpg://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" + +# Roda as migrations para criar o schema (Alembic via Makefile) +make run-migrations + +# Inicia a FastAPI via Makefile (roda em 0.0.0.0:7860) +make run diff --git a/mer.jpg b/mer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e503bbb38495e1abcdb6b1a5805ceece639624e3 --- /dev/null +++ b/mer.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40117e2eb9366a58683f2f0dcb374b4cf4f6019494652641f8203ec113bf57ed +size 123652 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ebb5aba79047664029474321e26a0a301710c2f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +alembic==1.11.1 +annotated-types==0.5.0 +anyio==3.7.1 +asyncpg==0.28.0 +click==8.1.6 +fastapi==0.100.1 +greenlet==2.0.2 +h11==0.14.0 +idna==3.4 +Mako==1.2.4 +MarkupSafe==2.1.3 +pydantic==2.1.1 +pydantic_core==2.4.0 +sniffio==1.3.0 +SQLAlchemy==2.0.19 +starlette==0.27.0 +typing_extensions==4.15.0 +uvicorn==0.23.1 +pydantic-settings==2.0.3 +fastapi-pagination==0.14.3 diff --git a/workout_api/__init__.py b/workout_api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..53257f1ec3beaff20d3faab9fa0181906fdba61a --- /dev/null +++ b/workout_api/__init__.py @@ -0,0 +1,3 @@ +from workout_api.categorias.models import CategoriaModel +from workout_api.centro_treinamento.models import CentroTreinamentoModel +from workout_api.atleta.models import AtletaModel \ No newline at end of file diff --git a/workout_api/__pycache__/__init__.cpython-311.pyc b/workout_api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ede3abec846c32c1054660080a931e3167c2f1c7 Binary files /dev/null and b/workout_api/__pycache__/__init__.cpython-311.pyc differ diff --git a/workout_api/__pycache__/main.cpython-311.pyc b/workout_api/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79957f50d324736f4da5aa3b00a65042a6c37eb8 Binary files /dev/null and b/workout_api/__pycache__/main.cpython-311.pyc differ diff --git a/workout_api/__pycache__/routers.cpython-311.pyc b/workout_api/__pycache__/routers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73dcc3ad27135dfda93c7413e10095f5bf4cc8ca Binary files /dev/null and b/workout_api/__pycache__/routers.cpython-311.pyc differ diff --git a/workout_api/atleta/__init__.py b/workout_api/atleta/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/workout_api/atleta/__pycache__/__init__.cpython-311.pyc b/workout_api/atleta/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..543aa0f77460660b209a5a794b8f09aa5a3736ef Binary files /dev/null and b/workout_api/atleta/__pycache__/__init__.cpython-311.pyc differ diff --git a/workout_api/atleta/__pycache__/controller.cpython-311.pyc b/workout_api/atleta/__pycache__/controller.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52bed8b224021ab9ac436558bad979dbdb23ad00 Binary files /dev/null and b/workout_api/atleta/__pycache__/controller.cpython-311.pyc differ diff --git a/workout_api/atleta/__pycache__/models.cpython-311.pyc b/workout_api/atleta/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fff890486fc0fd03f0a9091a7887c95f27dc811c Binary files /dev/null and b/workout_api/atleta/__pycache__/models.cpython-311.pyc differ diff --git a/workout_api/atleta/__pycache__/schemas.cpython-311.pyc b/workout_api/atleta/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57b58e20f6d5d5ac6a953e49c5fdd37e993c41c6 Binary files /dev/null and b/workout_api/atleta/__pycache__/schemas.cpython-311.pyc differ diff --git a/workout_api/atleta/controller.py b/workout_api/atleta/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..39486d8d5f77539770b712331c6335e57f955783 --- /dev/null +++ b/workout_api/atleta/controller.py @@ -0,0 +1,177 @@ +from datetime import datetime +from uuid import uuid4 +from fastapi import APIRouter, Body, HTTPException, Query, status +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import selectinload +from pydantic import UUID4 +from fastapi_pagination import Page, paginate + +from workout_api.atleta.schemas import AtletaIn, AtletaOut, AtletaUpdate, AtletaCustomOut +from workout_api.atleta.models import AtletaModel +from workout_api.categorias.models import CategoriaModel +from workout_api.centro_treinamento.models import CentroTreinamentoModel + +from workout_api.contrib.dependencies import DatabaseDependency +from sqlalchemy.future import select + +router = APIRouter() + +@router.post( + '/', + summary='Criar um novo atleta', + status_code=status.HTTP_201_CREATED, + response_model=AtletaOut +) +async def post( + db_session: DatabaseDependency, + atleta_in: AtletaIn = Body(...) +): + categoria_nome = atleta_in.categoria.nome + centro_treinamento_nome = atleta_in.centro_treinamento.nome + + categoria = (await db_session.execute( + select(CategoriaModel).filter_by(nome=categoria_nome)) + ).scalars().first() + + if not categoria: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f'A categoria {categoria_nome} não foi encontrada.' + ) + + centro_treinamento = (await db_session.execute( + select(CentroTreinamentoModel).filter_by(nome=centro_treinamento_nome)) + ).scalars().first() + + if not centro_treinamento: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f'O centro de treinamento {centro_treinamento_nome} não foi encontrado.' + ) + try: + atleta_model = AtletaModel( + **atleta_in.model_dump(exclude={'categoria', 'centro_treinamento'}), + categoria_id=categoria.pk_id, + centro_treinamento_id=centro_treinamento.pk_id, + created_at=datetime.utcnow() + ) + + db_session.add(atleta_model) + await db_session.commit() + await db_session.refresh(atleta_model) + except IntegrityError: + await db_session.rollback() + raise HTTPException( + status_code=status.HTTP_303_SEE_OTHER, + detail=f"Já existe um atleta cadastrado com o cpf: {atleta_in.cpf}" + ) + except Exception as e: + await db_session.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Ocorreu um erro ao inserir os dados no banco: {e}' + ) + + return AtletaOut.model_validate(atleta_model) + + +@router.get( + '/', + summary='Consultar Atletas com filtros e paginação', + status_code=status.HTTP_200_OK, + response_model=Page[AtletaCustomOut], +) +async def query( + db_session: DatabaseDependency, + nome: str | None = Query(None, description="Filtrar atleta por nome"), + cpf: str | None = Query(None, description="Filtrar atleta por CPF") +) -> Page[AtletaCustomOut]: + stmt = select(AtletaModel).options( + selectinload(AtletaModel.categoria), + selectinload(AtletaModel.centro_treinamento) + ) + + if nome: + stmt = stmt.where(AtletaModel.nome.ilike(f"%{nome}%")) + + if cpf: + stmt = stmt.where(AtletaModel.cpf == cpf) + + atletas: list[AtletaModel] = (await db_session.execute(stmt)).scalars().all() + + atletas_custom = [ + AtletaCustomOut( + nome=atleta.nome, + categoria=atleta.categoria.nome, + centro_treinamento=atleta.centro_treinamento.nome + ) for atleta in atletas + ] + + return paginate(atletas_custom) + + +@router.get( + '/{id}', + summary='Consulta um Atleta pelo id', + status_code=status.HTTP_200_OK, + response_model=AtletaOut, +) +async def get(id: UUID4, db_session: DatabaseDependency) -> AtletaOut: + atleta_model: AtletaModel | None = ( + await db_session.execute(select(AtletaModel).filter_by(id=id)) + ).scalars().first() + + if not atleta_model: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Atleta não encontrado no id: {id}' + ) + + return AtletaOut.model_validate(atleta_model) + + +@router.patch( + '/{id}', + summary='Editar um Atleta pelo id', + status_code=status.HTTP_200_OK, + response_model=AtletaOut, +) +async def patch(id: UUID4, db_session: DatabaseDependency, atleta_up: AtletaUpdate = Body(...)) -> AtletaOut: + atleta_model: AtletaModel | None = ( + await db_session.execute(select(AtletaModel).filter_by(id=id)) + ).scalars().first() + + if not atleta_model: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Atleta não encontrado no id: {id}' + ) + + atleta_update = atleta_up.model_dump(exclude_unset=True) + for key, value in atleta_update.items(): + setattr(atleta_model, key, value) + + await db_session.commit() + await db_session.refresh(atleta_model) + + return AtletaOut.model_validate(atleta_model) + + +@router.delete( + '/{id}', + summary='Deletar um Atleta pelo id', + status_code=status.HTTP_204_NO_CONTENT +) +async def delete(id: UUID4, db_session: DatabaseDependency) -> None: + atleta_model: AtletaModel | None = ( + await db_session.execute(select(AtletaModel).filter_by(id=id)) + ).scalars().first() + + if not atleta_model: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Atleta não encontrado no id: {id}' + ) + + await db_session.delete(atleta_model) + await db_session.commit() diff --git a/workout_api/atleta/models.py b/workout_api/atleta/models.py new file mode 100644 index 0000000000000000000000000000000000000000..06262d833d4df1617cff5547ca9fd65f717df126 --- /dev/null +++ b/workout_api/atleta/models.py @@ -0,0 +1,21 @@ +from datetime import datetime +from sqlalchemy import DateTime, ForeignKey, Integer, String, Float +from sqlalchemy.orm import Mapped, mapped_column, relationship +from workout_api.contrib.models import BaseModel + + +class AtletaModel(BaseModel): + __tablename__ = 'atletas' + + pk_id: Mapped[int] = mapped_column(Integer, primary_key=True) + nome: Mapped[str] = mapped_column(String(50), nullable=False) + cpf: Mapped[str] = mapped_column(String(11), unique=True, nullable=False) + idade: Mapped[int] = mapped_column(Integer, nullable=False) + peso: Mapped[float] = mapped_column(Float, nullable=False) + altura: Mapped[float] = mapped_column(Float, nullable=False) + sexo: Mapped[str] = mapped_column(String(1), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + categoria: Mapped['CategoriaModel'] = relationship(back_populates="atletas", lazy='selectin') + categoria_id: Mapped[int] = mapped_column(ForeignKey("categorias.pk_id")) + centro_treinamento: Mapped['CentroTreinamentoModel'] = relationship(back_populates="atletas", lazy='selectin') + centro_treinamento_id: Mapped[int] = mapped_column(ForeignKey("centros_treinamento.pk_id")) \ No newline at end of file diff --git a/workout_api/atleta/schemas.py b/workout_api/atleta/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..a8cb517940f13498796450e46a02e4be1d87502b --- /dev/null +++ b/workout_api/atleta/schemas.py @@ -0,0 +1,36 @@ +from typing import Annotated, Optional +from pydantic import Field, PositiveFloat, BaseModel +from workout_api.categorias.schemas import CategoriaIn +from workout_api.centro_treinamento.schemas import CentroTreinamentoAtleta + +from workout_api.contrib.schemas import BaseSchema, OutMixin + + +class Atleta(BaseSchema): + nome: Annotated[str, Field(description='Nome do atleta', example='Joao', max_length=50)] + cpf: Annotated[str, Field(description='CPF do atleta', example='12345678900', max_length=11)] + idade: Annotated[int, Field(description='Idade do atleta', example=25)] + peso: Annotated[PositiveFloat, Field(description='Peso do atleta', example=75.5)] + altura: Annotated[PositiveFloat, Field(description='Altura do atleta', example=1.70)] + sexo: Annotated[str, Field(description='Sexo do atleta', example='M', max_length=1)] + categoria: Annotated[CategoriaIn, Field(description='Categoria do atleta')] + centro_treinamento: Annotated[CentroTreinamentoAtleta, Field(description='Centro de treinamento do atleta')] + + +class AtletaIn(Atleta): + pass + + +class AtletaOut(Atleta, OutMixin): + pass + +class AtletaUpdate(BaseSchema): + nome: Annotated[Optional[str], Field(None, description='Nome do atleta', example='Joao', max_length=50)] + idade: Annotated[Optional[int], Field(None, description='Idade do atleta', example=25)] + + +# NOVO: Response customizado para GET all paginado e filtros +class AtletaCustomOut(BaseModel): + nome: str + categoria: str + centro_treinamento: str \ No newline at end of file diff --git a/workout_api/categorias/__init__.py b/workout_api/categorias/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/workout_api/categorias/__pycache__/__init__.cpython-311.pyc b/workout_api/categorias/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5ffee00496a2e70ac7423497543998992a2c9af Binary files /dev/null and b/workout_api/categorias/__pycache__/__init__.cpython-311.pyc differ diff --git a/workout_api/categorias/__pycache__/controller.cpython-311.pyc b/workout_api/categorias/__pycache__/controller.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61971954569395158279c7f39eb0b1c82240f918 Binary files /dev/null and b/workout_api/categorias/__pycache__/controller.cpython-311.pyc differ diff --git a/workout_api/categorias/__pycache__/models.cpython-311.pyc b/workout_api/categorias/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f09e6d454d4d8492e9487d48125b7810eeeca77 Binary files /dev/null and b/workout_api/categorias/__pycache__/models.cpython-311.pyc differ diff --git a/workout_api/categorias/__pycache__/schemas.cpython-311.pyc b/workout_api/categorias/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d076cc8907bf43b2cce7ede8e9d555e41b65dee Binary files /dev/null and b/workout_api/categorias/__pycache__/schemas.cpython-311.pyc differ diff --git a/workout_api/categorias/controller.py b/workout_api/categorias/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..66ba1f536b639df189ba4fddb1f10f0915c45355 --- /dev/null +++ b/workout_api/categorias/controller.py @@ -0,0 +1,66 @@ +from fastapi import APIRouter, Body, HTTPException, status +from pydantic import UUID4 +from workout_api.categorias.schemas import CategoriaIn, CategoriaOut +from workout_api.categorias.models import CategoriaModel + +from workout_api.contrib.dependencies import DatabaseDependency +from sqlalchemy.future import select +from sqlalchemy.exc import IntegrityError + +router = APIRouter() + +@router.post( + '/', + summary='Criar uma nova Categoria', + status_code=status.HTTP_201_CREATED, + response_model=CategoriaOut, +) +async def post( + db_session: DatabaseDependency, + categoria_in: CategoriaIn = Body(...) +) -> CategoriaOut: + categoria_model = CategoriaModel(**categoria_in.model_dump()) + try: + db_session.add(categoria_model) + await db_session.commit() + await db_session.refresh(categoria_model) + except IntegrityError: + await db_session.rollback() + raise HTTPException( + status_code=status.HTTP_303_SEE_OTHER, + detail=f"Já existe uma categoria cadastrada com o nome: {categoria_in.nome}" + ) + + return CategoriaOut.model_validate(categoria_model) + + +@router.get( + '/', + summary='Consultar todas as Categorias', + status_code=status.HTTP_200_OK, + response_model=list[CategoriaOut], +) +async def query(db_session: DatabaseDependency) -> list[CategoriaOut]: + categorias: list[CategoriaModel] = (await db_session.execute(select(CategoriaModel))).scalars().all() + + return [CategoriaOut.model_validate(cat) for cat in categorias] + + +@router.get( + '/{id}', + summary='Consulta uma Categoria pelo id', + status_code=status.HTTP_200_OK, + response_model=CategoriaOut, +) +async def get(id: UUID4, db_session: DatabaseDependency) -> CategoriaOut: + categoria: CategoriaModel | None = ( + await db_session.execute(select(CategoriaModel).filter_by(id=id)) + ).scalars().first() + + if not categoria: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Categoria não encontrada no id: {id}' + ) + + return CategoriaOut.model_validate(categoria) \ No newline at end of file diff --git a/workout_api/categorias/models.py b/workout_api/categorias/models.py new file mode 100644 index 0000000000000000000000000000000000000000..9019700848adb4ac56d1ddacbfae65e0f4ce8b40 --- /dev/null +++ b/workout_api/categorias/models.py @@ -0,0 +1,10 @@ +from sqlalchemy import Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship +from workout_api.contrib.models import BaseModel + +class CategoriaModel(BaseModel): + __tablename__ = 'categorias' + + pk_id: Mapped[int] = mapped_column(Integer, primary_key=True) + nome: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) + atletas: Mapped[list['AtletaModel']] = relationship(back_populates="categoria") \ No newline at end of file diff --git a/workout_api/categorias/schemas.py b/workout_api/categorias/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..b384e3289c96d35eeed52eaad43208e6cb16f187 --- /dev/null +++ b/workout_api/categorias/schemas.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from pydantic import UUID4, Field +from workout_api.contrib.schemas import BaseSchema + + +class CategoriaIn(BaseSchema): + nome: Annotated[str, Field(description='Nome da categoria', example='Scale', max_length=10)] + + +class CategoriaOut(CategoriaIn): + id: Annotated[UUID4, Field(description='Identificador da categoria')] + diff --git a/workout_api/centro_treinamento/__init__.py b/workout_api/centro_treinamento/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/workout_api/centro_treinamento/__pycache__/__init__.cpython-311.pyc b/workout_api/centro_treinamento/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf50e14fccf406177994f67853dadc48cb216303 Binary files /dev/null and b/workout_api/centro_treinamento/__pycache__/__init__.cpython-311.pyc differ diff --git a/workout_api/centro_treinamento/__pycache__/controller.cpython-311.pyc b/workout_api/centro_treinamento/__pycache__/controller.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cae9908a43fec457b3d853111109e19c19b7237 Binary files /dev/null and b/workout_api/centro_treinamento/__pycache__/controller.cpython-311.pyc differ diff --git a/workout_api/centro_treinamento/__pycache__/models.cpython-311.pyc b/workout_api/centro_treinamento/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b3770901cd963ac1f79c3bffd7223a9d7d35ee8 Binary files /dev/null and b/workout_api/centro_treinamento/__pycache__/models.cpython-311.pyc differ diff --git a/workout_api/centro_treinamento/__pycache__/schemas.cpython-311.pyc b/workout_api/centro_treinamento/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..501d7311cb954e136e07142cbcd522f679247279 Binary files /dev/null and b/workout_api/centro_treinamento/__pycache__/schemas.cpython-311.pyc differ diff --git a/workout_api/centro_treinamento/controller.py b/workout_api/centro_treinamento/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..a3dd87b71dba860299dbafbe080d9774f46f5431 --- /dev/null +++ b/workout_api/centro_treinamento/controller.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, Body, HTTPException, status +from pydantic import UUID4 +from workout_api.centro_treinamento.schemas import CentroTreinamentoIn, CentroTreinamentoOut +from workout_api.centro_treinamento.models import CentroTreinamentoModel + +from workout_api.contrib.dependencies import DatabaseDependency +from sqlalchemy.future import select +from sqlalchemy.exc import IntegrityError + +router = APIRouter() + +@router.post( + '/', + summary='Criar um novo Centro de treinamento', + status_code=status.HTTP_201_CREATED, + response_model=CentroTreinamentoOut, +) +async def post( + db_session: DatabaseDependency, + centro_treinamento_in: CentroTreinamentoIn = Body(...) +) -> CentroTreinamentoOut: + centro_treinamento_model = CentroTreinamentoModel(**centro_treinamento_in.model_dump()) + try: + db_session.add(centro_treinamento_model) + await db_session.commit() + await db_session.refresh(centro_treinamento_model) + except IntegrityError: + await db_session.rollback() + raise HTTPException( + status_code=status.HTTP_303_SEE_OTHER, + detail=f"Já existe um centro de treinamento cadastrado com o nome: {centro_treinamento_in.nome}" + ) + + return CentroTreinamentoOut.model_validate(centro_treinamento_model) + + +@router.get( + '/', + summary='Consultar todos os centros de treinamento', + status_code=status.HTTP_200_OK, + response_model=list[CentroTreinamentoOut], +) +async def query(db_session: DatabaseDependency) -> list[CentroTreinamentoOut]: + centros_treinamento: list[CentroTreinamentoModel] = ( + await db_session.execute(select(CentroTreinamentoModel)) + ).scalars().all() + + return [CentroTreinamentoOut.model_validate(ct) for ct in centros_treinamento] + + +@router.get( + '/{id}', + summary='Consulta um centro de treinamento pelo id', + status_code=status.HTTP_200_OK, + response_model=CentroTreinamentoOut, +) +async def get(id: UUID4, db_session: DatabaseDependency) -> CentroTreinamentoOut: + centro_treinamento: CentroTreinamentoModel | None = ( + await db_session.execute(select(CentroTreinamentoModel).filter_by(id=id)) + ).scalars().first() + + if not centro_treinamento: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Centro de treinamento não encontrado no id: {id}' + ) + + return CentroTreinamentoOut.model_validate(centro_treinamento) \ No newline at end of file diff --git a/workout_api/centro_treinamento/models.py b/workout_api/centro_treinamento/models.py new file mode 100644 index 0000000000000000000000000000000000000000..58f735d2ccf8b3aa106b06dd70a396578ac5c421 --- /dev/null +++ b/workout_api/centro_treinamento/models.py @@ -0,0 +1,12 @@ +from sqlalchemy import Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship +from workout_api.contrib.models import BaseModel + +class CentroTreinamentoModel(BaseModel): + __tablename__ = 'centros_treinamento' + + pk_id: Mapped[int] = mapped_column(Integer, primary_key=True) + nome: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) + endereco: Mapped[str] = mapped_column(String(60), nullable=False) + proprietario: Mapped[str] = mapped_column(String(30), nullable=False) + atletas: Mapped[list['AtletaModel']] = relationship(back_populates="centro_treinamento") \ No newline at end of file diff --git a/workout_api/centro_treinamento/schemas.py b/workout_api/centro_treinamento/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..449bf55d23d7b07d0bd36baac90706d335db0785 --- /dev/null +++ b/workout_api/centro_treinamento/schemas.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from pydantic import Field, UUID4 +from workout_api.contrib.schemas import BaseSchema + + +class CentroTreinamentoIn(BaseSchema): + nome: Annotated[str, Field(description='Nome do centro de treinamento', example='CT King', max_length=20)] + endereco: Annotated[str, Field(description='Endereço do centro de treinamento', example='Rua X, Q02', max_length=60)] + proprietario: Annotated[str, Field(description='Proprietario do centro de treinamento', example='Marcos', max_length=30)] + + +class CentroTreinamentoAtleta(BaseSchema): + nome: Annotated[str, Field(description='Nome do centro de treinamento', example='CT King', max_length=20)] + + +class CentroTreinamentoOut(CentroTreinamentoIn): + id: Annotated[UUID4, Field(description='Identificador do centro de treinamento')] \ No newline at end of file diff --git a/workout_api/configs/__init__.py b/workout_api/configs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/workout_api/configs/__pycache__/__init__.cpython-311.pyc b/workout_api/configs/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43a091974fb26ed5651e4aa5c4c50495e926be09 Binary files /dev/null and b/workout_api/configs/__pycache__/__init__.cpython-311.pyc differ diff --git a/workout_api/configs/__pycache__/database.cpython-311.pyc b/workout_api/configs/__pycache__/database.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0951445e9f38bda04f9530670f2e80980b84094 Binary files /dev/null and b/workout_api/configs/__pycache__/database.cpython-311.pyc differ diff --git a/workout_api/configs/__pycache__/settings.cpython-311.pyc b/workout_api/configs/__pycache__/settings.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6feb95c70ce22e726c0418a98778464ea2cb3ee3 Binary files /dev/null and b/workout_api/configs/__pycache__/settings.cpython-311.pyc differ diff --git a/workout_api/configs/database.py b/workout_api/configs/database.py new file mode 100644 index 0000000000000000000000000000000000000000..327fac7c996ed3874900c5ad28be322ab88b0d01 --- /dev/null +++ b/workout_api/configs/database.py @@ -0,0 +1,15 @@ +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from workout_api.configs.settings import settings + + +engine = create_async_engine(settings.DB_URL, echo=False) +async_session = sessionmaker( + engine, class_=AsyncSession, expire_on_commit=False +) + +async def get_session() -> AsyncGenerator: + async with async_session() as session: + yield session \ No newline at end of file diff --git a/workout_api/configs/settings.py b/workout_api/configs/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..0f6452e27e71a056542aaa5ce8e98ef8a37da1bf --- /dev/null +++ b/workout_api/configs/settings.py @@ -0,0 +1,8 @@ +from pydantic import Field +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + DB_URL: str = Field(default='postgresql+asyncpg://workout:workout@localhost/workout') + + +settings = Settings() \ No newline at end of file diff --git a/workout_api/contrib/__init__.py b/workout_api/contrib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/workout_api/contrib/__pycache__/__init__.cpython-311.pyc b/workout_api/contrib/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20454194aa9f86874c6ea499a25c7bd73f4f946f Binary files /dev/null and b/workout_api/contrib/__pycache__/__init__.cpython-311.pyc differ diff --git a/workout_api/contrib/__pycache__/dependencies.cpython-311.pyc b/workout_api/contrib/__pycache__/dependencies.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5fe4e2ebb1a182036b1e063415e6b270a8eec5a6 Binary files /dev/null and b/workout_api/contrib/__pycache__/dependencies.cpython-311.pyc differ diff --git a/workout_api/contrib/__pycache__/models.cpython-311.pyc b/workout_api/contrib/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8465263477e370f38c70bd8ab6e215cd929c6fdd Binary files /dev/null and b/workout_api/contrib/__pycache__/models.cpython-311.pyc differ diff --git a/workout_api/contrib/__pycache__/schemas.cpython-311.pyc b/workout_api/contrib/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41d370e21c9be5bd5c411e0072c8e939785b5165 Binary files /dev/null and b/workout_api/contrib/__pycache__/schemas.cpython-311.pyc differ diff --git a/workout_api/contrib/dependencies.py b/workout_api/contrib/dependencies.py new file mode 100644 index 0000000000000000000000000000000000000000..354f444abf395ec048b1f2cd70d346933c2e85be --- /dev/null +++ b/workout_api/contrib/dependencies.py @@ -0,0 +1,8 @@ +from typing import Annotated +from fastapi import Depends + +from sqlalchemy.ext.asyncio import AsyncSession + +from workout_api.configs.database import get_session + +DatabaseDependency = Annotated[AsyncSession, Depends(get_session)] \ No newline at end of file diff --git a/workout_api/contrib/models.py b/workout_api/contrib/models.py new file mode 100644 index 0000000000000000000000000000000000000000..1fb30b9a0852ca24b5b059e66a7bcdb11a4da127 --- /dev/null +++ b/workout_api/contrib/models.py @@ -0,0 +1,8 @@ +from uuid import uuid4 +from sqlalchemy import UUID +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.dialects.postgresql import UUID as PG_UUID + + +class BaseModel(DeclarativeBase): + id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid4, nullable=False) \ No newline at end of file diff --git a/workout_api/contrib/repository/__init__.py b/workout_api/contrib/repository/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/workout_api/contrib/repository/__pycache__/__init__.cpython-311.pyc b/workout_api/contrib/repository/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b794e92b943045724f22242431ce4a1d6dd1e1e Binary files /dev/null and b/workout_api/contrib/repository/__pycache__/__init__.cpython-311.pyc differ diff --git a/workout_api/contrib/repository/__pycache__/models.cpython-311.pyc b/workout_api/contrib/repository/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7af26d902013574882cc756c7a703bdb3c3e0e5c Binary files /dev/null and b/workout_api/contrib/repository/__pycache__/models.cpython-311.pyc differ diff --git a/workout_api/contrib/repository/models.py b/workout_api/contrib/repository/models.py new file mode 100644 index 0000000000000000000000000000000000000000..72c71dee15f696b53e7b16ab317bbc553bc6c305 --- /dev/null +++ b/workout_api/contrib/repository/models.py @@ -0,0 +1,3 @@ +from workout_api.categorias.models import CategoriaModel +from workout_api.atleta.models import AtletaModel +from workout_api.centro_treinamento.models import CentroTreinamentoModel diff --git a/workout_api/contrib/schemas.py b/workout_api/contrib/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..5413f64e351f0aa182aeaba2e064d3285c9725e6 --- /dev/null +++ b/workout_api/contrib/schemas.py @@ -0,0 +1,14 @@ +from typing import Annotated +from pydantic import UUID4, BaseModel, Field +from datetime import datetime + + +class BaseSchema(BaseModel): + class Config: + extra = 'forbid' + from_attributes = True + + +class OutMixin(BaseSchema): + id: Annotated[UUID4, Field(description='Identificador')] + created_at: Annotated[datetime, Field(description='Data de criação')] \ No newline at end of file diff --git a/workout_api/main.py b/workout_api/main.py new file mode 100644 index 0000000000000000000000000000000000000000..fc23f7c73485c5153f984d1d50591cf708559112 --- /dev/null +++ b/workout_api/main.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI +from workout_api.routers import api_router +from fastapi_pagination import add_pagination + +app = FastAPI(title='WorkoutApi') +app.include_router(api_router) +add_pagination(app) \ No newline at end of file diff --git a/workout_api/routers.py b/workout_api/routers.py new file mode 100644 index 0000000000000000000000000000000000000000..e1a8f253dc964fceb41f96129719069db633c496 --- /dev/null +++ b/workout_api/routers.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter +from workout_api.atleta.controller import router as atleta +from workout_api.categorias.controller import router as categorias +from workout_api.centro_treinamento.controller import router as centro_treinamento + +api_router = APIRouter() +api_router.include_router(atleta, prefix='/atletas', tags=['atletas']) +api_router.include_router(categorias, prefix='/categorias', tags=['categorias']) +api_router.include_router(centro_treinamento, prefix='/centros_treinamento', tags=['centros_treinamento']) \ No newline at end of file