Spaces:
Sleeping
Sleeping
Upload 61 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- .gitignore +160 -0
- Dockerfile +27 -0
- Makefile +8 -0
- alembic.ini +45 -0
- alembic/README +1 -0
- alembic/__pycache__/env.cpython-311.pyc +0 -0
- alembic/env.py +59 -0
- alembic/script.py.mako +24 -0
- alembic/versions/__pycache__/c006e8463eb4_init_db.cpython-311.pyc +0 -0
- alembic/versions/c006e8463eb4_init_db.py +62 -0
- entrypoint.sh +20 -0
- mer.jpg +3 -0
- requirements.txt +20 -0
- workout_api/__init__.py +3 -0
- workout_api/__pycache__/__init__.cpython-311.pyc +0 -0
- workout_api/__pycache__/main.cpython-311.pyc +0 -0
- workout_api/__pycache__/routers.cpython-311.pyc +0 -0
- workout_api/atleta/__init__.py +0 -0
- workout_api/atleta/__pycache__/__init__.cpython-311.pyc +0 -0
- workout_api/atleta/__pycache__/controller.cpython-311.pyc +0 -0
- workout_api/atleta/__pycache__/models.cpython-311.pyc +0 -0
- workout_api/atleta/__pycache__/schemas.cpython-311.pyc +0 -0
- workout_api/atleta/controller.py +177 -0
- workout_api/atleta/models.py +21 -0
- workout_api/atleta/schemas.py +36 -0
- workout_api/categorias/__init__.py +0 -0
- workout_api/categorias/__pycache__/__init__.cpython-311.pyc +0 -0
- workout_api/categorias/__pycache__/controller.cpython-311.pyc +0 -0
- workout_api/categorias/__pycache__/models.cpython-311.pyc +0 -0
- workout_api/categorias/__pycache__/schemas.cpython-311.pyc +0 -0
- workout_api/categorias/controller.py +66 -0
- workout_api/categorias/models.py +10 -0
- workout_api/categorias/schemas.py +13 -0
- workout_api/centro_treinamento/__init__.py +0 -0
- workout_api/centro_treinamento/__pycache__/__init__.cpython-311.pyc +0 -0
- workout_api/centro_treinamento/__pycache__/controller.cpython-311.pyc +0 -0
- workout_api/centro_treinamento/__pycache__/models.cpython-311.pyc +0 -0
- workout_api/centro_treinamento/__pycache__/schemas.cpython-311.pyc +0 -0
- workout_api/centro_treinamento/controller.py +68 -0
- workout_api/centro_treinamento/models.py +12 -0
- workout_api/centro_treinamento/schemas.py +18 -0
- workout_api/configs/__init__.py +0 -0
- workout_api/configs/__pycache__/__init__.cpython-311.pyc +0 -0
- workout_api/configs/__pycache__/database.cpython-311.pyc +0 -0
- workout_api/configs/__pycache__/settings.cpython-311.pyc +0 -0
- workout_api/configs/database.py +15 -0
- workout_api/configs/settings.py +8 -0
- workout_api/contrib/__init__.py +0 -0
- workout_api/contrib/__pycache__/__init__.cpython-311.pyc +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
mer.jpg filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py,cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# poetry
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 102 |
+
#poetry.lock
|
| 103 |
+
|
| 104 |
+
# pdm
|
| 105 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 106 |
+
#pdm.lock
|
| 107 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 108 |
+
# in version control.
|
| 109 |
+
# https://pdm.fming.dev/#use-with-ide
|
| 110 |
+
.pdm.toml
|
| 111 |
+
|
| 112 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 113 |
+
__pypackages__/
|
| 114 |
+
|
| 115 |
+
# Celery stuff
|
| 116 |
+
celerybeat-schedule
|
| 117 |
+
celerybeat.pid
|
| 118 |
+
|
| 119 |
+
# SageMath parsed files
|
| 120 |
+
*.sage.py
|
| 121 |
+
|
| 122 |
+
# Environments
|
| 123 |
+
.env
|
| 124 |
+
.venv
|
| 125 |
+
env/
|
| 126 |
+
venv/
|
| 127 |
+
ENV/
|
| 128 |
+
env.bak/
|
| 129 |
+
venv.bak/
|
| 130 |
+
|
| 131 |
+
# Spyder project settings
|
| 132 |
+
.spyderproject
|
| 133 |
+
.spyproject
|
| 134 |
+
|
| 135 |
+
# Rope project settings
|
| 136 |
+
.ropeproject
|
| 137 |
+
|
| 138 |
+
# mkdocs documentation
|
| 139 |
+
/site
|
| 140 |
+
|
| 141 |
+
# mypy
|
| 142 |
+
.mypy_cache/
|
| 143 |
+
.dmypy.json
|
| 144 |
+
dmypy.json
|
| 145 |
+
|
| 146 |
+
# Pyre type checker
|
| 147 |
+
.pyre/
|
| 148 |
+
|
| 149 |
+
# pytype static type analyzer
|
| 150 |
+
.pytype/
|
| 151 |
+
|
| 152 |
+
# Cython debug symbols
|
| 153 |
+
cython_debug/
|
| 154 |
+
|
| 155 |
+
# PyCharm
|
| 156 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 157 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 158 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 159 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 160 |
+
#.idea/
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Instala PostgreSQL, utilitários e Make para poder rodar migrações via Makefile
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
postgresql postgresql-contrib postgresql-client \
|
| 6 |
+
make
|
| 7 |
+
|
| 8 |
+
# Define variáveis default do banco (ajuste as credenciais conforme sua preferência)
|
| 9 |
+
ENV POSTGRES_USER=workout
|
| 10 |
+
ENV POSTGRES_PASSWORD=workout
|
| 11 |
+
ENV POSTGRES_DB=workout
|
| 12 |
+
|
| 13 |
+
# Cria pasta de trabalho
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
# Copia dependências e instala
|
| 17 |
+
COPY requirements.txt .
|
| 18 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 19 |
+
|
| 20 |
+
# Copia todo o código da aplicação e scripts
|
| 21 |
+
COPY . /app
|
| 22 |
+
|
| 23 |
+
# Dá permissão para o script de entrypoint
|
| 24 |
+
RUN chmod +x /app/entrypoint.sh
|
| 25 |
+
|
| 26 |
+
# Comando para rodar ao iniciar o container
|
| 27 |
+
CMD ["/app/entrypoint.sh"]
|
Makefile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
@uvicorn workout_api.main:app --host 0.0.0.0 --port 7860
|
| 3 |
+
|
| 4 |
+
create-migrations:
|
| 5 |
+
@PYTHONPATH=$PYTHONPATH:$(pwd) alembic revision --autogenerate -m $(d)
|
| 6 |
+
|
| 7 |
+
run-migrations:
|
| 8 |
+
@PYTHONPATH=$PYTHONPATH:$(pwd) alembic upgrade head
|
alembic.ini
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[alembic]
|
| 2 |
+
|
| 3 |
+
script_location = alembic
|
| 4 |
+
|
| 5 |
+
prepend_sys_path = .
|
| 6 |
+
|
| 7 |
+
version_path_separator = os
|
| 8 |
+
|
| 9 |
+
sqlalchemy.url = postgresql+asyncpg://workout:workout@localhost/workout
|
| 10 |
+
|
| 11 |
+
[post_write_hooks]
|
| 12 |
+
|
| 13 |
+
[loggers]
|
| 14 |
+
keys = root,sqlalchemy,alembic
|
| 15 |
+
|
| 16 |
+
[handlers]
|
| 17 |
+
keys = console
|
| 18 |
+
|
| 19 |
+
[formatters]
|
| 20 |
+
keys = generic
|
| 21 |
+
|
| 22 |
+
[logger_root]
|
| 23 |
+
level = WARN
|
| 24 |
+
handlers = console
|
| 25 |
+
qualname =
|
| 26 |
+
|
| 27 |
+
[logger_sqlalchemy]
|
| 28 |
+
level = WARN
|
| 29 |
+
handlers =
|
| 30 |
+
qualname = sqlalchemy.engine
|
| 31 |
+
|
| 32 |
+
[logger_alembic]
|
| 33 |
+
level = INFO
|
| 34 |
+
handlers =
|
| 35 |
+
qualname = alembic
|
| 36 |
+
|
| 37 |
+
[handler_console]
|
| 38 |
+
class = StreamHandler
|
| 39 |
+
args = (sys.stderr,)
|
| 40 |
+
level = NOTSET
|
| 41 |
+
formatter = generic
|
| 42 |
+
|
| 43 |
+
[formatter_generic]
|
| 44 |
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
| 45 |
+
datefmt = %H:%M:%S
|
alembic/README
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Generic single-database configuration.
|
alembic/__pycache__/env.cpython-311.pyc
ADDED
|
Binary file (3.4 kB). View file
|
|
|
alembic/env.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import asyncio
|
| 3 |
+
from logging.config import fileConfig
|
| 4 |
+
|
| 5 |
+
from sqlalchemy.engine import Connection
|
| 6 |
+
from sqlalchemy.ext.asyncio import async_engine_from_config
|
| 7 |
+
from sqlalchemy import pool
|
| 8 |
+
|
| 9 |
+
from alembic import context
|
| 10 |
+
from workout_api.contrib.models import BaseModel
|
| 11 |
+
from workout_api.contrib.repository.models import *
|
| 12 |
+
|
| 13 |
+
config = context.config
|
| 14 |
+
|
| 15 |
+
if config.config_file_name is not None:
|
| 16 |
+
fileConfig(config.config_file_name)
|
| 17 |
+
|
| 18 |
+
target_metadata = BaseModel.metadata
|
| 19 |
+
|
| 20 |
+
def get_url():
|
| 21 |
+
# Busca a DATABASE_URL do ambiente, se não encontra usa alembic.ini
|
| 22 |
+
return os.getenv("DATABASE_URL") or config.get_main_option("sqlalchemy.url")
|
| 23 |
+
|
| 24 |
+
def run_migrations_offline() -> None:
|
| 25 |
+
url = get_url()
|
| 26 |
+
context.configure(
|
| 27 |
+
url=url,
|
| 28 |
+
target_metadata=target_metadata,
|
| 29 |
+
literal_binds=True,
|
| 30 |
+
dialect_opts={"paramstyle": "named"},
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
with context.begin_transaction():
|
| 34 |
+
context.run_migrations()
|
| 35 |
+
|
| 36 |
+
def do_run_migrations(connection: Connection) -> None:
|
| 37 |
+
context.configure(connection=connection, target_metadata=target_metadata)
|
| 38 |
+
with context.begin_transaction():
|
| 39 |
+
context.run_migrations()
|
| 40 |
+
|
| 41 |
+
async def run_async_migrations() -> None:
|
| 42 |
+
# Injeta a URL pela configuração dinâmica
|
| 43 |
+
section = config.get_section(config.config_ini_section, {})
|
| 44 |
+
section["sqlalchemy.url"] = get_url()
|
| 45 |
+
connectable = async_engine_from_config(
|
| 46 |
+
section,
|
| 47 |
+
prefix="sqlalchemy.",
|
| 48 |
+
poolclass=pool.NullPool,
|
| 49 |
+
)
|
| 50 |
+
async with connectable.connect() as connection:
|
| 51 |
+
await connection.run_sync(do_run_migrations)
|
| 52 |
+
|
| 53 |
+
def run_migrations_online() -> None:
|
| 54 |
+
asyncio.run(run_async_migrations())
|
| 55 |
+
|
| 56 |
+
if context.is_offline_mode():
|
| 57 |
+
run_migrations_offline()
|
| 58 |
+
else:
|
| 59 |
+
run_migrations_online()
|
alembic/script.py.mako
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""${message}
|
| 2 |
+
|
| 3 |
+
Revision ID: ${up_revision}
|
| 4 |
+
Revises: ${down_revision | comma,n}
|
| 5 |
+
Create Date: ${create_date}
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from alembic import op
|
| 9 |
+
import sqlalchemy as sa
|
| 10 |
+
${imports if imports else ""}
|
| 11 |
+
|
| 12 |
+
# revision identifiers, used by Alembic.
|
| 13 |
+
revision = ${repr(up_revision)}
|
| 14 |
+
down_revision = ${repr(down_revision)}
|
| 15 |
+
branch_labels = ${repr(branch_labels)}
|
| 16 |
+
depends_on = ${repr(depends_on)}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def upgrade() -> None:
|
| 20 |
+
${upgrades if upgrades else "pass"}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def downgrade() -> None:
|
| 24 |
+
${downgrades if downgrades else "pass"}
|
alembic/versions/__pycache__/c006e8463eb4_init_db.cpython-311.pyc
ADDED
|
Binary file (4.03 kB). View file
|
|
|
alembic/versions/c006e8463eb4_init_db.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""init_db
|
| 2 |
+
|
| 3 |
+
Revision ID: c006e8463eb4
|
| 4 |
+
Revises:
|
| 5 |
+
Create Date: 2023-07-27 19:13:13.567144
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from alembic import op
|
| 9 |
+
import sqlalchemy as sa
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# revision identifiers, used by Alembic.
|
| 13 |
+
revision = 'c006e8463eb4'
|
| 14 |
+
down_revision = None
|
| 15 |
+
branch_labels = None
|
| 16 |
+
depends_on = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def upgrade() -> None:
|
| 20 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 21 |
+
op.create_table('categorias',
|
| 22 |
+
sa.Column('pk_id', sa.Integer(), nullable=False),
|
| 23 |
+
sa.Column('nome', sa.String(length=50), nullable=False),
|
| 24 |
+
sa.Column('id', sa.UUID(), nullable=False),
|
| 25 |
+
sa.PrimaryKeyConstraint('pk_id'),
|
| 26 |
+
sa.UniqueConstraint('nome')
|
| 27 |
+
)
|
| 28 |
+
op.create_table('centros_treinamento',
|
| 29 |
+
sa.Column('pk_id', sa.Integer(), nullable=False),
|
| 30 |
+
sa.Column('nome', sa.String(length=50), nullable=False),
|
| 31 |
+
sa.Column('endereco', sa.String(length=60), nullable=False),
|
| 32 |
+
sa.Column('proprietario', sa.String(length=30), nullable=False),
|
| 33 |
+
sa.Column('id', sa.UUID(), nullable=False),
|
| 34 |
+
sa.PrimaryKeyConstraint('pk_id'),
|
| 35 |
+
sa.UniqueConstraint('nome')
|
| 36 |
+
)
|
| 37 |
+
op.create_table('atletas',
|
| 38 |
+
sa.Column('pk_id', sa.Integer(), nullable=False),
|
| 39 |
+
sa.Column('nome', sa.String(length=50), nullable=False),
|
| 40 |
+
sa.Column('cpf', sa.String(length=11), nullable=False),
|
| 41 |
+
sa.Column('idade', sa.Integer(), nullable=False),
|
| 42 |
+
sa.Column('peso', sa.Float(), nullable=False),
|
| 43 |
+
sa.Column('altura', sa.Float(), nullable=False),
|
| 44 |
+
sa.Column('sexo', sa.String(length=1), nullable=False),
|
| 45 |
+
sa.Column('created_at', sa.DateTime(), nullable=False),
|
| 46 |
+
sa.Column('categoria_id', sa.Integer(), nullable=False),
|
| 47 |
+
sa.Column('centro_treinamento_id', sa.Integer(), nullable=False),
|
| 48 |
+
sa.Column('id', sa.UUID(), nullable=False),
|
| 49 |
+
sa.ForeignKeyConstraint(['categoria_id'], ['categorias.pk_id'], ),
|
| 50 |
+
sa.ForeignKeyConstraint(['centro_treinamento_id'], ['centros_treinamento.pk_id'], ),
|
| 51 |
+
sa.PrimaryKeyConstraint('pk_id'),
|
| 52 |
+
sa.UniqueConstraint('cpf')
|
| 53 |
+
)
|
| 54 |
+
# ### end Alembic commands ###
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def downgrade() -> None:
|
| 58 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 59 |
+
op.drop_table('atletas')
|
| 60 |
+
op.drop_table('centros_treinamento')
|
| 61 |
+
op.drop_table('categorias')
|
| 62 |
+
# ### end Alembic commands ###
|
entrypoint.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Inicia o serviço Postgres em background
|
| 4 |
+
service postgresql start
|
| 5 |
+
|
| 6 |
+
# Aguarda o banco de dados subir
|
| 7 |
+
sleep 5
|
| 8 |
+
|
| 9 |
+
# Cria usuário e o banco, se não existirem (idempotente)
|
| 10 |
+
su - postgres -c "psql -c \"CREATE USER $POSTGRES_USER WITH PASSWORD '$POSTGRES_PASSWORD';\"" 2>/dev/null
|
| 11 |
+
su - postgres -c "psql -c \"CREATE DATABASE $POSTGRES_DB OWNER $POSTGRES_USER;\"" 2>/dev/null
|
| 12 |
+
|
| 13 |
+
# Exporta a variável DATABASE_URL para Alembic/FastAPI encontrarem
|
| 14 |
+
export DATABASE_URL="postgresql+asyncpg://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB"
|
| 15 |
+
|
| 16 |
+
# Roda as migrations para criar o schema (Alembic via Makefile)
|
| 17 |
+
make run-migrations
|
| 18 |
+
|
| 19 |
+
# Inicia a FastAPI via Makefile (roda em 0.0.0.0:7860)
|
| 20 |
+
make run
|
mer.jpg
ADDED
|
Git LFS Details
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
alembic==1.11.1
|
| 2 |
+
annotated-types==0.5.0
|
| 3 |
+
anyio==3.7.1
|
| 4 |
+
asyncpg==0.28.0
|
| 5 |
+
click==8.1.6
|
| 6 |
+
fastapi==0.100.1
|
| 7 |
+
greenlet==2.0.2
|
| 8 |
+
h11==0.14.0
|
| 9 |
+
idna==3.4
|
| 10 |
+
Mako==1.2.4
|
| 11 |
+
MarkupSafe==2.1.3
|
| 12 |
+
pydantic==2.1.1
|
| 13 |
+
pydantic_core==2.4.0
|
| 14 |
+
sniffio==1.3.0
|
| 15 |
+
SQLAlchemy==2.0.19
|
| 16 |
+
starlette==0.27.0
|
| 17 |
+
typing_extensions==4.15.0
|
| 18 |
+
uvicorn==0.23.1
|
| 19 |
+
pydantic-settings==2.0.3
|
| 20 |
+
fastapi-pagination==0.14.3
|
workout_api/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from workout_api.categorias.models import CategoriaModel
|
| 2 |
+
from workout_api.centro_treinamento.models import CentroTreinamentoModel
|
| 3 |
+
from workout_api.atleta.models import AtletaModel
|
workout_api/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (415 Bytes). View file
|
|
|
workout_api/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (421 Bytes). View file
|
|
|
workout_api/__pycache__/routers.cpython-311.pyc
ADDED
|
Binary file (860 Bytes). View file
|
|
|
workout_api/atleta/__init__.py
ADDED
|
File without changes
|
workout_api/atleta/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (163 Bytes). View file
|
|
|
workout_api/atleta/__pycache__/controller.cpython-311.pyc
ADDED
|
Binary file (7.64 kB). View file
|
|
|
workout_api/atleta/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (2.42 kB). View file
|
|
|
workout_api/atleta/__pycache__/schemas.cpython-311.pyc
ADDED
|
Binary file (2.84 kB). View file
|
|
|
workout_api/atleta/controller.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from uuid import uuid4
|
| 3 |
+
from fastapi import APIRouter, Body, HTTPException, Query, status
|
| 4 |
+
from sqlalchemy.exc import IntegrityError
|
| 5 |
+
from sqlalchemy.orm import selectinload
|
| 6 |
+
from pydantic import UUID4
|
| 7 |
+
from fastapi_pagination import Page, paginate
|
| 8 |
+
|
| 9 |
+
from workout_api.atleta.schemas import AtletaIn, AtletaOut, AtletaUpdate, AtletaCustomOut
|
| 10 |
+
from workout_api.atleta.models import AtletaModel
|
| 11 |
+
from workout_api.categorias.models import CategoriaModel
|
| 12 |
+
from workout_api.centro_treinamento.models import CentroTreinamentoModel
|
| 13 |
+
|
| 14 |
+
from workout_api.contrib.dependencies import DatabaseDependency
|
| 15 |
+
from sqlalchemy.future import select
|
| 16 |
+
|
| 17 |
+
router = APIRouter()
|
| 18 |
+
|
| 19 |
+
@router.post(
|
| 20 |
+
'/',
|
| 21 |
+
summary='Criar um novo atleta',
|
| 22 |
+
status_code=status.HTTP_201_CREATED,
|
| 23 |
+
response_model=AtletaOut
|
| 24 |
+
)
|
| 25 |
+
async def post(
|
| 26 |
+
db_session: DatabaseDependency,
|
| 27 |
+
atleta_in: AtletaIn = Body(...)
|
| 28 |
+
):
|
| 29 |
+
categoria_nome = atleta_in.categoria.nome
|
| 30 |
+
centro_treinamento_nome = atleta_in.centro_treinamento.nome
|
| 31 |
+
|
| 32 |
+
categoria = (await db_session.execute(
|
| 33 |
+
select(CategoriaModel).filter_by(nome=categoria_nome))
|
| 34 |
+
).scalars().first()
|
| 35 |
+
|
| 36 |
+
if not categoria:
|
| 37 |
+
raise HTTPException(
|
| 38 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 39 |
+
detail=f'A categoria {categoria_nome} não foi encontrada.'
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
centro_treinamento = (await db_session.execute(
|
| 43 |
+
select(CentroTreinamentoModel).filter_by(nome=centro_treinamento_nome))
|
| 44 |
+
).scalars().first()
|
| 45 |
+
|
| 46 |
+
if not centro_treinamento:
|
| 47 |
+
raise HTTPException(
|
| 48 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 49 |
+
detail=f'O centro de treinamento {centro_treinamento_nome} não foi encontrado.'
|
| 50 |
+
)
|
| 51 |
+
try:
|
| 52 |
+
atleta_model = AtletaModel(
|
| 53 |
+
**atleta_in.model_dump(exclude={'categoria', 'centro_treinamento'}),
|
| 54 |
+
categoria_id=categoria.pk_id,
|
| 55 |
+
centro_treinamento_id=centro_treinamento.pk_id,
|
| 56 |
+
created_at=datetime.utcnow()
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
db_session.add(atleta_model)
|
| 60 |
+
await db_session.commit()
|
| 61 |
+
await db_session.refresh(atleta_model)
|
| 62 |
+
except IntegrityError:
|
| 63 |
+
await db_session.rollback()
|
| 64 |
+
raise HTTPException(
|
| 65 |
+
status_code=status.HTTP_303_SEE_OTHER,
|
| 66 |
+
detail=f"Já existe um atleta cadastrado com o cpf: {atleta_in.cpf}"
|
| 67 |
+
)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
await db_session.rollback()
|
| 70 |
+
raise HTTPException(
|
| 71 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 72 |
+
detail=f'Ocorreu um erro ao inserir os dados no banco: {e}'
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return AtletaOut.model_validate(atleta_model)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@router.get(
|
| 79 |
+
'/',
|
| 80 |
+
summary='Consultar Atletas com filtros e paginação',
|
| 81 |
+
status_code=status.HTTP_200_OK,
|
| 82 |
+
response_model=Page[AtletaCustomOut],
|
| 83 |
+
)
|
| 84 |
+
async def query(
|
| 85 |
+
db_session: DatabaseDependency,
|
| 86 |
+
nome: str | None = Query(None, description="Filtrar atleta por nome"),
|
| 87 |
+
cpf: str | None = Query(None, description="Filtrar atleta por CPF")
|
| 88 |
+
) -> Page[AtletaCustomOut]:
|
| 89 |
+
stmt = select(AtletaModel).options(
|
| 90 |
+
selectinload(AtletaModel.categoria),
|
| 91 |
+
selectinload(AtletaModel.centro_treinamento)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
if nome:
|
| 95 |
+
stmt = stmt.where(AtletaModel.nome.ilike(f"%{nome}%"))
|
| 96 |
+
|
| 97 |
+
if cpf:
|
| 98 |
+
stmt = stmt.where(AtletaModel.cpf == cpf)
|
| 99 |
+
|
| 100 |
+
atletas: list[AtletaModel] = (await db_session.execute(stmt)).scalars().all()
|
| 101 |
+
|
| 102 |
+
atletas_custom = [
|
| 103 |
+
AtletaCustomOut(
|
| 104 |
+
nome=atleta.nome,
|
| 105 |
+
categoria=atleta.categoria.nome,
|
| 106 |
+
centro_treinamento=atleta.centro_treinamento.nome
|
| 107 |
+
) for atleta in atletas
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
return paginate(atletas_custom)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@router.get(
|
| 114 |
+
'/{id}',
|
| 115 |
+
summary='Consulta um Atleta pelo id',
|
| 116 |
+
status_code=status.HTTP_200_OK,
|
| 117 |
+
response_model=AtletaOut,
|
| 118 |
+
)
|
| 119 |
+
async def get(id: UUID4, db_session: DatabaseDependency) -> AtletaOut:
|
| 120 |
+
atleta_model: AtletaModel | None = (
|
| 121 |
+
await db_session.execute(select(AtletaModel).filter_by(id=id))
|
| 122 |
+
).scalars().first()
|
| 123 |
+
|
| 124 |
+
if not atleta_model:
|
| 125 |
+
raise HTTPException(
|
| 126 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 127 |
+
detail=f'Atleta não encontrado no id: {id}'
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
return AtletaOut.model_validate(atleta_model)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@router.patch(
|
| 134 |
+
'/{id}',
|
| 135 |
+
summary='Editar um Atleta pelo id',
|
| 136 |
+
status_code=status.HTTP_200_OK,
|
| 137 |
+
response_model=AtletaOut,
|
| 138 |
+
)
|
| 139 |
+
async def patch(id: UUID4, db_session: DatabaseDependency, atleta_up: AtletaUpdate = Body(...)) -> AtletaOut:
|
| 140 |
+
atleta_model: AtletaModel | None = (
|
| 141 |
+
await db_session.execute(select(AtletaModel).filter_by(id=id))
|
| 142 |
+
).scalars().first()
|
| 143 |
+
|
| 144 |
+
if not atleta_model:
|
| 145 |
+
raise HTTPException(
|
| 146 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 147 |
+
detail=f'Atleta não encontrado no id: {id}'
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
atleta_update = atleta_up.model_dump(exclude_unset=True)
|
| 151 |
+
for key, value in atleta_update.items():
|
| 152 |
+
setattr(atleta_model, key, value)
|
| 153 |
+
|
| 154 |
+
await db_session.commit()
|
| 155 |
+
await db_session.refresh(atleta_model)
|
| 156 |
+
|
| 157 |
+
return AtletaOut.model_validate(atleta_model)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@router.delete(
|
| 161 |
+
'/{id}',
|
| 162 |
+
summary='Deletar um Atleta pelo id',
|
| 163 |
+
status_code=status.HTTP_204_NO_CONTENT
|
| 164 |
+
)
|
| 165 |
+
async def delete(id: UUID4, db_session: DatabaseDependency) -> None:
|
| 166 |
+
atleta_model: AtletaModel | None = (
|
| 167 |
+
await db_session.execute(select(AtletaModel).filter_by(id=id))
|
| 168 |
+
).scalars().first()
|
| 169 |
+
|
| 170 |
+
if not atleta_model:
|
| 171 |
+
raise HTTPException(
|
| 172 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 173 |
+
detail=f'Atleta não encontrado no id: {id}'
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
await db_session.delete(atleta_model)
|
| 177 |
+
await db_session.commit()
|
workout_api/atleta/models.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from sqlalchemy import DateTime, ForeignKey, Integer, String, Float
|
| 3 |
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
| 4 |
+
from workout_api.contrib.models import BaseModel
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class AtletaModel(BaseModel):
|
| 8 |
+
__tablename__ = 'atletas'
|
| 9 |
+
|
| 10 |
+
pk_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
| 11 |
+
nome: Mapped[str] = mapped_column(String(50), nullable=False)
|
| 12 |
+
cpf: Mapped[str] = mapped_column(String(11), unique=True, nullable=False)
|
| 13 |
+
idade: Mapped[int] = mapped_column(Integer, nullable=False)
|
| 14 |
+
peso: Mapped[float] = mapped_column(Float, nullable=False)
|
| 15 |
+
altura: Mapped[float] = mapped_column(Float, nullable=False)
|
| 16 |
+
sexo: Mapped[str] = mapped_column(String(1), nullable=False)
|
| 17 |
+
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
| 18 |
+
categoria: Mapped['CategoriaModel'] = relationship(back_populates="atletas", lazy='selectin')
|
| 19 |
+
categoria_id: Mapped[int] = mapped_column(ForeignKey("categorias.pk_id"))
|
| 20 |
+
centro_treinamento: Mapped['CentroTreinamentoModel'] = relationship(back_populates="atletas", lazy='selectin')
|
| 21 |
+
centro_treinamento_id: Mapped[int] = mapped_column(ForeignKey("centros_treinamento.pk_id"))
|
workout_api/atleta/schemas.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated, Optional
|
| 2 |
+
from pydantic import Field, PositiveFloat, BaseModel
|
| 3 |
+
from workout_api.categorias.schemas import CategoriaIn
|
| 4 |
+
from workout_api.centro_treinamento.schemas import CentroTreinamentoAtleta
|
| 5 |
+
|
| 6 |
+
from workout_api.contrib.schemas import BaseSchema, OutMixin
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Atleta(BaseSchema):
|
| 10 |
+
nome: Annotated[str, Field(description='Nome do atleta', example='Joao', max_length=50)]
|
| 11 |
+
cpf: Annotated[str, Field(description='CPF do atleta', example='12345678900', max_length=11)]
|
| 12 |
+
idade: Annotated[int, Field(description='Idade do atleta', example=25)]
|
| 13 |
+
peso: Annotated[PositiveFloat, Field(description='Peso do atleta', example=75.5)]
|
| 14 |
+
altura: Annotated[PositiveFloat, Field(description='Altura do atleta', example=1.70)]
|
| 15 |
+
sexo: Annotated[str, Field(description='Sexo do atleta', example='M', max_length=1)]
|
| 16 |
+
categoria: Annotated[CategoriaIn, Field(description='Categoria do atleta')]
|
| 17 |
+
centro_treinamento: Annotated[CentroTreinamentoAtleta, Field(description='Centro de treinamento do atleta')]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AtletaIn(Atleta):
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class AtletaOut(Atleta, OutMixin):
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
class AtletaUpdate(BaseSchema):
|
| 28 |
+
nome: Annotated[Optional[str], Field(None, description='Nome do atleta', example='Joao', max_length=50)]
|
| 29 |
+
idade: Annotated[Optional[int], Field(None, description='Idade do atleta', example=25)]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# NOVO: Response customizado para GET all paginado e filtros
|
| 33 |
+
class AtletaCustomOut(BaseModel):
|
| 34 |
+
nome: str
|
| 35 |
+
categoria: str
|
| 36 |
+
centro_treinamento: str
|
workout_api/categorias/__init__.py
ADDED
|
File without changes
|
workout_api/categorias/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (167 Bytes). View file
|
|
|
workout_api/categorias/__pycache__/controller.cpython-311.pyc
ADDED
|
Binary file (3.21 kB). View file
|
|
|
workout_api/categorias/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (1.17 kB). View file
|
|
|
workout_api/categorias/__pycache__/schemas.cpython-311.pyc
ADDED
|
Binary file (1.18 kB). View file
|
|
|
workout_api/categorias/controller.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Body, HTTPException, status
|
| 2 |
+
from pydantic import UUID4
|
| 3 |
+
from workout_api.categorias.schemas import CategoriaIn, CategoriaOut
|
| 4 |
+
from workout_api.categorias.models import CategoriaModel
|
| 5 |
+
|
| 6 |
+
from workout_api.contrib.dependencies import DatabaseDependency
|
| 7 |
+
from sqlalchemy.future import select
|
| 8 |
+
from sqlalchemy.exc import IntegrityError
|
| 9 |
+
|
| 10 |
+
router = APIRouter()
|
| 11 |
+
|
| 12 |
+
@router.post(
|
| 13 |
+
'/',
|
| 14 |
+
summary='Criar uma nova Categoria',
|
| 15 |
+
status_code=status.HTTP_201_CREATED,
|
| 16 |
+
response_model=CategoriaOut,
|
| 17 |
+
)
|
| 18 |
+
async def post(
|
| 19 |
+
db_session: DatabaseDependency,
|
| 20 |
+
categoria_in: CategoriaIn = Body(...)
|
| 21 |
+
) -> CategoriaOut:
|
| 22 |
+
categoria_model = CategoriaModel(**categoria_in.model_dump())
|
| 23 |
+
try:
|
| 24 |
+
db_session.add(categoria_model)
|
| 25 |
+
await db_session.commit()
|
| 26 |
+
await db_session.refresh(categoria_model)
|
| 27 |
+
except IntegrityError:
|
| 28 |
+
await db_session.rollback()
|
| 29 |
+
raise HTTPException(
|
| 30 |
+
status_code=status.HTTP_303_SEE_OTHER,
|
| 31 |
+
detail=f"Já existe uma categoria cadastrada com o nome: {categoria_in.nome}"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
return CategoriaOut.model_validate(categoria_model)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@router.get(
|
| 38 |
+
'/',
|
| 39 |
+
summary='Consultar todas as Categorias',
|
| 40 |
+
status_code=status.HTTP_200_OK,
|
| 41 |
+
response_model=list[CategoriaOut],
|
| 42 |
+
)
|
| 43 |
+
async def query(db_session: DatabaseDependency) -> list[CategoriaOut]:
|
| 44 |
+
categorias: list[CategoriaModel] = (await db_session.execute(select(CategoriaModel))).scalars().all()
|
| 45 |
+
|
| 46 |
+
return [CategoriaOut.model_validate(cat) for cat in categorias]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@router.get(
|
| 50 |
+
'/{id}',
|
| 51 |
+
summary='Consulta uma Categoria pelo id',
|
| 52 |
+
status_code=status.HTTP_200_OK,
|
| 53 |
+
response_model=CategoriaOut,
|
| 54 |
+
)
|
| 55 |
+
async def get(id: UUID4, db_session: DatabaseDependency) -> CategoriaOut:
|
| 56 |
+
categoria: CategoriaModel | None = (
|
| 57 |
+
await db_session.execute(select(CategoriaModel).filter_by(id=id))
|
| 58 |
+
).scalars().first()
|
| 59 |
+
|
| 60 |
+
if not categoria:
|
| 61 |
+
raise HTTPException(
|
| 62 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 63 |
+
detail=f'Categoria não encontrada no id: {id}'
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
return CategoriaOut.model_validate(categoria)
|
workout_api/categorias/models.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Integer, String
|
| 2 |
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
| 3 |
+
from workout_api.contrib.models import BaseModel
|
| 4 |
+
|
| 5 |
+
class CategoriaModel(BaseModel):
|
| 6 |
+
__tablename__ = 'categorias'
|
| 7 |
+
|
| 8 |
+
pk_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
| 9 |
+
nome: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
| 10 |
+
atletas: Mapped[list['AtletaModel']] = relationship(back_populates="categoria")
|
workout_api/categorias/schemas.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated
|
| 2 |
+
|
| 3 |
+
from pydantic import UUID4, Field
|
| 4 |
+
from workout_api.contrib.schemas import BaseSchema
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class CategoriaIn(BaseSchema):
|
| 8 |
+
nome: Annotated[str, Field(description='Nome da categoria', example='Scale', max_length=10)]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class CategoriaOut(CategoriaIn):
|
| 12 |
+
id: Annotated[UUID4, Field(description='Identificador da categoria')]
|
| 13 |
+
|
workout_api/centro_treinamento/__init__.py
ADDED
|
File without changes
|
workout_api/centro_treinamento/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (175 Bytes). View file
|
|
|
workout_api/centro_treinamento/__pycache__/controller.cpython-311.pyc
ADDED
|
Binary file (3.35 kB). View file
|
|
|
workout_api/centro_treinamento/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (1.55 kB). View file
|
|
|
workout_api/centro_treinamento/__pycache__/schemas.cpython-311.pyc
ADDED
|
Binary file (1.88 kB). View file
|
|
|
workout_api/centro_treinamento/controller.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Body, HTTPException, status
|
| 2 |
+
from pydantic import UUID4
|
| 3 |
+
from workout_api.centro_treinamento.schemas import CentroTreinamentoIn, CentroTreinamentoOut
|
| 4 |
+
from workout_api.centro_treinamento.models import CentroTreinamentoModel
|
| 5 |
+
|
| 6 |
+
from workout_api.contrib.dependencies import DatabaseDependency
|
| 7 |
+
from sqlalchemy.future import select
|
| 8 |
+
from sqlalchemy.exc import IntegrityError
|
| 9 |
+
|
| 10 |
+
router = APIRouter()
|
| 11 |
+
|
| 12 |
+
@router.post(
|
| 13 |
+
'/',
|
| 14 |
+
summary='Criar um novo Centro de treinamento',
|
| 15 |
+
status_code=status.HTTP_201_CREATED,
|
| 16 |
+
response_model=CentroTreinamentoOut,
|
| 17 |
+
)
|
| 18 |
+
async def post(
|
| 19 |
+
db_session: DatabaseDependency,
|
| 20 |
+
centro_treinamento_in: CentroTreinamentoIn = Body(...)
|
| 21 |
+
) -> CentroTreinamentoOut:
|
| 22 |
+
centro_treinamento_model = CentroTreinamentoModel(**centro_treinamento_in.model_dump())
|
| 23 |
+
try:
|
| 24 |
+
db_session.add(centro_treinamento_model)
|
| 25 |
+
await db_session.commit()
|
| 26 |
+
await db_session.refresh(centro_treinamento_model)
|
| 27 |
+
except IntegrityError:
|
| 28 |
+
await db_session.rollback()
|
| 29 |
+
raise HTTPException(
|
| 30 |
+
status_code=status.HTTP_303_SEE_OTHER,
|
| 31 |
+
detail=f"Já existe um centro de treinamento cadastrado com o nome: {centro_treinamento_in.nome}"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
return CentroTreinamentoOut.model_validate(centro_treinamento_model)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@router.get(
|
| 38 |
+
'/',
|
| 39 |
+
summary='Consultar todos os centros de treinamento',
|
| 40 |
+
status_code=status.HTTP_200_OK,
|
| 41 |
+
response_model=list[CentroTreinamentoOut],
|
| 42 |
+
)
|
| 43 |
+
async def query(db_session: DatabaseDependency) -> list[CentroTreinamentoOut]:
|
| 44 |
+
centros_treinamento: list[CentroTreinamentoModel] = (
|
| 45 |
+
await db_session.execute(select(CentroTreinamentoModel))
|
| 46 |
+
).scalars().all()
|
| 47 |
+
|
| 48 |
+
return [CentroTreinamentoOut.model_validate(ct) for ct in centros_treinamento]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@router.get(
|
| 52 |
+
'/{id}',
|
| 53 |
+
summary='Consulta um centro de treinamento pelo id',
|
| 54 |
+
status_code=status.HTTP_200_OK,
|
| 55 |
+
response_model=CentroTreinamentoOut,
|
| 56 |
+
)
|
| 57 |
+
async def get(id: UUID4, db_session: DatabaseDependency) -> CentroTreinamentoOut:
|
| 58 |
+
centro_treinamento: CentroTreinamentoModel | None = (
|
| 59 |
+
await db_session.execute(select(CentroTreinamentoModel).filter_by(id=id))
|
| 60 |
+
).scalars().first()
|
| 61 |
+
|
| 62 |
+
if not centro_treinamento:
|
| 63 |
+
raise HTTPException(
|
| 64 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 65 |
+
detail=f'Centro de treinamento não encontrado no id: {id}'
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
return CentroTreinamentoOut.model_validate(centro_treinamento)
|
workout_api/centro_treinamento/models.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Integer, String
|
| 2 |
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
| 3 |
+
from workout_api.contrib.models import BaseModel
|
| 4 |
+
|
| 5 |
+
class CentroTreinamentoModel(BaseModel):
|
| 6 |
+
__tablename__ = 'centros_treinamento'
|
| 7 |
+
|
| 8 |
+
pk_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
| 9 |
+
nome: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
| 10 |
+
endereco: Mapped[str] = mapped_column(String(60), nullable=False)
|
| 11 |
+
proprietario: Mapped[str] = mapped_column(String(30), nullable=False)
|
| 12 |
+
atletas: Mapped[list['AtletaModel']] = relationship(back_populates="centro_treinamento")
|
workout_api/centro_treinamento/schemas.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated
|
| 2 |
+
|
| 3 |
+
from pydantic import Field, UUID4
|
| 4 |
+
from workout_api.contrib.schemas import BaseSchema
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class CentroTreinamentoIn(BaseSchema):
|
| 8 |
+
nome: Annotated[str, Field(description='Nome do centro de treinamento', example='CT King', max_length=20)]
|
| 9 |
+
endereco: Annotated[str, Field(description='Endereço do centro de treinamento', example='Rua X, Q02', max_length=60)]
|
| 10 |
+
proprietario: Annotated[str, Field(description='Proprietario do centro de treinamento', example='Marcos', max_length=30)]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CentroTreinamentoAtleta(BaseSchema):
|
| 14 |
+
nome: Annotated[str, Field(description='Nome do centro de treinamento', example='CT King', max_length=20)]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class CentroTreinamentoOut(CentroTreinamentoIn):
|
| 18 |
+
id: Annotated[UUID4, Field(description='Identificador do centro de treinamento')]
|
workout_api/configs/__init__.py
ADDED
|
File without changes
|
workout_api/configs/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (164 Bytes). View file
|
|
|
workout_api/configs/__pycache__/database.cpython-311.pyc
ADDED
|
Binary file (1.17 kB). View file
|
|
|
workout_api/configs/__pycache__/settings.cpython-311.pyc
ADDED
|
Binary file (736 Bytes). View file
|
|
|
workout_api/configs/database.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import AsyncGenerator
|
| 2 |
+
|
| 3 |
+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
| 4 |
+
from sqlalchemy.orm import sessionmaker
|
| 5 |
+
from workout_api.configs.settings import settings
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
engine = create_async_engine(settings.DB_URL, echo=False)
|
| 9 |
+
async_session = sessionmaker(
|
| 10 |
+
engine, class_=AsyncSession, expire_on_commit=False
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
async def get_session() -> AsyncGenerator:
|
| 14 |
+
async with async_session() as session:
|
| 15 |
+
yield session
|
workout_api/configs/settings.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import Field
|
| 2 |
+
from pydantic_settings import BaseSettings
|
| 3 |
+
|
| 4 |
+
class Settings(BaseSettings):
|
| 5 |
+
DB_URL: str = Field(default='postgresql+asyncpg://workout:workout@localhost/workout')
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
settings = Settings()
|
workout_api/contrib/__init__.py
ADDED
|
File without changes
|
workout_api/contrib/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (164 Bytes). View file
|
|
|