diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..973466c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +venv +__pycache__ \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..3f53646 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) diff --git a/.gitignore b/.gitignore index 90f8fbb..0f92aee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,176 +1,176 @@ -# Created by https://www.toptal.com/developers/gitignore/api/python -# Edit at https://www.toptal.com/developers/gitignore?templates=python - -### Python ### -# 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/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# 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/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + # End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..974c3aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13.4-alpine3.22 +WORKDIR /app + +# Instala compiladores e dependências do sistema +RUN apk add --no-cache build-base + +# Copia apenas o requirements.txt para aproveitar o cache de dependências +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +# Copia todo o código da aplicação +COPY . . + +# Expõe a porta padrão do FastAPI/Uvicorn +EXPOSE 8000 + +# Comando padrão para rodar a aplicação em modo desenvolvimento +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file diff --git a/app.py b/app.py index 8f868ac..a14a664 100644 --- a/app.py +++ b/app.py @@ -1,22 +1,22 @@ -from fastapi import FastAPI -from database import engine, Base -from routers.alunos import alunos_router -from routers.cursos import cursos_router -from routers.matriculas import matriculas_router - - -Base.metadata.create_all(bind=engine) - -app = FastAPI( - title="API de Gestão Escolar", - description=""" - Esta API fornece endpoints para gerenciar alunos, cursos e turmas, em uma instituição de ensino. - - Permite realizar diferentes operações em cada uma dessas entidades. - """, - version="1.0.0", -) - -app.include_router(alunos_router, tags=["alunos"]) -app.include_router(cursos_router, tags=["cursos"]) +from fastapi import FastAPI +from database import engine, Base +from routers.alunos import alunos_router +from routers.cursos import cursos_router +from routers.matriculas import matriculas_router + + +Base.metadata.create_all(bind=engine) + +app = FastAPI( + title="API de Gestão Escolar", + description=""" + Esta API fornece endpoints para gerenciar alunos, cursos e turmas, em uma instituição de ensino. + + Permite realizar diferentes operações em cada uma dessas entidades. + """, + version="1.0.0", +) + +app.include_router(alunos_router, tags=["alunos"]) +app.include_router(cursos_router, tags=["cursos"]) app.include_router(matriculas_router, tags=["matriculas"]) \ No newline at end of file diff --git a/database.py b/database.py index b851121..313e816 100644 --- a/database.py +++ b/database.py @@ -1,24 +1,24 @@ -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker -import os - -# Caminho para o arquivo SQLite (pode ser ajustado conforme necessário) -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -DATABASE_URL = f"sqlite:///{os.path.join(BASE_DIR, 'escola.db')}" - -engine = create_engine( - DATABASE_URL, connect_args={"check_same_thread": False} -) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -Base = declarative_base() - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - - +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os + +# Caminho para o arquivo SQLite (pode ser ajustado conforme necessário) +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DATABASE_URL = f"sqlite:///{os.path.join(BASE_DIR, 'escola.db')}" + +engine = create_engine( + DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9417e16 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: # Define os serviços que serão executados pelo Docker Compose + app: # Nome do serviço (pode ser usado para referenciar em outros serviços) + build: . # Constrói a imagem Docker a partir do Dockerfile localizado no diretório atual + container_name: imersaodevops-app # Define o nome do container gerado + ports: + - "8000:8000" # Mapeia a porta 8000 do host para a porta 8000 do container + volumes: + - .:/app # Sincroniza o diretório atual do host com o diretório /app do container (útil para desenvolvimento) + ##command: ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000","--reload"] # Comando para iniciar o servidor FastAPI \ No newline at end of file diff --git a/models.py b/models.py index e0e69d6..243e8be 100644 --- a/models.py +++ b/models.py @@ -1,33 +1,33 @@ -from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table -from sqlalchemy.orm import relationship -from database import Base - -class Matricula(Base): - __tablename__ = "matriculas" - - id = Column(Integer, primary_key=True, index=True) - aluno_id = Column(Integer, ForeignKey("alunos.id")) - curso_id = Column(Integer, ForeignKey("cursos.id")) - - aluno = relationship("Aluno", back_populates="matriculas") - curso = relationship("Curso", back_populates="matriculas") - -class Aluno(Base): - __tablename__ = "alunos" - - id = Column(Integer, primary_key=True, index=True) - nome = Column(String, nullable=False) - email = Column(String, nullable=False) - telefone = Column(String, nullable=False) - - matriculas = relationship("Matricula", back_populates="aluno") - -class Curso(Base): - __tablename__ = "cursos" - - id = Column(Integer, primary_key=True, index=True) - nome = Column(String, nullable=False) - codigo = Column(String, nullable=False, unique=True) - descricao = Column(Text) - +from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table +from sqlalchemy.orm import relationship +from database import Base + +class Matricula(Base): + __tablename__ = "matriculas" + + id = Column(Integer, primary_key=True, index=True) + aluno_id = Column(Integer, ForeignKey("alunos.id")) + curso_id = Column(Integer, ForeignKey("cursos.id")) + + aluno = relationship("Aluno", back_populates="matriculas") + curso = relationship("Curso", back_populates="matriculas") + +class Aluno(Base): + __tablename__ = "alunos" + + id = Column(Integer, primary_key=True, index=True) + nome = Column(String, nullable=False) + email = Column(String, nullable=False) + telefone = Column(String, nullable=False) + + matriculas = relationship("Matricula", back_populates="aluno") + +class Curso(Base): + __tablename__ = "cursos" + + id = Column(Integer, primary_key=True, index=True) + nome = Column(String, nullable=False) + codigo = Column(String, nullable=False, unique=True) + descricao = Column(Text) + matriculas = relationship("Matricula", back_populates="curso") \ No newline at end of file diff --git a/readme.md b/readme.md index 85f1813..5eed6d5 100644 --- a/readme.md +++ b/readme.md @@ -1,68 +1,68 @@ -# Imersão DevOps - Alura Google Cloud - -Este projeto é uma API desenvolvida com FastAPI para gerenciar alunos, cursos e matrículas em uma instituição de ensino. - -## Pré-requisitos - -- [Python 3.10 ou superior instalado](https://www.python.org/downloads/) -- [Git](https://git-scm.com/downloads) -- [Docker](https://www.docker.com/get-started/) - -## Passos para subir o projeto - -1. **Faça o download do repositório:** - [Clique aqui para realizar o download](https://github.com/guilhermeonrails/imersao-devops/archive/refs/heads/main.zip) - -2. **Crie um ambiente virtual:** - ```sh - python3 -m venv ./venv - ``` - -3. **Ative o ambiente virtual:** - - No Linux/Mac: - ```sh - source venv/bin/activate - ``` - - No Windows, abra um terminal no modo administrador e execute o comando: - ```sh - Set-ExecutionPolicy RemoteSigned - ``` - - ```sh - venv\Scripts\activate - ``` - -4. **Instale as dependências:** - ```sh - pip install -r requirements.txt - ``` - -5. **Execute a aplicação:** - ```sh - uvicorn app:app --reload - ``` - -6. **Acesse a documentação interativa:** - - Abra o navegador e acesse: - [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) - - Aqui você pode testar todos os endpoints da API de forma interativa. - ---- - -## Estrutura do Projeto - -- `app.py`: Arquivo principal da aplicação FastAPI. -- `models.py`: Modelos do banco de dados (SQLAlchemy). -- `schemas.py`: Schemas de validação (Pydantic). -- `database.py`: Configuração do banco de dados SQLite. -- `routers/`: Diretório com os arquivos de rotas (alunos, cursos, matrículas). -- `requirements.txt`: Lista de dependências do projeto. - ---- - -- O banco de dados SQLite será criado automaticamente como `escola.db` na primeira execução. -- Para reiniciar o banco, basta apagar o arquivo `escola.db` (isso apagará todos os dados). - ---- +# Imersão DevOps - Alura Google Cloud + +Este projeto é uma API desenvolvida com FastAPI para gerenciar alunos, cursos e matrículas em uma instituição de ensino. + +## Pré-requisitos + +- [Python 3.10 ou superior instalado](https://www.python.org/downloads/) +- [Git](https://git-scm.com/downloads) +- [Docker](https://www.docker.com/get-started/) + +## Passos para subir o projeto + +1. **Faça o download do repositório:** + [Clique aqui para realizar o download](https://github.com/guilhermeonrails/imersao-devops/archive/refs/heads/main.zip) + +2. **Crie um ambiente virtual:** + ```sh + python3 -m venv ./venv + ``` + +3. **Ative o ambiente virtual:** + - No Linux/Mac: + ```sh + source venv/bin/activate + ``` + - No Windows, abra um terminal no modo administrador e execute o comando: + ```sh + Set-ExecutionPolicy RemoteSigned + ``` + + ```sh + venv\Scripts\activate + ``` + +4. **Instale as dependências:** + ```sh + pip install -r requirements.txt + ``` + +5. **Execute a aplicação:** + ```sh + uvicorn app:app --reload + ``` + +6. **Acesse a documentação interativa:** + + Abra o navegador e acesse: + [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) + + Aqui você pode testar todos os endpoints da API de forma interativa. + +--- + +## Estrutura do Projeto + +- `app.py`: Arquivo principal da aplicação FastAPI. +- `models.py`: Modelos do banco de dados (SQLAlchemy). +- `schemas.py`: Schemas de validação (Pydantic). +- `database.py`: Configuração do banco de dados SQLite. +- `routers/`: Diretório com os arquivos de rotas (alunos, cursos, matrículas). +- `requirements.txt`: Lista de dependências do projeto. + +--- + +- O banco de dados SQLite será criado automaticamente como `escola.db` na primeira execução. +- Para reiniciar o banco, basta apagar o arquivo `escola.db` (isso apagará todos os dados). + +--- diff --git a/requirements.txt b/requirements.txt index 6846b24..c6417eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ -annotated-types==0.7.0 -anyio==4.9.0 -click==8.2.1 -dnspython==2.7.0 -email_validator==2.2.0 -fastapi==0.115.12 -h11==0.16.0 -idna==3.10 -pydantic==2.11.5 -pydantic_core==2.33.2 -sniffio==1.3.1 -SQLAlchemy==2.0.41 -starlette==0.46.2 -typing-inspection==0.4.1 -typing_extensions==4.13.2 -uvicorn==0.34.2 +annotated-types==0.7.0 +anyio==4.9.0 +click==8.2.1 +dnspython==2.7.0 +email_validator==2.2.0 +fastapi==0.115.12 +h11==0.16.0 +idna==3.10 +pydantic==2.11.5 +pydantic_core==2.33.2 +sniffio==1.3.1 +SQLAlchemy==2.0.41 +starlette==0.46.2 +typing-inspection==0.4.1 +typing_extensions==4.13.2 +uvicorn==0.34.2 \ No newline at end of file diff --git a/routers/alunos.py b/routers/alunos.py index 28ce764..7cbf536 100644 --- a/routers/alunos.py +++ b/routers/alunos.py @@ -1,146 +1,146 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from typing import List, Union -from schemas import Aluno -from models import Aluno as ModelAluno -from database import get_db - -alunos_router = APIRouter() - -@alunos_router.get("/alunos", response_model=List[Aluno]) -def read_alunos(db: Session = Depends(get_db)): - """ - Retorna uma lista de todos os alunos cadastrados. - - """ - alunos = db.query(ModelAluno).all() - return [Aluno.from_orm(aluno) for aluno in alunos] - -@alunos_router.get("/alunos/{aluno_id}", response_model=Aluno) -def read_aluno(aluno_id: int, db: Session = Depends(get_db)): - """ - Retorna os detalhes de um aluno específico com base no ID fornecido. - - Args: - aluno_id: O ID do aluno. - - Raises: - HTTPException: Se o aluno não for encontrado. - """ - db_aluno = db.query(ModelAluno).filter(ModelAluno.id == aluno_id).first() - if db_aluno is None: - raise HTTPException(status_code=404, detail="Aluno não encontrado") - return Aluno.from_orm(db_aluno) - -@alunos_router.post("/alunos", response_model=Aluno) -def create_aluno(aluno: Aluno, db: Session = Depends(get_db)): - """ - Cria um novo aluno com os dados fornecidos. - - Args: - aluno: Dados do aluno a ser criado. - - Returns: - Aluno: aluno criado. - """ - db_aluno = ModelAluno(**aluno.dict(exclude={"id"})) - db.add(db_aluno) - db.commit() - db.refresh(db_aluno) - return Aluno.from_orm(db_aluno) - -@alunos_router.put("/alunos/{aluno_id}", response_model=Aluno) -def update_aluno(aluno_id: int, aluno: Aluno, db: Session = Depends(get_db)): - """ - Atualiza os dados de um aluno existente. - - Args: - aluno_id: O ID do aluno a ser atualizado. - aluno: Os novos dados do aluno. - - Raises: - HTTPException: 404 - Aluno não encontrado. - - Returns: - Aluno: O aluno atualizado. - """ - db_aluno = db.query(ModelAluno).filter(ModelAluno.id == aluno_id).first() - if db_aluno is None: - raise HTTPException(status_code=404, detail="Aluno não encontrado") - - for key, value in aluno.dict(exclude_unset=True).items(): - setattr(db_aluno, key, value) - - db.commit() - db.refresh(db_aluno) - return Aluno.from_orm(db_aluno) - -@alunos_router.delete("/alunos/{aluno_id}", response_model=Aluno) -def delete_aluno(aluno_id: int, db: Session = Depends(get_db)): - """ - Exclui um aluno. - - Args: - aluno_id: O ID do aluno a ser excluído. - - Raises: - HTTPException: 404 - Aluno não encontrado. - - Returns: - Aluno: O aluno excluído. - """ - db_aluno = db.query(ModelAluno).filter(ModelAluno.id == aluno_id).first() - if db_aluno is None: - raise HTTPException(status_code=404, detail="Aluno não encontrado") - - aluno_deletado = Aluno.from_orm(db_aluno) - - db.delete(db_aluno) - db.commit() - return aluno_deletado - -@alunos_router.get("/alunos/nome/{nome_aluno}", response_model=Union[Aluno, List[Aluno]]) -def read_aluno_por_nome(nome_aluno: str, db: Session = Depends(get_db)): - """ - Busca alunos pelo nome (parcial ou completo). - - Args: - nome_aluno: O nome (ou parte do nome) do aluno a ser buscado. - - Raises: - HTTPException: 404 - Nenhum aluno encontrado com esse nome. - - Returns: - Union[Aluno, List[Aluno]]: Um único objeto `Aluno` se houver apenas uma correspondência, - ou uma lista de `Aluno` se houver várias correspondências. - """ - db_alunos = db.query(ModelAluno).filter(ModelAluno.nome.ilike(f"%{nome_aluno}%")).all() # ilike para case-insensitive - - if not db_alunos: - raise HTTPException(status_code=404, detail="Nenhum aluno encontrado com esse nome") - - if len(db_alunos) == 1: # Retorna um único Aluno se houver apenas uma correspondência - return Aluno.from_orm(db_alunos[0]) - - return [Aluno.from_orm(aluno) for aluno in db_alunos] - -@alunos_router.get("/alunos/email/{email_aluno}", response_model=Aluno) -def read_aluno_por_email(email_aluno: str, db: Session = Depends(get_db)): - """ - Busca um aluno pelo email. - - Args: - email_aluno: O email do aluno a ser buscado. - - Raises: - HTTPException: 404 - Nenhum aluno encontrado com esse email. - - Returns: - Aluno: O aluno encontrado. - """ - db_aluno = db.query(ModelAluno).filter(ModelAluno.email == email_aluno).first() - - if db_aluno is None: - raise HTTPException(status_code=404, detail="Nenhum aluno encontrado com esse email") - +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List, Union +from schemas import Aluno +from models import Aluno as ModelAluno +from database import get_db + +alunos_router = APIRouter() + +@alunos_router.get("/alunos", response_model=List[Aluno]) +def read_alunos(db: Session = Depends(get_db)): + """ + Retorna uma lista de todos os alunos cadastrados. + + """ + alunos = db.query(ModelAluno).all() + return [Aluno.from_orm(aluno) for aluno in alunos] + +@alunos_router.get("/alunos/{aluno_id}", response_model=Aluno) +def read_aluno(aluno_id: int, db: Session = Depends(get_db)): + """ + Retorna os detalhes de um aluno específico com base no ID fornecido. + + Args: + aluno_id: O ID do aluno. + + Raises: + HTTPException: Se o aluno não for encontrado. + """ + db_aluno = db.query(ModelAluno).filter(ModelAluno.id == aluno_id).first() + if db_aluno is None: + raise HTTPException(status_code=404, detail="Aluno não encontrado") + return Aluno.from_orm(db_aluno) + +@alunos_router.post("/alunos", response_model=Aluno) +def create_aluno(aluno: Aluno, db: Session = Depends(get_db)): + """ + Cria um novo aluno com os dados fornecidos. + + Args: + aluno: Dados do aluno a ser criado. + + Returns: + Aluno: aluno criado. + """ + db_aluno = ModelAluno(**aluno.dict(exclude={"id"})) + db.add(db_aluno) + db.commit() + db.refresh(db_aluno) + return Aluno.from_orm(db_aluno) + +@alunos_router.put("/alunos/{aluno_id}", response_model=Aluno) +def update_aluno(aluno_id: int, aluno: Aluno, db: Session = Depends(get_db)): + """ + Atualiza os dados de um aluno existente. + + Args: + aluno_id: O ID do aluno a ser atualizado. + aluno: Os novos dados do aluno. + + Raises: + HTTPException: 404 - Aluno não encontrado. + + Returns: + Aluno: O aluno atualizado. + """ + db_aluno = db.query(ModelAluno).filter(ModelAluno.id == aluno_id).first() + if db_aluno is None: + raise HTTPException(status_code=404, detail="Aluno não encontrado") + + for key, value in aluno.dict(exclude_unset=True).items(): + setattr(db_aluno, key, value) + + db.commit() + db.refresh(db_aluno) + return Aluno.from_orm(db_aluno) + +@alunos_router.delete("/alunos/{aluno_id}", response_model=Aluno) +def delete_aluno(aluno_id: int, db: Session = Depends(get_db)): + """ + Exclui um aluno. + + Args: + aluno_id: O ID do aluno a ser excluído. + + Raises: + HTTPException: 404 - Aluno não encontrado. + + Returns: + Aluno: O aluno excluído. + """ + db_aluno = db.query(ModelAluno).filter(ModelAluno.id == aluno_id).first() + if db_aluno is None: + raise HTTPException(status_code=404, detail="Aluno não encontrado") + + aluno_deletado = Aluno.from_orm(db_aluno) + + db.delete(db_aluno) + db.commit() + return aluno_deletado + +@alunos_router.get("/alunos/nome/{nome_aluno}", response_model=Union[Aluno, List[Aluno]]) +def read_aluno_por_nome(nome_aluno: str, db: Session = Depends(get_db)): + """ + Busca alunos pelo nome (parcial ou completo). + + Args: + nome_aluno: O nome (ou parte do nome) do aluno a ser buscado. + + Raises: + HTTPException: 404 - Nenhum aluno encontrado com esse nome. + + Returns: + Union[Aluno, List[Aluno]]: Um único objeto `Aluno` se houver apenas uma correspondência, + ou uma lista de `Aluno` se houver várias correspondências. + """ + db_alunos = db.query(ModelAluno).filter(ModelAluno.nome.ilike(f"%{nome_aluno}%")).all() # ilike para case-insensitive + + if not db_alunos: + raise HTTPException(status_code=404, detail="Nenhum aluno encontrado com esse nome") + + if len(db_alunos) == 1: # Retorna um único Aluno se houver apenas uma correspondência + return Aluno.from_orm(db_alunos[0]) + + return [Aluno.from_orm(aluno) for aluno in db_alunos] + +@alunos_router.get("/alunos/email/{email_aluno}", response_model=Aluno) +def read_aluno_por_email(email_aluno: str, db: Session = Depends(get_db)): + """ + Busca um aluno pelo email. + + Args: + email_aluno: O email do aluno a ser buscado. + + Raises: + HTTPException: 404 - Nenhum aluno encontrado com esse email. + + Returns: + Aluno: O aluno encontrado. + """ + db_aluno = db.query(ModelAluno).filter(ModelAluno.email == email_aluno).first() + + if db_aluno is None: + raise HTTPException(status_code=404, detail="Nenhum aluno encontrado com esse email") + return Aluno.from_orm(db_aluno) \ No newline at end of file diff --git a/routers/cursos.py b/routers/cursos.py index 1534415..237b943 100644 --- a/routers/cursos.py +++ b/routers/cursos.py @@ -1,64 +1,64 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from typing import List -from schemas import Curso -from models import Curso as ModelCurso -from database import get_db - -cursos_router = APIRouter() - -@cursos_router.get("/cursos", response_model=List[Curso]) -def read_cursos(db: Session = Depends(get_db)): - cursos = db.query(ModelCurso).all() - return [Curso.from_orm(curso) for curso in cursos] - -@cursos_router.post("/cursos", response_model=Curso) -def create_curso(curso: Curso, db: Session = Depends(get_db)): - db_curso = ModelCurso(**curso.dict(exclude={"id"})) - db.add(db_curso) - db.commit() - db.refresh(db_curso) - return Curso.from_orm(db_curso) - -@cursos_router.put("/cursos/{codigo_curso}", response_model=Curso) -def update_curso(codigo_curso: str, curso: Curso, db: Session = Depends(get_db)): - db_curso = db.query(ModelCurso).filter(ModelCurso.codigo == codigo_curso).first() - if db_curso is None: - raise HTTPException(status_code=404, detail="Curso não encontrado") - - for key, value in curso.dict(exclude_unset=True, exclude={"id"}).items(): - setattr(db_curso, key, value) - - db.commit() - db.refresh(db_curso) - return Curso.from_orm(db_curso) - -@cursos_router.get("/cursos/{codigo_curso}", response_model=Curso) -def read_curso_por_codigo(codigo_curso: str, db: Session = Depends(get_db)): - db_curso = db.query(ModelCurso).filter(ModelCurso.codigo == codigo_curso).first() - if db_curso is None: - raise HTTPException(status_code=404, detail="Nenhum curso encontrado com esse código") - return Curso.from_orm(db_curso) - - -# Não buscar um curso pelo ID nem deletar em nenhuma hipótese - -# @cursos_router.get("/cursos/{curso_id}", response_model=Curso) -# def read_curso(curso_id: int, db: Session = Depends(get_db)): -# db_curso = db.query(ModelCurso).filter(ModelCurso.id == curso_id).first() -# if db_curso is None: -# raise HTTPException(status_code=404, detail="Curso não encontrado") -# return Curso.from_orm(db_curso) - - -# @cursos_router.delete("/cursos/{curso_id}", response_model=Curso) -# def delete_curso(curso_id: int, db: Session = Depends(get_db)): -# db_curso = db.query(ModelCurso).filter(ModelCurso.id == curso_id).first() -# if db_curso is None: -# raise HTTPException(status_code=404, detail="Curso não encontrado") - -# curso_deletado = Curso.from_orm(db_curso) - -# db.delete(db_curso) -# db.commit() +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from schemas import Curso +from models import Curso as ModelCurso +from database import get_db + +cursos_router = APIRouter() + +@cursos_router.get("/cursos", response_model=List[Curso]) +def read_cursos(db: Session = Depends(get_db)): + cursos = db.query(ModelCurso).all() + return [Curso.from_orm(curso) for curso in cursos] + +@cursos_router.post("/cursos", response_model=Curso) +def create_curso(curso: Curso, db: Session = Depends(get_db)): + db_curso = ModelCurso(**curso.dict(exclude={"id"})) + db.add(db_curso) + db.commit() + db.refresh(db_curso) + return Curso.from_orm(db_curso) + +@cursos_router.put("/cursos/{codigo_curso}", response_model=Curso) +def update_curso(codigo_curso: str, curso: Curso, db: Session = Depends(get_db)): + db_curso = db.query(ModelCurso).filter(ModelCurso.codigo == codigo_curso).first() + if db_curso is None: + raise HTTPException(status_code=404, detail="Curso não encontrado") + + for key, value in curso.dict(exclude_unset=True, exclude={"id"}).items(): + setattr(db_curso, key, value) + + db.commit() + db.refresh(db_curso) + return Curso.from_orm(db_curso) + +@cursos_router.get("/cursos/{codigo_curso}", response_model=Curso) +def read_curso_por_codigo(codigo_curso: str, db: Session = Depends(get_db)): + db_curso = db.query(ModelCurso).filter(ModelCurso.codigo == codigo_curso).first() + if db_curso is None: + raise HTTPException(status_code=404, detail="Nenhum curso encontrado com esse código") + return Curso.from_orm(db_curso) + + +# Não buscar um curso pelo ID nem deletar em nenhuma hipótese + +# @cursos_router.get("/cursos/{curso_id}", response_model=Curso) +# def read_curso(curso_id: int, db: Session = Depends(get_db)): +# db_curso = db.query(ModelCurso).filter(ModelCurso.id == curso_id).first() +# if db_curso is None: +# raise HTTPException(status_code=404, detail="Curso não encontrado") +# return Curso.from_orm(db_curso) + + +# @cursos_router.delete("/cursos/{curso_id}", response_model=Curso) +# def delete_curso(curso_id: int, db: Session = Depends(get_db)): +# db_curso = db.query(ModelCurso).filter(ModelCurso.id == curso_id).first() +# if db_curso is None: +# raise HTTPException(status_code=404, detail="Curso não encontrado") + +# curso_deletado = Curso.from_orm(db_curso) + +# db.delete(db_curso) +# db.commit() # return curso_deletado \ No newline at end of file diff --git a/routers/matriculas.py b/routers/matriculas.py index 68bdb47..6de5e87 100644 --- a/routers/matriculas.py +++ b/routers/matriculas.py @@ -1,62 +1,62 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -from typing import List, Dict, Union -from schemas import Matricula -from models import Matricula as ModelMatricula, Aluno as ModelAluno, Curso as ModelCurso # Importe os modelos -from database import get_db - -matriculas_router = APIRouter() - -@matriculas_router.post("/matriculas", response_model=Matricula, status_code=status.HTTP_201_CREATED) -def create_matricula(matricula: Matricula, db: Session = Depends(get_db)): - - db_aluno = db.query(ModelAluno).filter(ModelAluno.id == matricula.aluno_id).first() - db_curso = db.query(ModelCurso).filter(ModelCurso.id == matricula.curso_id).first() - - if db_aluno is None or db_curso is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Aluno ou Curso não encontrado") - - db_matricula = ModelMatricula(**matricula.dict()) - db.add(db_matricula) - db.commit() - db.refresh(db_matricula) - return Matricula.from_orm(db_matricula) - - - -@matriculas_router.get("/matriculas/aluno/{nome_aluno}", response_model=Dict[str, Union[str, List[str]]]) -def read_matriculas_por_nome_aluno(nome_aluno: str, db: Session = Depends(get_db)): - db_aluno = db.query(ModelAluno).filter(ModelAluno.nome.ilike(f"%{nome_aluno}%")).first() - - if not db_aluno: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Aluno não encontrado") - - cursos_matriculados = [] - for matricula in db_aluno.matriculas: - curso = matricula.curso - if curso: - cursos_matriculados.append(curso.nome) - - if not cursos_matriculados: - raise HTTPException(status_code=404, detail=f"O aluno '{nome_aluno}' não possui matrículas cadastradas.") - - return {"aluno": db_aluno.nome, "cursos": cursos_matriculados} - -@matriculas_router.get("/matriculas/curso/{codigo_curso}", response_model=Dict[str, Union[str, List[str]]]) -def read_alunos_matriculados_por_codigo_curso(codigo_curso: str, db: Session = Depends(get_db)): - """Retorna o nome do curso e uma lista com os nomes dos alunos matriculados.""" - db_curso = db.query(ModelCurso).filter(ModelCurso.codigo == codigo_curso).first() - - if not db_curso: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Curso não encontrado") - - alunos_matriculados = [] - for matricula in db_curso.matriculas: # Itera pelas matrículas do curso - aluno = matricula.aluno # Acessa o aluno diretamente pelo relacionamento - if aluno: # Verifica se o aluno existe (pode ter sido excluído) - alunos_matriculados.append(aluno.nome) - - if not alunos_matriculados: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Nenhum aluno matriculado no curso '{db_curso.nome}'.") - +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Dict, Union +from schemas import Matricula +from models import Matricula as ModelMatricula, Aluno as ModelAluno, Curso as ModelCurso # Importe os modelos +from database import get_db + +matriculas_router = APIRouter() + +@matriculas_router.post("/matriculas", response_model=Matricula, status_code=status.HTTP_201_CREATED) +def create_matricula(matricula: Matricula, db: Session = Depends(get_db)): + + db_aluno = db.query(ModelAluno).filter(ModelAluno.id == matricula.aluno_id).first() + db_curso = db.query(ModelCurso).filter(ModelCurso.id == matricula.curso_id).first() + + if db_aluno is None or db_curso is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Aluno ou Curso não encontrado") + + db_matricula = ModelMatricula(**matricula.dict()) + db.add(db_matricula) + db.commit() + db.refresh(db_matricula) + return Matricula.from_orm(db_matricula) + + + +@matriculas_router.get("/matriculas/aluno/{nome_aluno}", response_model=Dict[str, Union[str, List[str]]]) +def read_matriculas_por_nome_aluno(nome_aluno: str, db: Session = Depends(get_db)): + db_aluno = db.query(ModelAluno).filter(ModelAluno.nome.ilike(f"%{nome_aluno}%")).first() + + if not db_aluno: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Aluno não encontrado") + + cursos_matriculados = [] + for matricula in db_aluno.matriculas: + curso = matricula.curso + if curso: + cursos_matriculados.append(curso.nome) + + if not cursos_matriculados: + raise HTTPException(status_code=404, detail=f"O aluno '{nome_aluno}' não possui matrículas cadastradas.") + + return {"aluno": db_aluno.nome, "cursos": cursos_matriculados} + +@matriculas_router.get("/matriculas/curso/{codigo_curso}", response_model=Dict[str, Union[str, List[str]]]) +def read_alunos_matriculados_por_codigo_curso(codigo_curso: str, db: Session = Depends(get_db)): + """Retorna o nome do curso e uma lista com os nomes dos alunos matriculados.""" + db_curso = db.query(ModelCurso).filter(ModelCurso.codigo == codigo_curso).first() + + if not db_curso: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Curso não encontrado") + + alunos_matriculados = [] + for matricula in db_curso.matriculas: # Itera pelas matrículas do curso + aluno = matricula.aluno # Acessa o aluno diretamente pelo relacionamento + if aluno: # Verifica se o aluno existe (pode ter sido excluído) + alunos_matriculados.append(aluno.nome) + + if not alunos_matriculados: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Nenhum aluno matriculado no curso '{db_curso.nome}'.") + return {"curso": db_curso.nome, "alunos": alunos_matriculados} \ No newline at end of file diff --git a/schemas.py b/schemas.py index dbd6700..8ccbe8e 100644 --- a/schemas.py +++ b/schemas.py @@ -1,32 +1,32 @@ -from pydantic import BaseModel, EmailStr -from typing import List - -class Matricula(BaseModel): - aluno_id: int - curso_id: int - - class Config: - from_attributes = True - from_attributes = True - -Matriculas = List[Matricula] - -class Aluno(BaseModel): - nome: str - email: EmailStr - telefone: str - - class Config: - from_attributes = True - -Alunos = List[Aluno] - -class Curso(BaseModel): - nome: str - codigo: str - descricao: str - - class Config: - from_attributes = True - +from pydantic import BaseModel, EmailStr +from typing import List + +class Matricula(BaseModel): + aluno_id: int + curso_id: int + + class Config: + from_attributes = True + from_attributes = True + +Matriculas = List[Matricula] + +class Aluno(BaseModel): + nome: str + email: EmailStr + telefone: str + + class Config: + from_attributes = True + +Alunos = List[Aluno] + +class Curso(BaseModel): + nome: str + codigo: str + descricao: str + + class Config: + from_attributes = True + Cursos = List[Curso] \ No newline at end of file