Commit Inicial

This commit is contained in:
RicardoJDaleprane 2025-09-18 15:52:28 -03:00
commit 91a4568db1
91 changed files with 11707 additions and 0 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
iniciar.txt
iniciar_multi_tenant.txt
docker.txt
__pycache__/
*.py[cod]
*.pyo
*.pyd
.env
.git

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Poetry (AdminUuidPostgreSql)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="GOOGLE" />
<option name="myDocStringFormat" value="Google" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>

View File

@ -0,0 +1,11 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>expiracao</w>
<w>maximos</w>
<w>minimos</w>
<w>registrados</w>
<w>validacoes</w>
</words>
</dictionary>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Poetry (AdminUuidPostgreSql)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (AdminUuidPostgreSql)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/AdminUuidPostgreSql.iml" filepath="$PROJECT_DIR$/.idea/AdminUuidPostgreSql.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

26
Dockerfile Normal file
View File

@ -0,0 +1,26 @@
FROM python:3.12-slim
ENV POETRY_VIRTUALENVS_CREATE=false
WORKDIR /code
# Copie os arquivos do Poetry para o contêiner
COPY pyproject.toml poetry.lock ./
RUN pip install poetry
RUN poetry config installer.max-workers 10
RUN poetry install --no-interaction --no-ansi --no-root
COPY ./app /code/app
COPY ./alembic /code/alembic
COPY iniciar_permissoes_e_papeis.py /code/
COPY alembic.ini /code/
COPY check_db.py /code/
COPY start.sh /code/
# Dar permissão de execução aos scripts
RUN chmod +x /code/start.sh
#CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"]
CMD ["./start.sh"]

118
alembic.ini Normal file
View File

@ -0,0 +1,118 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
version_path_separator = newline
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic1[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic1/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic1/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic1.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
#version_path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
;level = DEBUG
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
;level = INFO
level = DEBUG
handlers = console
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

1
alembic/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration with an async dbapi.

300
alembic/env.py Normal file
View File

@ -0,0 +1,300 @@
# import asyncio
# import logging
# from logging.config import fileConfig
# from alembic import context
# from sqlalchemy import text, pool
# from sqlalchemy.ext.asyncio import async_engine_from_config
# from sqlalchemy.ext.asyncio import AsyncEngine
# from app.database.models import Base
# from app.config import URL_BD
#
# config = context.config
# config.set_main_option("sqlalchemy.url", URL_BD)
#
# if config.config_file_name is not None:
# fileConfig(config.config_file_name)
#
# target_metadata = Base.metadata
#
# logging.basicConfig()
# # logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
#
#
# logging.getLogger("sqlalchemy.engine").setLevel(logging.DEBUG)
#
#
# def run_migrations_offline() -> None:
# raise NotImplementedError("Modo offline não implementado.")
#
#
# async def run_async_migrations() -> None:
# connectable: AsyncEngine = async_engine_from_config(
# config.get_section(config.config_ini_section, {}),
# prefix="sqlalchemy.",
# poolclass=pool.NullPool,
# )
#
# args = context.get_x_argument(as_dictionary=True)
# special_schema = args.get("special_schema")
# current_tenant = args.get("tenant")
#
# if current_tenant is None and special_schema is None:
# raise Exception("Você deve fornecer 'tenant' ou 'special_schema' como argumento.")
# elif current_tenant is not None and special_schema is not None:
# raise Exception("'tenant' e 'special_schema' não podem ser usados simultaneamente.")
#
# async with connectable.connect() as async_connection:
# await async_connection.run_sync(
# lambda connection: run_migrations_online_internal(connection, current_tenant, special_schema)
# )
#
# await connectable.dispose()
#
#
# def run_migrations_online_internal(connection, current_tenant: str, special_schema: str) -> None:
# if special_schema == "shared":
# script_location = "alembic/versions/shared"
# schema_name = "shared"
# """
# Esquema da Tabela definido como o mesmo valor do esquema a serem criadas as Tabelas
# Como a tabelas comuns já tem o esquema definido nelas essa configuração garante que a tabela versão será
# criada no mesmo esquma das tabelas compartilhadas
# """
# schema_table = special_schema
#
# else:
# script_location = "alembic/versions/tenants"
# schema_name = current_tenant
# """
# Esquema da Tabela definido como None
# Como a tabelas dos inquilinos serão cadastradas cada um em um esquema diferente elas saõ configuradas como None
# Se nesse ponto já definirmos o esquema como o do inquilino a migração junto como o search_path exclui a tablea
# de versão no script de migração por isso é necessáiro configurar o esquema como None no Upgrade junto com
# o search_path a tabela de versão vai ser criada no mesmo esquema do inquilino
# """
# schema_table = None
#
# context.script.version_locations = [script_location]
#
# create_schema_if_not_exists(connection, schema_name)
# # connection.execute(text('set search_path to "%s"' % schema_name))
# # connection.commit()
# # connection.dialect.default_schema_name = schema_name
# # print("Default schema set to:", connection.dialect.default_schema_name)
#
#
# if current_tenant:
# connection.execute(text(f'SET search_path TO "{current_tenant}"'))
# # connection.execute(text('set search_path to "%s"' % current_tenant))
# connection.commit()
# else:
# print("set schema")
# connection.execute(text(f'SET search_path TO "shared"'))
# # connection.execute(text('set search_path to "%s"' % current_tenant))
# connection.commit()
#
# # Verificar o search_path configurado
# result = connection.execute(text("SHOW search_path"))
# current_search_path = result.scalar()
# print(f"Current search_path: {current_search_path}")
#
# def include_object(object, name, type_, reflected, compare_to):
#
# schema = getattr(object, "schema", None)
#
# if special_schema == "shared":
# # Sobrescreve o schema para None apenas se for 'shared'
# # object.schema = None
# # Inclusçaõ na Migração apenas as tabelas compartilhadas
# if schema == "shared":
# return True
# else:
# return False
#
# if special_schema is None:
# # Inclusão na Migração apenas as tabelas dos inquilinos
# if schema is None:
# return True
# else:
# return False
#
# # Exclui por padrão se não atender aos critérios
# return False
#
# # def include_name(name, type_, parent_names):
# # if type_ == "table":
# # return name in target_metadata.tables
# # else:
# # return True
#
# context.configure(
# connection=connection,
# target_metadata=target_metadata,
# include_object=include_object,
# version_table_schema=schema_table,
# include_schemas=True,
# # dialect_opts={"paramstyle": "named"},
# # include_name=include_name,
#
#
#
# )
#
# with context.begin_transaction():
# context.run_migrations()
#
#
# def create_schema_if_not_exists(connection, schema_name: str):
# query = text(f'CREATE SCHEMA IF NOT EXISTS "{schema_name}"')
# connection.execute(query)
# connection.commit()
#
#
# def run_migrations_online() -> None:
# asyncio.run(run_async_migrations())
#
#
# if context.is_offline_mode():
# run_migrations_offline()
# else:
# run_migrations_online()
import asyncio
import logging
from logging.config import fileConfig
from alembic import context
from sqlalchemy import text, pool
from sqlalchemy.ext.asyncio import async_engine_from_config
from sqlalchemy.ext.asyncio import AsyncEngine
from app.database.models import Base
from app.config import URL_BD
from sqlalchemy.orm import clear_mappers
clear_mappers()
config = context.config
config.set_main_option("sqlalchemy.url", URL_BD)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
logging.basicConfig()
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
def run_migrations_offline() -> None:
raise NotImplementedError("Modo offline não implementado.")
async def run_async_migrations() -> None:
connectable: AsyncEngine = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
args = context.get_x_argument(as_dictionary=True)
special_schema = args.get("special_schema")
current_tenant = args.get("tenant")
if current_tenant is None and special_schema is None:
raise Exception("Você deve fornecer 'tenant' ou 'special_schema' como argumento.")
elif current_tenant is not None and special_schema is not None:
raise Exception("'tenant' e 'special_schema' não podem ser usados simultaneamente.")
async with connectable.connect() as async_connection:
await async_connection.run_sync(
lambda connection: run_migrations_online_internal(connection, current_tenant, special_schema)
)
await connectable.dispose()
def run_migrations_online_internal(connection, current_tenant: str, special_schema: str) -> None:
if special_schema == "shared":
script_location = "alembic/versions/shared"
schema_name = "shared"
"""
Esquema da Tabela definido como o mesmo valor do esquema a serem criadas as Tabelas
Como a tabelas comuns tem o esquema definido nelas essa configuração garante que a tabela versão será
criada no mesmo esquma das tabelas compartilhadas
"""
schema_table = "shared"
else:
script_location = "alembic/versions/tenants"
schema_name = current_tenant
"""
Esquema da Tabela definido como None
Como a tabelas dos inquilinos serão cadastradas cada um em um esquema diferente elas saõ configuradas como None
Se nesse ponto definirmos o esquema como o do inquilino a migração junto como o search_path exclui a tablea
de versão no script de migração por isso é necessáiro configurar o esquema como None no Upgrade junto com
o search_path a tabela de versão vai ser criada no mesmo esquema do inquilino
"""
schema_table = None
context.script.version_locations = [script_location]
create_schema_if_not_exists(connection, schema_name)
if current_tenant:
# print("print dentro current_tenant ")
# connection.execute(text(f'SET search_path TO "{current_tenant}"'))
connection.execute(text('set search_path to "%s"' % current_tenant))
connection.commit()
connection.dialect.default_schema_name = current_tenant
if special_schema:
# print("print dentro special_schema ")
# connection.execute(text(f'SET search_path TO "{special_schema}"'))
connection.execute(text('set search_path to "%s"' % special_schema))
connection.commit()
connection.dialect.default_schema_name = special_schema
def include_object(object, name, type_, reflected, compare_to):
if special_schema == "shared":
# Inclusçaõ na Migração apenas as tabelas compartilhadas
if (type_ == "table" and (name.startswith("rbac_") or name == "inquilinos")) or type_ == "column" \
or type_ == "foreign_key_constraint":
print(f"Table included: {name}")
return True
else:
return False
if special_schema is None:
# Inclusão na Migração apenas as tabelas dos inquilinos
if (type_ == "table" and not (name.startswith("rbac_") or name == "inquilinos")) or type_ == "column"\
or type_ == "foreign_key_constraint":
return True
else:
return False
# Exclui por padrão se não atender aos critérios
return False
context.configure(
connection=connection,
target_metadata=target_metadata,
include_object=include_object,
version_table_schema=schema_table,
# include_schemas=True,
)
with context.begin_transaction():
context.run_migrations()
def create_schema_if_not_exists(connection, schema_name: str):
query = text(f'CREATE SCHEMA IF NOT EXISTS "{schema_name}"')
connection.execute(query)
connection.commit()
def run_migrations_online() -> None:
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

100
alembic/env_padrao.py Normal file
View File

@ -0,0 +1,100 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from Apagar.config import config as app_config
from app.database.session import Base
# Import the initialization function
from Apagar.init_permissions_bkp1 import init_permissions
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
config.set_main_option("sqlalchemy.url", app_config.DB_CONFIG)
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.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:
"""In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
# Call the initialization script
init_permissions()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,53 @@
- Primeiro usar a copia do arquivo ou criar uma migração vazia
alembic revision -m "Criar tabelas do schema shared"
se usar a cópia do arquivo os outros passo são desnecessários
- Ajustar imports da migração
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import fastapi_users_db_sqlalchemy
from sqlalchemy import MetaData, schema
from app.database.models import Base
- Incluir a função para filtra os shcemas shared para criar apenas eles
def get_shared_metadata():
"""Filtra as tabelas do schema 'shared'."""
meta = MetaData()
for table in Base.metadata.tables.values():
if table.schema != "tenant": # Filtra apenas as tabelas do schema 'shared'
table.to_metadata(meta)
return meta
- Ajustar função upgrade
"""Criação do schema 'shared' e tabelas associadas."""
conn = op.get_bind()
# Criar o schema 'shared' se não existir
schema_exists_query = sa.text(
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'shared';"
)
result = conn.execute(schema_exists_query)
schema_exists = result.scalar() is not None
if not schema_exists:
op.execute(schema.CreateSchema("shared"))
print("Schema 'shared' criado com sucesso.")
# Criar as tabelas do schema 'shared'
metadata = get_shared_metadata()
metadata.create_all(bind=conn) # Cria as tabelas do shared
print("Tabelas do schema 'shared' criadas com sucesso.")
- Ajustar função downgrade
"""Remoção do schema 'shared' e tabelas associadas."""
conn = op.get_bind()
# Remover as tabelas do schema 'shared'
metadata = get_shared_metadata()
metadata.drop_all(bind=conn)
print("Tabelas do schema 'shared' removidas com sucesso.")
# Remover o schema 'shared'
op.execute("DROP SCHEMA IF EXISTS shared CASCADE")
print("Schema 'shared' removido com sucesso.")

27
alembic/script.py.mako Normal file
View File

@ -0,0 +1,27 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import fastapi_users_db_sqlalchemy
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

0
app/__init__.py Normal file
View File

18
app/config.py Normal file
View File

@ -0,0 +1,18 @@
import os
def str_to_bool(value: str) -> bool:
return value.lower() in ['true', '1', 't', 'yes', 'y']
COLUNA = os.getenv('COLUNA', 'uuid')
URL_BD = os.getenv('URL_BD', 'postgresql+asyncpg://sonora:sonora@192.168.0.11:5432/pytest')
URL_BD_TESTE = os.getenv('URL_BD_TESTE', 'postgresql+asyncpg://sonora:sonora@192.168.0.11:5432/pytest')
SECRET = os.getenv('SECRET', '6be9ce93ea990b59f4448f5e84b37d785d7585245dbf2cc81e340389c2fdb4af')
ECHO = str_to_bool(os.getenv('ECHO', 'False'))
ENV = os.getenv('ENV', 'teste')
S3_ACCESS_KEY_ID = os.getenv('S3_ACCESS_KEY_ID', 'JFqmuTx4qh51kuGIzSZI')
S3_SECRET_ACCESS_KEY = os.getenv('S3_SECRET_ACCESS_KEY', 'ZjjvaDGpwDWpYO6zxgOhI0T9ibrRe7JnNl7AXyjH')
S3_BUCKET_NAME = os.getenv('S3_BUCKET_NAME', 'sistema')
S3_REGION_NAME = os.getenv('S3_REGION_NAME', 'br-vilavelha')
S3_ENDPOINT_URL = os.getenv('S3_ENDPOINT_URL', 'https://s3-api.sonoraav.com.br')

View File

@ -0,0 +1,592 @@
# Importações de bibliotecas padrão
from typing import Generic, TypeVar, Dict, Type, Union, Any, List
from uuid import UUID
import copy
# Importações de bibliotecas de terceiros
from sqlalchemy import BinaryExpression, literal
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.inspection import inspect
from sqlalchemy.future import select
from sqlalchemy import delete
from pydantic import BaseModel
from fastapi import HTTPException
from app.database.audit_log import audit_log
# Importações do seu próprio projeto
from app.database.RepositoryBase import (
RepositoryBase, IntegrityError, IntegrityConflictException,
SnippetException, NotFoundException
)
from app.database import models
Model = TypeVar("Model", bound=models.Base)
Schema = TypeVar("Schema", bound=BaseModel)
class RelationalTableRepository(RepositoryBase[Model], Generic[Model]):
def __init__(self, model: type[Model], session: AsyncSession) -> None:
super().__init__(model, session)
async def filter(
self,
*expressions: BinaryExpression,
) -> list[Model]:
query = select(self.model)
if expressions:
query = query.where(*expressions)
return list(await self.session.scalars(query))
async def create(self, data_one: Schema, *args: Any, **kwargs: Any) -> Model:
try:
if kwargs.get("related_info_extra_columns"):
db_model = await self.process_related_items_with_extra_columns(data_one, **kwargs)
else:
db_model = await self.process_related_items(data_one, **kwargs)
# Commit e refresh do modelo
await self.session.commit()
await self.session.refresh(db_model)
return db_model
except HTTPException as e:
# Repassa a HTTPException específica
raise e
except Exception as e:
raise HTTPException(status_code=500, detail="Erro ao criar relação com entidades relacionadas") from e
async def create_many(self, data: List[Schema], return_models: bool = False, *args: Any, **kwargs: Any) -> (
list[Model] | bool):
db_models = []
if kwargs.get("related_info_extra_columns"):
for single_data in data:
db_model = await self.process_related_items_with_extra_columns(single_data, **kwargs)
db_models.append(db_model)
else:
for single_data in data:
db_model = await self.process_related_items(single_data, **kwargs)
db_models.append(db_model)
try:
self.session.add_all(db_models)
await self.session.commit()
except IntegrityError:
raise IntegrityConflictException(
f"Na tabela {self.model.__tablename__} existe conflito com dados já cadastrados em campo único.",
)
except Exception as e:
raise SnippetException(f"Unknown error occurred: {e}") from e
if not return_models:
return True
for m in db_models:
await self.session.refresh(m)
return db_models
# async def update_by_id(self, update: Schema, coluna: str, *args: Any, **kwargs: Any) -> Model:
# related_info_append = kwargs.get('related_info_append', [])
# related_info_add = kwargs.get('related_info_add', [])
#
# # Prepara as variáveis para os relacionamentos simples e muitos para muitos
# simple_relationship_fields = {
# info["key"]: {"related_model": info["related_model"], "exclude_field": info["foreign_key"]}
# for info in related_info_add
# }
# many_to_many_relationship_fields = {
# info["key"]: info["related_model"] for info in related_info_append
# }
#
# uuid = str(update.uuid)
# db_model = await self.get_one_by_id(uuid, coluna, with_for_update=True)
#
# if not db_model:
# raise NotFoundException(
# f"{self.model.__tablename__} {coluna}={uuid} não encontrada."
# )
#
# values = update.model_dump(exclude_unset=True)
#
# # Atualiza campos simples
# for k, v in values.items():
# if k not in simple_relationship_fields and k not in many_to_many_relationship_fields:
# setattr(db_model, k, v)
#
# # Atualiza relacionamentos simples
# for field, details in simple_relationship_fields.items():
# related_model = details["related_model"]
# foreign_key = details["exclude_field"] # Isso é o "fk_pessoa_uuid"
#
# # Se a chave não estiver presente ou estiver com uma lista vazia, remove os itens relacionados
# if field not in values or values.get(field) == []:
# foreign_key_column = getattr(related_model, foreign_key)
#
# # Garantimos que a comparação é feita entre expressões SQL válidas
# await self.session.execute(
# delete(related_model).where(foreign_key_column == literal(db_model.uuid))
# )
# await self.session.flush()
# else:
# # Faz a Atualização de valores da lista e exclusão do que não estão na lista
# related_items = values.get(field)
# await self.update_simple_relationship(db_model=db_model, field=field, related_items=related_items,
# related_model=related_model, session=self.session,
# foreign_key=foreign_key)
#
# # Atualiza relacionamentos muitos para muitos
# for field, related_model in many_to_many_relationship_fields.items():
# # Se a chave não estiver presente ou estiver com uma lista vazia, remove os itens relacionados
# if field not in values or values.get(field) == []:
# setattr(db_model, field, [])
# else:
# related_item_ids = values.get(field)
# await self.update_many_to_many_relationship(db_model, field, related_item_ids, related_model,
# coluna=coluna)
#
# try:
# await self.session.commit()
# await self.session.refresh(db_model)
# return db_model
# except IntegrityError:
# raise IntegrityConflictException(
# f"{self.model.__tablename__} {coluna}={uuid} conflito com dados existentes."
# )
# @audit_log
async def update_by_id(self, update: Schema, coluna: str, *args: Any, **kwargs: Any) -> Model:
# Obtém as configurações dos relacionamentos
related_info_append = kwargs.get('related_info_append', [])
related_info_add = kwargs.get('related_info_add', [])
related_info_extra = kwargs.get('related_info_extra_columns', [])
# Preparação dos relacionamentos simples (one-to-many) e muitos-para-muitos simples
simple_relationship_fields = {
info["key"]: {"related_model": info["related_model"], "exclude_field": info["foreign_key"]}
for info in related_info_add
}
many_to_many_relationship_fields = {
info["key"]: info["related_model"] for info in related_info_append
}
# Para relacionamentos com extra columns, armazenamos a configuração completa
extra_relationship_fields = {info["key"]: info for info in related_info_extra}
# Busca o objeto base a ser atualizado
uuid = str(update.uuid)
db_model = await self.get_one_by_id(uuid, coluna, with_for_update=True)
if not db_model:
raise NotFoundException(f"{self.model.__tablename__} {coluna}={uuid} não encontrada.")
# Guardar o estado atual antes da modificação (cópia profunda para evitar problemas com lazy attributes)
original_model = copy.deepcopy(db_model)
values = update.model_dump(exclude_unset=True)
# Atualiza os campos simples (excluindo os campos que representam relacionamentos)
all_relationship_keys = set(simple_relationship_fields.keys()) | set(
many_to_many_relationship_fields.keys()) | set(extra_relationship_fields.keys())
for k, v in values.items():
if k not in all_relationship_keys:
setattr(db_model, k, v)
# Atualiza relacionamentos simples (one-to-many)
for field, details in simple_relationship_fields.items():
related_model = details["related_model"]
foreign_key = details["exclude_field"] # por exemplo, "fk_pessoa_uuid"
if field not in values or values.get(field) == []:
foreign_key_column = getattr(related_model, foreign_key)
await self.session.execute(
delete(related_model).where(foreign_key_column == literal(db_model.uuid))
)
await self.session.flush()
else:
related_items = values.get(field)
await self.update_simple_relationship(
db_model=db_model,
field=field,
related_items=related_items,
related_model=related_model,
session=self.session,
foreign_key=foreign_key
)
# Atualiza relacionamentos muitos-para-muitos simples
for field, related_model in many_to_many_relationship_fields.items():
if field not in values or values.get(field) == []:
setattr(db_model, field, [])
else:
related_item_ids = values.get(field)
await self.update_many_to_many_relationship(
db_model=db_model,
field=field,
related_item_ids=related_item_ids,
related_model=related_model,
coluna=coluna)
# Atualiza relacionamentos muitos-para-muitos com campos extras
for field, config in extra_relationship_fields.items():
# Se o campo não foi enviado no update, ignore a atualização desse relacionamento.
if field not in values:
continue
new_items = values.get(field) # Espera-se que seja uma lista (pode ser vazia)
await self.update_many_to_many_extra_relationship(
db_model=db_model,
field=field,
new_items=new_items,
association_model=config["association_model"],
base_foreign_key=config["base_foreign_key"],
related_foreign_key=config["related_foreign_key"],
extra_fields=config.get("extra_fields", [])
)
try:
await self.session.commit()
await self.session.refresh(db_model)
return db_model
except IntegrityError:
raise IntegrityConflictException(
f"{self.model.__tablename__} {coluna}={uuid} conflito com dados existentes."
)
async def update_many_by_ids(self, *args, **kwargs):
raise NotImplementedError("Update many não implementado para relacionamentos.")
@staticmethod
def _create_base_model(data: Schema, db_data,
related_info_append: List[Dict[str, Any]],
related_info_add: List[Dict[str, Any]],
related_info_extra_columns: List[Dict[str, Any]]) -> Model:
# Inicia com as chaves dos relacionamentos simples (append e add)
exclude_keys = {info["key"] for info in related_info_append + related_info_add + related_info_extra_columns}
return db_data(**data.model_dump(exclude=exclude_keys))
async def _collect_related_items(self, data: Schema, related_info_append: List[Dict[str, Any]]) -> (
Dict[str, List[Any]]):
related_items_to_append = {}
for info in related_info_append:
key = info["key"]
related_model = info["related_model"]
foreign_key_field = info["foreign_key_field"]
related_items_to_append[key] = []
related_data = getattr(data, key, [])
if not related_data:
continue # Pula para a próxima iteração se não houver dados para este campo
for related_item in related_data:
related_item_id = related_item[foreign_key_field] if isinstance(related_item, dict) else getattr(
related_item, foreign_key_field)
related_item_instance = await self.session.get(related_model, related_item_id)
if not related_item_instance:
raise HTTPException(status_code=400, detail=f"ID {related_item_id} inválido")
related_items_to_append[key].append(related_item_instance)
return related_items_to_append
async def process_related_items(self, data: Schema, **kwargs: Any) -> Model:
# Obtendo argumentos adicionais
db_data = kwargs.get('db_data')
related_info_append = kwargs.get('related_info_append', [])
related_info_add = kwargs.get('related_info_add', [])
related_info_extra_columns = []
# Criação do modelo base
db_model = self._create_base_model(data, db_data, related_info_append, related_info_add,
related_info_extra_columns)
# Processamento de related_info_append, se presente
if related_info_append:
related_items_to_append = await self._collect_related_items(data, related_info_append)
if related_items_to_append: # Verifica se há itens para serem relacionados
self._append_related_items(db_model, related_items_to_append)
# Adiciona o modelo à sessão
self.session.add(db_model)
await self.session.flush()
# Processamento de related_info_add, se presente
if related_info_add:
await self._add_related_items(data, db_model, related_info_add)
return db_model
async def process_related_items_with_extra_columns(self, data: Schema, **kwargs: Any) -> Model:
"""
Processa os relacionamentos onde a tabela de associação possui colunas extras.
Espera-se que em kwargs seja informado:
- db_data: a classe/modelo base a ser instanciado com os dados.
- related_info_extra_columns: uma lista de dicionários com as seguintes chaves:
* key: nome do atributo no schema contendo os dados do relacionamento.
* association_model: o modelo da tabela de associação (com colunas extras).
* base_foreign_key: nome da coluna que referencia o modelo base.
* related_foreign_key: nome da coluna que referencia a entidade relacionada.
* related_model: modelo da entidade relacionada (para validação, por exemplo).
* extra_fields: (opcional) lista de nomes dos campos extras que deverão ser extraídos.
"""
# Cria o modelo base usando os dados enviados no schema
db_data = kwargs.get('db_data')
related_info_append = []
related_info_extra_columns = kwargs.get('related_info_extra_columns', [])
related_info_add = kwargs.get('related_info_add', [])
db_model = self._create_base_model(data, db_data, related_info_append, related_info_add,
related_info_extra_columns,
)
self.session.add(db_model)
await self.session.flush() # Garante que db_model possua sua PK definida (ex.: uuid)
# Processa os relacionamentos com colunas extras
related_info_extra = kwargs.get('related_info_extra_columns', [])
for info in related_info_extra:
key = info.get("key")
association_model = info.get("association_model")
base_foreign_key = info.get("base_foreign_key")
related_foreign_key = info.get("related_foreign_key")
related_model = info.get("related_model") # Para validação do item relacionado
extra_fields = info.get("extra_fields", [])
# Obtém os dados do relacionamento a partir do schema
related_items = getattr(data, key, [])
if not related_items:
continue
for item in related_items:
# Verifica se o identificador da entidade relacionada está presente no item
if not hasattr(item, related_foreign_key):
raise HTTPException(
status_code=400,
detail=f"O campo '{related_foreign_key}' é obrigatório em '{key}'."
)
related_item_id = item[related_foreign_key] if isinstance(item, dict) else getattr(item,
related_foreign_key)
# Valida se o item relacionado existe (usando o modelo relacionado)
if not related_model:
raise HTTPException(
status_code=400,
detail="Não foi definido o modelo relacionado para o relacionamento."
)
related_instance = await self.session.get(related_model, related_item_id)
if not related_instance:
raise HTTPException(
status_code=400,
detail=f"ID {related_item_id} inválido para {related_model.__tablename__}"
)
# Extrai os valores dos campos extras, se existirem
extra_data = {field: getattr(item, field) for field in extra_fields if hasattr(item, field)}
# Cria a instância da tabela de associação, populando as FKs e os campos extras
association_instance = association_model(**{
base_foreign_key: db_model.uuid,
related_foreign_key: related_item_id,
**extra_data
})
self.session.add(association_instance)
# Realiza um flush para persistir os itens da associação antes de continuar
await self.session.flush()
# Se houver relacionamentos do tipo um-para-muitos (add), processa-os também
related_info_add = kwargs.get('related_info_add', [])
if related_info_add:
await self._add_related_items(data, db_model, related_info_add)
return db_model
@staticmethod
def _append_related_items(db_model: Model, related_items_to_append: Dict[str, List[Model]]) -> None:
for key, items in related_items_to_append.items():
getattr(db_model, key).extend(items)
async def _add_related_items(self, data: Schema, db_model: Model, related_info_add: List[Dict[str, Any]]) -> None:
for info in related_info_add:
key = info["key"]
foreign_key = info["foreign_key"]
related_model = info["related_model"]
relations = info.get("relations", []) # Pode ser uma lista de relações
related_items = getattr(data, key, [])
if not related_items:
continue # Pula para a próxima iteração se não houver dados para este campo
for related_item in related_items:
for relation in relations:
related_model_fk = relation.get("related_model_fk")
foreign_key_fk = relation.get("foreign_key_fk")
if related_model_fk and foreign_key_fk:
fk = getattr(related_item, foreign_key_fk)
relacao = await self.session.get(related_model_fk, fk)
if not relacao:
raise HTTPException(status_code=404,
detail=f"{related_model_fk.__name__} com UUID {fk} não encontrado")
new_item = related_model(**related_item.model_dump(), **{foreign_key: db_model.uuid})
self.session.add(new_item)
@staticmethod
async def update_simple_relationship(
db_model: Model,
field: str,
related_items: List[Dict[str, Any]],
related_model: Type[Model],
session: AsyncSession,
foreign_key: str
) -> None:
current_items = getattr(db_model, field, [])
# Lista de UUIDs dos itens enviados no JSON
new_item_uuids = {str(item['uuid']) for item in related_items if 'uuid' in item}
# Remover itens que não estão mais presentes no JSON
for item in current_items:
if str(item.uuid) not in new_item_uuids:
foreign_key_column = getattr(related_model, foreign_key)
await session.execute(
delete(related_model).where(foreign_key_column == literal(db_model.uuid)).where(
related_model.uuid == literal(item.uuid)
)
)
await session.flush()
# Atualizar ou adicionar novos itens
for related_item in related_items:
if 'uuid' in related_item:
item_uuid = str(related_item['uuid'])
db_item = next((item for item in current_items if str(item.uuid) == item_uuid), None)
if db_item:
for k, v in related_item.items():
setattr(db_item, k, v)
else:
raise NotFoundException(f"Related item {field} with UUID {item_uuid} not found.")
else:
new_item = related_model(**related_item)
setattr(new_item, db_model.__class__.__name__.lower() + "_uuid", db_model.uuid)
session.add(new_item)
current_items.append(new_item)
setattr(db_model, field, current_items)
await session.flush()
async def update_many_to_many_relationship(
self,
db_model: Model,
field: str,
related_item_ids: List[Union[str, UUID, Dict[str, Any]]], # Aceita "strings", UUIDs ou dicionários
related_model: Type[Model], # Adicionando o modelo relacionado
coluna: str = "uuid" # Adicionando a coluna para uso dinâmico
) -> None:
current_items = getattr(db_model, field, [])
current_item_ids = {str(getattr(item, coluna)) for item in current_items}
# Extrair "IDs" dos dicionários, se necessário
new_item_ids = set()
for item in related_item_ids:
if isinstance(item, dict) and coluna in item:
new_item_ids.add(str(item[coluna]))
else:
new_item_ids.add(str(item))
# Remover itens que não estão mais relacionados
items_to_remove = current_item_ids - new_item_ids
for item in current_items:
if str(getattr(item, coluna)) in items_to_remove:
current_items.remove(item)
# Adicionar novos itens
items_to_add = new_item_ids - current_item_ids
for item_id in items_to_add:
related_item = await self.session.get(related_model, item_id)
if related_item:
current_items.append(related_item)
else:
raise NotFoundException(f"Related item {field} with UUID {item_id} not found.")
setattr(db_model, field, current_items)
async def update_many_to_many_extra_relationship(
self,
db_model: Model,
field: str,
new_items: List[Union[Dict[str, Any], Any]], # Aceita dicionários ou objetos com .dict()
association_model: Type[Model],
base_foreign_key: str,
related_foreign_key: str,
extra_fields: List[str] = [],
) -> None:
"""
Atualiza um relacionamento muitos-para-muitos com colunas extras.
Parâmetros:
- db_model: objeto base que possui o relacionamento.
- field: nome do atributo em db_model que contém a lista de associações.
- new_items: lista de novos itens para o relacionamento (pode ser dicionário ou objeto com .dict()).
- association_model: classe mapeada da tabela de associação.
- base_foreign_key: nome do atributo da associação que referencia db_model (ex: 'fk_manutencao_uuid').
- related_foreign_key: nome do atributo da associação que referencia o objeto relacionado (ex: 'fk_itens_equipamentos_uuid').
- extra_fields: lista de campos extras que também devem ser atualizados ou definidos.
"""
current_assocs = getattr(db_model, field, [])
if not new_items:
# Se o payload enviar uma lista vazia, remova todas as associações
for assoc in current_assocs:
await self.session.delete(assoc)
setattr(db_model, field, [])
await self.session.flush()
return
new_assoc_instances = []
for new_item in new_items:
# Suporta tanto dicionários quanto objetos com método .dict()
new_item_data = new_item if isinstance(new_item, dict) else new_item.dict()
if related_foreign_key not in new_item_data:
raise HTTPException(
status_code=400,
detail=f"O campo '{related_foreign_key}' é obrigatório em '{field}'."
)
new_related_id = new_item_data[related_foreign_key]
# Procura uma associação existente que corresponda ao valor do foreign key
existing_assoc = next(
(assoc for assoc in current_assocs if str(getattr(assoc, related_foreign_key)) == str(new_related_id)),
None
)
if existing_assoc:
# Atualiza os campos extras se houver valores informados
for ef in extra_fields:
if ef in new_item_data:
setattr(existing_assoc, ef, new_item_data[ef])
new_assoc_instances.append(existing_assoc)
else:
# Cria uma nova associação, incluindo os campos extras
extra_data = {ef: new_item_data.get(ef) for ef in extra_fields if ef in new_item_data}
new_assoc = association_model(**{
base_foreign_key: db_model.uuid,
related_foreign_key: new_related_id,
**extra_data
})
self.session.add(new_assoc)
await self.session.flush()
new_assoc_instances.append(new_assoc)
# Mescla as associações atuais com as novas (ou atualizadas)
merged_assocs = list({*current_assocs, *new_assoc_instances})
setattr(db_model, field, merged_assocs)

View File

@ -0,0 +1,474 @@
# Importações de bibliotecas padrão
from uuid import UUID
from typing import Generic, TypeVar, Any, List, Optional, Dict, Union
# Importações de bibliotecas de terceiros
from sqlalchemy import select, delete, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from pydantic import BaseModel
from fastapi import HTTPException, status
from sqlalchemy.orm import joinedload
from sqlalchemy import and_
# Importações do seu próprio projeto
from app.database import models
from app.database.audit_log import audit_log
from app.database.TratamentoErros import ErrorHandler
Model = TypeVar("Model", bound=models.Base)
Schema = TypeVar("Schema", bound=BaseModel)
class SnippetException(Exception):
pass
class IntegrityConflictException(Exception):
pass
class NotFoundException(Exception):
pass
class RepositoryBase(Generic[Model]):
"""Repository for performing database queries."""
def __init__(
self, model: type[Model],
session: AsyncSession,
default_order_by: str = None,
ascending: bool = True) -> None:
self.model = model
self.session = session
self.default_order_by = default_order_by
self.ascending = ascending
async def create(self, data_one: Schema) -> Model:
try:
db_model = self.model(**data_one.model_dump())
self.session.add(db_model)
await self.session.commit()
await self.session.refresh(db_model)
return db_model.__dict__
except SQLAlchemyError as e:
handler = ErrorHandler()
handler.handle_error(e)
# Testado ok
async def create_many(self, data: List[Schema], return_models: bool = False) -> list[Model] | bool:
# Cria instâncias dos modelos a partir dos dados fornecidos
db_models = [self.model(**d.model_dump()) for d in data]
try:
self.session.add_all(db_models)
await self.session.commit()
except SQLAlchemyError as e:
handler = ErrorHandler()
handler.handle_error(e)
if not return_models:
return True
for m in db_models:
await self.session.refresh(m)
return db_models
async def get_one_by_id(self, uuid: str | UUID, coluna: str, with_for_update: bool = False, ) -> Model:
try:
q = select(self.model).where(getattr(self.model, coluna) == uuid)
except AttributeError:
raise HTTPException(status_code=400, detail=f"A Coluna {coluna} não existe em: {self.model.__tablename__}.")
# Verifica se o modelo tem a coluna 'ativo' e adiciona a condição
if hasattr(self.model, 'ativo'):
q = q.where(self.model.ativo.is_(True))
if with_for_update:
q = q.with_for_update()
results = await self.session.execute(q)
result = results.unique().scalar_one_or_none()
if result is None:
raise HTTPException(status_code=404,
detail=f"Registro com {coluna}={uuid} não encontrado na tabela "
f"{self.model.__tablename__}.")
return result
async def get_many_by_ids(self, coluna: str, uuids: List[str | UUID] = None, with_for_update: bool = False,
order_by: str = None, ascending: bool = True):
try:
q = select(self.model)
if uuids:
try:
q = q.where(getattr(self.model, coluna).in_(uuids))
except AttributeError:
raise HTTPException(
status_code=400,
detail=f"A coluna '{coluna}' não existe em: {self.model.__tablename__}."
)
# Verifica se o modelo tem a coluna 'ativo' e adiciona a condição
if hasattr(self.model, 'ativo'):
q = q.where(self.model.ativo.is_(True))
if with_for_update:
q = q.with_for_update()
# Verificar se a ordenação foi solicitada e aplicar à consulta
if order_by:
order_by_column = getattr(self.model, order_by, None)
if not order_by_column:
raise HTTPException(
status_code=400,
detail=f"A coluna de ordenação '{order_by}' não foi encontrada na tabela "
f"{self.model.__tablename__}."
)
q = q.order_by(order_by_column.asc() if ascending else order_by_column.desc())
rows = await self.session.execute(q)
return rows.unique().scalars().all()
except SQLAlchemyError as e:
handler = ErrorHandler()
handler.handle_error(e)
async def get_filter(
self,
coluna: str,
uuids: Optional[List[Union[str, UUID]]] = None,
filters: Optional[List[Dict[str, Any]]] = None,
relationships: Optional[List[str]] = None,
order_by: Optional[Union[str, List[str]]] = None, # Aceita str ou List[str]
ascending: Optional[List[bool]] = None
):
try:
query = select(self.model)
# Adicionar relacionamentos com joinedload para otimizar carregamento
if relationships:
for relation in relationships:
try:
relation_attr = getattr(self.model, relation)
query = query.options(joinedload(relation_attr))
except AttributeError:
raise ValueError(
f"Relacionamento '{relation}' não encontrado no modelo '{self.model.__name__}'.")
# Aplicar filtros dinâmicos com suporte a múltiplos níveis de relacionamento
if filters:
# Inicializamos um conjunto para controlar os joins já visitados
visited_joins = set()
# Acumular condições separadas por tipo lógico
and_conditions = []
or_conditions = []
for condition in filters: # Iteramos diretamente sobre a lista de condições
column_path = getattr(condition, "column") # Acessamos diretamente os atributos
operator = getattr(condition, "operator", "==") # Operador padrão é igualdade
value = getattr(condition, "value")
logical = getattr(condition, "logical", "AND") # Operador lógico padrão é AND
path_parts = column_path.split(".")
current_model = self.model
for i, part in enumerate(path_parts[:-1]): # Navegar pelos relacionamentos no caminho
if part not in visited_joins: # Verificar se o relacionamento já foi adicionado
# Obtemos o relacionamento usando o modelo atual
try:
related_table = getattr(current_model, part).property.mapper.class_
except AttributeError as e:
raise ValueError(
f"Relacionamento '{part}' não encontrado em '{current_model.__name__}'."
f"Erro: {str(e)}"
)
# Adicionamos o relacionamento à query com um join
query = query.join(related_table, isouter=True)
# Registramos o relacionamento no conjunto para evitar duplicação
visited_joins.add(part)
else:
# Atualizar o modelo mesmo que o join seja pulado
try:
related_table = getattr(current_model, part).property.mapper.class_
except AttributeError as e:
raise ValueError(
f"Relacionamento '{part}' não encontrado em '{current_model.__name__}'."
f"Erro: {str(e)}"
)
# Atualizamos o modelo atual para o próximo relacionamento
current_model = related_table
# Obtém a coluna final no caminho para aplicar o filtro
try:
final_column = getattr(current_model, path_parts[-1])
except AttributeError as e:
raise ValueError(
f"Coluna '{path_parts[-1]}' não encontrada em '{current_model.__name__}'."
f"Erro: {str(e)}"
)
# Mapear operadores para SQLAlchemy
operator_mapping = {
"==": final_column == value,
"!=": final_column != value,
">": final_column > value,
"<": final_column < value,
">=": final_column >= value,
"<=": final_column <= value,
"IN": final_column.in_(value) if isinstance(value, list) else final_column == value,
"NOT IN": final_column.notin_(value) if isinstance(value, list) else final_column != value,
"LIKE": final_column.like(f"%{value}%"),
"ILIKE": final_column.ilike(f"%{value}%"), # Apenas para PostgreSQL
"IS NULL": final_column.is_(None),
"IS NOT NULL": final_column.isnot(None),
"BETWEEN": final_column.between(value[0], value[1]) if isinstance(value, list) and len(
value) == 2 else None,
}
# Adiciona a condição à lista apropriada (AND ou OR)
if operator in operator_mapping:
condition_expression = operator_mapping[operator]
if logical.upper() == "AND":
and_conditions.append(condition_expression)
elif logical.upper() == "OR":
or_conditions.append(condition_expression)
else:
raise ValueError(f"Operador '{operator}' não suportado.")
# Aplicar condições acumuladas na query
if and_conditions:
query = query.filter(and_(*and_conditions)) # Aplica todos os AND combinados
if or_conditions:
query = query.filter(or_(*or_conditions)) # Aplica todos os OR combinados
# Filtrar por IDs
if uuids:
query = query.where(getattr(self.model, coluna).in_(uuids))
# Verifica se o modelo tem a coluna 'ativo' e adiciona a condição
if hasattr(self.model, 'ativo'):
query = query.where(self.model.ativo.is_(True))
# Ordenação
# Resolução de colunas no contexto do modelo e seus relacionamentos
if order_by:
if ascending is None:
ascending = [True] * len(order_by) # Define `True` para todas as colunas por padrão
if filters: # Caso existam filtros, usamos o formato atual
for i, order_col in enumerate(order_by):
path_parts = order_col.split(".")
column = self.model
# Percorrer os relacionamentos
for part in path_parts[:-1]:
column = getattr(column, part).property.mapper.class_
# Resgatar a coluna final
final_column = getattr(column, path_parts[-1], None)
if final_column is None:
raise ValueError(f"Coluna de ordenação '{order_col}' não encontrada.")
# Adicionar a ordenação na consulta
query = query.order_by(
final_column.asc() if ascending[i] else final_column.desc()
)
else: # Caso não existam filtros, usamos o formato simples
order_by_column = getattr(self.model, order_by, None)
if not order_by_column:
raise ValueError(
f"A coluna de ordenação '{order_by}' não foi encontrada na tabela "
f"{self.model.__tablename__}."
)
query = query.order_by(
order_by_column.asc() if ascending else order_by_column.desc()
)
# Executar a consulta
result = await self.session.execute(query)
return result.scalars().all()
except SQLAlchemyError as e:
# Lidar com erros do SQLAlchemy
http_exception = HTTPException(
status_code=400,
detail=f"Erro interno do servidor ao acessar o banco de dados: {str(e)}"
)
raise http_exception
@audit_log
async def update_by_id(self, update: Schema, coluna: str) -> dict:
uuid = str(update.uuid)
db_model = await self.get_one_by_id(uuid, coluna, with_for_update=True)
if not db_model:
raise HTTPException(status_code=404,
detail=f"{self.model.__tablename__}{coluna}={uuid} não encontrada.")
# Guardar o estado atual antes da modificação
original_model = db_model.__dict__.copy()
values = update.model_dump(exclude_unset=True)
for k, v in values.items():
setattr(db_model, k, v)
try:
return {"db_models": db_model, "original_models": original_model, "operation": "UPDATE"}
except IntegrityError:
raise IntegrityConflictException(
f"{self.model.__tablename__} {coluna}={uuid} conflito com dados existentes."
)
@audit_log
async def update_many_by_ids(self, updates: List[Schema], coluna: str, return_models: bool = False) -> dict:
uuids = [str(update.uuid) for update in updates]
db_models = await self.get_many_by_ids(coluna, uuids, with_for_update=True)
if not db_models:
raise HTTPException(status_code=404,
detail=f"{self.model.__tablename__} {coluna}={uuids} não encontrada.")
try:
# Capturar o estado original dos modelos antes da modificação
original_models = [db_model.__dict__.copy() for db_model in db_models]
# Aplicar as atualizações
for db_model in db_models:
update_data = next((item for item in updates if item.uuid == getattr(db_model, coluna)), None)
if update_data:
values = update_data.model_dump(exclude_unset=True)
for k, v in values.items():
setattr(db_model, k, v)
# Retornar os modelos atualizados e os originais para o audit_log
return {
"db_models": db_models, # Lista de modelos que foram modificados
"original_models": original_models, # Lista de estados originais
"operation": "UPDATE"
}
except SQLAlchemyError as e:
handler = ErrorHandler()
handler.handle_error(e)
@audit_log
async def remove_by_id(self, uuid: str | UUID, coluna: str) -> dict:
if not uuid:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Não foi informado nenhum UUID")
# Tentar buscar o objeto antes de removê-lo para auditoria
db_model = await self.get_one_by_id(uuid, coluna)
if not db_model:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"{self.model.__tablename__} com {coluna}={uuid} não encontrado.")
try:
query = delete(self.model).where(getattr(self.model, coluna) == uuid)
except AttributeError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"A Coluna {coluna} não existe em: {self.model.__tablename__}.")
try:
rows = await self.session.execute(query)
await self.session.flush() # Confirma a exclusão, mas não comita ainda
if rows.rowcount is None or rows.rowcount == 0:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, )
# Retorna o modelo deletado e o rowcount
return {"db_models": db_model, "rowcount": rows.rowcount, "operation": "DELETE"}
except SQLAlchemyError as e:
handler = ErrorHandler()
handler.handle_error(e)
@audit_log
async def remove_many_by_ids(self, uuids: List[str | UUID], coluna: str) -> dict:
if not uuids:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Não foi informando nenhum uuid")
# Obter os modelos antes de deletá-los para fins de auditoria
db_models = await self.get_many_by_ids(coluna, uuids)
if not db_models:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"{self.model.__tablename__} {coluna}={uuids} não encontrada.")
try:
query = delete(self.model).where(getattr(self.model, coluna).in_(uuids))
rows = await self.session.execute(query)
except IntegrityError:
await self.session.rollback()
raise IntegrityConflictException(
f"Erro ao deletar registros em {self.model.__tablename__}."
)
if rows.rowcount is None or rows.rowcount == 0:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
# Retornar os modelos deletados e a contagem de linhas para o audit_log
return {
"db_models": db_models, # Modelos que foram deletados
"operation": "DELETE", # Especifica que é uma operação de deleção
"rowcount": rows.rowcount # Número de registros deletados
}
async def remove_by_column(self, column_name: str, value: str | UUID) -> dict:
"""
Remove um registro com base no nome da coluna e no valor correspondente.
"""
try:
# Verificar se a coluna existe no modelo
if not hasattr(self.model, column_name):
raise HTTPException(
status_code=404,
detail=f"A coluna {column_name} não existe em {self.model.__tablename__}."
)
# Executar a exclusão
query = delete(self.model).where(getattr(self.model, column_name) == value)
rows = await self.session.execute(query)
await self.session.flush()
if rows.rowcount is None or rows.rowcount == 0:
raise HTTPException(
status_code=404,
detail=f"Nenhum registro encontrado em {self.model.__tablename__} com {column_name}={value}."
)
return {"rowcount": rows.rowcount, "operation": "DELETE", "column": column_name, "value": value}
except SQLAlchemyError as e:
handler = ErrorHandler()
handler.handle_error(e)
async def ativar_registro(self, update: Schema, coluna: str) -> dict:
"""
Ativa um registro atualizando o campo 'ativo' para True.
"""
update.ativo = True
return await self.update_by_id(update, coluna)
async def desativar_registro(self, update: Schema, coluna: str) -> dict:
"""
Desativa um registro atualizando o campo 'ativo' para False.
"""
update.ativo = False
return await self.update_by_id(update, coluna)

View File

@ -0,0 +1,109 @@
from fastapi import HTTPException, status
from sqlalchemy.exc import IntegrityError, DataError, OperationalError, TimeoutError
from sqlalchemy.orm.exc import NoResultFound, StaleDataError
class ErrorHandler:
def handle_error(self, exception):
"""
Método principal para tratar exceções do SQLAlchemy e gerar respostas apropriadas.
"""
if isinstance(exception, IntegrityError):
return self.handle_integrity_error(exception)
elif isinstance(exception, DataError):
return self.handle_data_error(exception)
elif isinstance(exception, OperationalError):
return self.handle_operational_error(exception)
elif isinstance(exception, TimeoutError):
return self.handle_timeout_error(exception)
elif isinstance(exception, StaleDataError):
return self.handle_concurrency_error(exception)
elif isinstance(exception, NoResultFound):
return self.handle_no_result_found(exception)
else:
return self.handle_generic_error(exception)
@staticmethod
def handle_integrity_error(exception):
"""
Trata erros de integridade, como violações de chaves únicas ou campos not-null.
"""
if 'not-null constraint' in str(exception.orig):
column_name = str(exception.orig).split('column "')[1].split('"')[0]
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"O campo '{column_name}' não pode ser nulo. Por favor, forneça um valor válido."
)
elif 'unique constraint' in str(exception.orig):
column_name = str(exception.orig).split('constraint "')[1].split('"')[0]
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Violação de unicidade: O valor do campo '{column_name}' "
f"já está em uso. Por favor, use um valor único."
)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro de integridade no banco de dados."
)
@staticmethod
def handle_data_error(exception):
"""
Trata erros de dados, como formatação ou valores fora dos limites.
"""
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Erro de dados: {str(exception.orig)}"
)
@staticmethod
def handle_operational_error(exception):
"""
Trata erros de conexão ou operacionais com o banco de dados.
"""
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro de conexão com o banco de dados. Por favor, tente novamente mais tarde."
)
@staticmethod
def handle_timeout_error(exception):
"""
Trata erros de timeout em transações com o banco de dados.
"""
raise HTTPException(
status_code=status.HTTP_408_REQUEST_TIMEOUT,
detail="Ocorreu um timeout durante a operação. Por favor, tente novamente."
)
@staticmethod
def handle_concurrency_error(exception):
"""
Trata erros de concorrência quando múltiplas transações tentando modificar o mesmo dado.
"""
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Erro de concorrência. O dado foi modificado por outra transação."
)
@staticmethod
def handle_no_result_found(exception):
"""
Trata erros de busca sem resultado no banco de dados.
"""
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Nenhum resultado encontrado."
)
@staticmethod
def handle_generic_error(exception):
"""
Trata erros genéricos de SQLAlchemy e gera uma resposta padrão de erro.
"""
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ocorreu um erro inesperado no banco de dados. Por favor, tente novamente mais tarde."
)

0
app/database/__init__.py Normal file
View File

468
app/database/audit_log.py Normal file
View File

@ -0,0 +1,468 @@
from sqlalchemy import inspect
from app.database.models import HistoricoAlteracoes, HistoricoDelete, HistoricoUpdate
from datetime import datetime, timezone
# def audit_log(func):
# async def wrapper(*args, **kwargs):
# self = args[0] # O primeiro argumento será 'self', que é a instância do repositório
# session = self.session
#
# # Verificar se o modelo está habilitado para auditoria
# if not getattr(self.model, 'log_auditoria_habilitado', False):
# # Executa a função original sem auditoria
# result = await func(*args, **kwargs)
#
# # Realiza o commit e refresh
# await session.commit()
#
# # Verificar se é uma operação de UPDATE ou DELETE
# db_models = result.get("db_models")
# operation = result.get("operation")
# rowcount = result.get("rowcount")
#
# # Atualiza o db_model com refresh em caso de UPDATE
# if operation == "UPDATE" and db_models:
# for db_model in db_models:
# await session.refresh(db_model)
#
# # Retorna o que a rota espera: o db_model para update ou rowcount para delete
# if operation == "UPDATE":
# if len(db_models) == 1:
# return db_models[0].__dict__ # Retorna o único modelo como dicionário
# else:
# return db_models # Retorna a lista de modelos
# else:
# return rowcount # Retorna rowcount no caso de DELETE
#
# # Caso a auditoria esteja habilitada, continuar com o processo de auditoria
# # Tenta executar a função original e captura o retorno
# result = await func(*args, **kwargs)
#
# # Captura o db_model e a operação realizada
# db_models = result.get("db_models")
# original_models = result.get("original_models")
# operation = result.get("operation")
# rowcount = result.get("rowcount")
#
# # Para operações de UPDATE
# log_entries = []
# if operation == "UPDATE" and db_models:
# timestamp = datetime.now(timezone.utc).replace(tzinfo=None)
#
# # Iterar pelos modelos atualizados
# for db_model, original_model in zip(db_models, original_models):
# inspector = inspect(db_model)
# primary_key = str(inspect(db_model).identity[0])
#
# # Verificar alterações em cada atributo
# for attr in inspector.attrs:
# old_value = original_model.get(attr.key, None)
# new_value = getattr(db_model, attr.key, None)
#
# # Confirmar se houve mudança nos valores
# if old_value != new_value:
# log_entry = Historico_Alteracoes(
# tabela=db_model.__tablename__,
# coluna=attr.key,
# valor_antigo=str(old_value) if old_value else None,
# valor_novo=str(new_value) if new_value else None,
# data_modificacao=timestamp,
# action='UPDATE',
# usuario_id=kwargs.get('user_id'),
# registro_id=primary_key
# )
# log_entries.append(log_entry)
#
# # Verificar se é uma operação de DELETE
# elif operation == "DELETE":
# timestamp_naive = datetime.now(timezone.utc).replace(tzinfo=None)
#
# if isinstance(db_models, list): # Caso seja uma lista de modelos deletados (delete_many)
# for db_model in db_models:
# primary_key = str(inspect(db_model).identity[0])
# deletado = {attr: value for attr, value in db_model.__dict__.items()
# if not isinstance(value, list) and not attr.startswith('_')}
#
# json_deletado = json.dumps(deletado, default=str)
#
# log_entry = Historico_Alteracoes(
# tabela=db_model.__tablename__,
# coluna='N/A', # Especificar que esta é uma operação de DELETE
# valor_antigo=str(deletado), # Salvar o estado completo do modelo deletado
# valor_novo=None, # Valor novo é None porque o registro foi deletado
# data_modificacao=timestamp_naive,
# action='DELETE',
# usuario_id=kwargs.get('user_id'),
# registro_id=primary_key
# )
# log_entries.append(log_entry)
#
# else: # Caso seja apenas um modelo (delete_one)
# primary_key = str(inspect(db_models).identity[0])
# deletado = {attr: value for attr, value in db_models.__dict__.items()
# if not isinstance(value, list) and not attr.startswith('_')}
#
# log_entry = Historico_Alteracoes(
# tabela=db_models.__tablename__,
# coluna='N/A', # Especificar que esta é uma operação de DELETE
# valor_antigo=str(deletado), # Salvar o estado completo do modelo deletado
# valor_novo=None, # Valor novo é None porque o registro foi deletado
# data_modificacao=timestamp_naive,
# action='DELETE',
# usuario_id=kwargs.get('user_id'),
# registro_id=primary_key
# )
# log_entries.append(log_entry)
#
# # Se houver log_entries a serem salvas (no caso de UPDATE ou DELETE)
# if log_entries:
# session.add_all(log_entries)
#
# # Realizar o commit no final, para ambos UPDATE e DELETE
# await session.commit()
#
# # Se for um update, atualiza o db_model com refresh
# if operation == "UPDATE" and db_models:
# for db_model in db_models:
# await session.refresh(db_model)
#
# # Retorna o que a rota espera: o db_model para update ou rowcount para delete
# if operation == "UPDATE":
# if len(db_models) == 1:
# return db_models[0].__dict__ # Retorna o único modelo como dicionário
# else:
# return db_models # Retorna a lista de modelos
# else:
# return rowcount # Retorna rowcount no caso de DELETE
#
# return wrapper
# def audit_log(func):
# async def wrapper(*args, **kwargs):
# self = args[0] # O primeiro argumento será 'self', que é a instância do repositório
# session = self.session
#
# # Verificar se o modelo está habilitado para auditoria
# if not getattr(self.model, 'log_auditoria_habilitado', False):
# # Executa a função original sem auditoria
# result = await func(*args, **kwargs)
#
# # Realiza o commit e refresh
# await session.commit()
#
# # Verificar se é uma operação de UPDATE ou DELETE
# db_models = result.get("db_models")
# operation = result.get("operation")
# rowcount = result.get("rowcount")
#
# # Atualiza o db_model com refresh em caso de UPDATE
# if operation == "UPDATE" and db_models:
# for db_model in db_models:
# await session.refresh(db_model)
# print("refreshing relaizado")
#
# # Retorna o que a rota espera: o db_model para update ou rowcount para delete
# if operation == "UPDATE":
# if len(db_models) == 1:
# print(db_models[0].__dict__)
# return db_models[0].__dict__ # Retorna o único modelo como dicionário
# else:
# return db_models # Retorna a lista de modelos
# else:
# return rowcount # Retorna rowcount no caso de DELETE
#
# # Caso a auditoria esteja habilitada, continuar com o processo de auditoria
# # Tenta executar a função original e captura o retorno
# result = await func(*args, **kwargs)
#
# # Captura o db_model e a operação realizada
# db_models = result.get("db_models")
# original_models = result.get("original_models")
# operation = result.get("operation")
# rowcount = result.get("rowcount")
#
# # Para operações de UPDATE
# log_entries = []
# timestamp = datetime.now(timezone.utc).replace(tzinfo=None)
# if operation == "UPDATE" and db_models:
# # timestamp = datetime.now(timezone.utc).replace(tzinfo=None)
#
# # Iterar pelos modelos atualizados
# for db_model, original_model in zip(db_models, original_models):
# inspector = inspect(db_model)
# primary_key = str(inspect(db_model).identity[0])
#
# # Registro na tabela HistoricoAlteracoes
# log_entry_update = HistoricoAlteracoes(
# tabela=db_model.__tablename__,
# data_modificacao=timestamp,
# action='UPDATE',
# usuario_id=kwargs.get('user_id'),
# registro_id=primary_key
# )
# session.add(log_entry_update)
#
# # Verificar alterações em cada atributo
# for attr in inspector.attrs:
# old_value = original_model.get(attr.key, None)
# new_value = getattr(db_model, attr.key, None)
#
# # Confirmar se houve mudança nos valores
# if old_value != new_value:
# log_update = HistoricoUpdate(
# coluna=attr.key,
# valor_antigo=str(old_value) if old_value else None,
# valor_novo=str(new_value) if new_value else None,
# alteracao=log_entry_update # Relacionando ao log de alteração principal
# )
# session.add(log_update)
#
# # Verificar se é uma operação de DELETE
# elif operation == "DELETE":
# _timestamp_naive = datetime.now(timezone.utc).replace(tzinfo=None)
#
# if isinstance(db_models, list): # Caso seja uma lista de modelos deletados (delete_many)
# for db_model in db_models:
# primary_key = str(inspect(db_model).identity[0])
#
# # Filtra os campos que não são relationship
#
# def is_relationship(attr_name, model):
# """
# Função que verifica se um atributo é do tipo relacionamento no SQLAlchemy
# """
# # Inspeciona o modelo SQLAlchemy
# mapper = inspect(model.__class__)
#
# # Acessa todas as relationships do modelo
# relationships = mapper.relationships
#
# # Verifica se o atributo atual é uma relationship
# return attr_name in relationships
#
# deletado = {attr: value for attr, value in db_model.__dict__.items()
# if
# not isinstance(value, list) and not attr.startswith('_') and not
# is_relationship(attr, db_model)}
#
# # Registro na tabela HistoricoAlteracoes
# log_entry_delete = HistoricoAlteracoes(
# tabela=db_model.__tablename__,
# data_modificacao=timestamp,
# action='DELETE',
# usuario_id=kwargs.get('user_id'),
# registro_id=primary_key
# )
# session.add(log_entry_delete)
#
# log_delete = HistoricoDelete(
# registro_deletado=str(deletado), # Serializar o registro deletado
# alteracao=log_entry_delete # Relacionando ao log de alteração principal
# )
# session.add(log_delete)
#
# else: # Caso seja apenas um modelo (delete_one)
# primary_key = str(inspect(db_models).identity[0])
#
# def is_relationship(attr_name, model):
# """
# Função que verifica se um atributo é do tipo relacionamento no SQLAlchemy
# """
# # Inspeciona o modelo SQLAlchemy
# mapper = inspect(model.__class__)
#
# # Acessa todas as relationships do modelo
# relationships = mapper.relationships
#
# # Verifica se o atributo atual é uma relationship
# return attr_name in relationships
#
# # Filtra os campos que não são relationship
# deletado = {attr: value for attr, value in db_models.__dict__.items()
# if not isinstance(value, list) and not attr.startswith('_') and not
# is_relationship(attr, db_models)}
#
# # Registro na tabela HistoricoAlteracoes
# log_entry_delete = HistoricoAlteracoes(
# tabela=db_models.__tablename__,
# data_modificacao=timestamp,
# action='DELETE',
# usuario_id=kwargs.get('user_id'),
# registro_id=primary_key
# )
# session.add(log_entry_delete)
#
# log_delete = HistoricoDelete(
# registro_deletado=str(deletado), # Serializar o registro deletado
# alteracao=log_entry_delete # Relacionando ao log de alteração principal
# )
# session.add(log_delete)
#
# # Se houver log_entries a serem salvas (no caso de UPDATE ou DELETE)
# if log_entries:
# session.add_all(log_entries)
#
# # Realizar o commit no final, para ambos UPDATE e DELETE
# await session.commit()
#
# # Se for um update, atualiza o db_model com refresh
# if operation == "UPDATE" and db_models:
# for db_model in db_models:
# await session.refresh(db_model)
#
# # Retorna o que a rota espera: o db_model para update ou rowcount para delete
# if operation == "UPDATE":
# if len(db_models) == 1:
# return db_models[0].__dict__ # Retorna o único modelo como dicionário
# else:
# return db_models # Retorna a lista de modelos
# else:
# return rowcount # Retorna rowcount no caso de DELETE
#
# return wrapper
def audit_log(func):
async def wrapper(*args, **kwargs):
self = args[0] # 'self' é a instância do repositório
session = self.session
# Se auditoria não estiver habilitada, processa sem auditoria
if not getattr(self.model, 'log_auditoria_habilitado', False):
result = await func(*args, **kwargs)
await session.commit()
db_models = result.get("db_models")
operation = result.get("operation")
rowcount = result.get("rowcount")
if operation == "UPDATE" and db_models:
if isinstance(db_models, list):
for db_model in db_models:
await session.refresh(db_model)
print("refreshing realizado")
else:
await session.refresh(db_models)
# Retorna exatamente o que a função original produziu
if operation == "UPDATE":
return db_models
else:
return rowcount
# Auditoria habilitada: chama a função original e captura o retorno
result = await func(*args, **kwargs)
db_models = result.get("db_models")
original_models = result.get("original_models")
operation = result.get("operation")
rowcount = result.get("rowcount")
# Variável de controle: se o retorno for um objeto único, single_update será True.
single_update = False
original_db_model = None
if not isinstance(db_models, list):
single_update = True
original_db_model = db_models # Guarda o objeto único original
db_models = [db_models] # Encapsula para processamento da auditoria
original_models = [original_models]
# Processamento da auditoria para UPDATE
if operation == "UPDATE" and db_models:
timestamp = datetime.now(timezone.utc).replace(tzinfo=None)
for db_model, original_model in zip(db_models, original_models):
inspector = inspect(db_model)
primary_key = str(inspect(db_model).identity[0])
log_entry_update = HistoricoAlteracoes(
tabela=db_model.__tablename__,
data_modificacao=timestamp,
action='UPDATE',
usuario_id=kwargs.get('user_id'),
registro_id=primary_key
)
session.add(log_entry_update)
# Itera pelos atributos mapeados e registra alterações
for attr in inspector.attrs:
old_value = original_model.get(attr.key, None)
new_value = getattr(db_model, attr.key, None)
if old_value != new_value:
log_update = HistoricoUpdate(
coluna=attr.key,
valor_antigo=str(old_value) if old_value is not None else None,
valor_novo=str(new_value) if new_value is not None else None,
alteracao=log_entry_update
)
session.add(log_update)
# Processamento da auditoria para DELETE
elif operation == "DELETE":
_timestamp_naive = datetime.now(timezone.utc).replace(tzinfo=None)
if isinstance(db_models, list): # Caso seja delete_many
for db_model in db_models:
primary_key = str(inspect(db_model).identity[0])
def is_relationship(attr_name, model):
mapper = inspect(model.__class__)
return attr_name in mapper.relationships
deletado = {
attr: value
for attr, value in db_model.__dict__.items()
if
not isinstance(value, list) and not attr.startswith('_') and not is_relationship(attr, db_model)
}
log_entry_delete = HistoricoAlteracoes(
tabela=db_model.__tablename__,
data_modificacao=_timestamp_naive,
action='DELETE',
usuario_id=kwargs.get('user_id'),
registro_id=primary_key
)
session.add(log_entry_delete)
log_delete = HistoricoDelete(
registro_deletado=str(deletado),
alteracao=log_entry_delete
)
session.add(log_delete)
else: # Caso delete_one
primary_key = str(inspect(db_models).identity[0])
def is_relationship(attr_name, model):
mapper = inspect(model.__class__)
return attr_name in mapper.relationships
deletado = {
attr: value
for attr, value in db_models.__dict__.items()
if not isinstance(value, list) and not attr.startswith('_') and not is_relationship(attr, db_models)
}
log_entry_delete = HistoricoAlteracoes(
tabela=db_models.__tablename__,
data_modificacao=_timestamp_naive,
action='DELETE',
usuario_id=kwargs.get('user_id'),
registro_id=primary_key
)
session.add(log_entry_delete)
log_delete = HistoricoDelete(
registro_deletado=str(deletado),
alteracao=log_entry_delete
)
session.add(log_delete)
# Realiza o commit final após registrar a auditoria
await session.commit()
# Para operações de UPDATE, faz refresh dos objetos
if operation == "UPDATE" and db_models:
for db_model in db_models:
await session.refresh(db_model)
# Retorno final: se for update e se for update_by_id (single_update=True),
# retorna o objeto único; caso contrário, retorna a lista, mantendo o contrato original.
if operation == "UPDATE":
return original_db_model if single_update else db_models
else:
return rowcount
return wrapper

View File

@ -0,0 +1,76 @@
def format_itens_equipamentos(data):
"""
Formata os dados dos itens de equipamentos em um formato específico.
Retorna o formato:
[
{
"equipamento_nome": "string",
"tipo_equipamento_nome": "string",
"setor_nome": "string",
"uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"itens": [
{
"itens_equipamentos_ns": "string",
"itens_equipamentos_patrimonio": "string",
"itens_equipamentos_data_compra": "2024-11-15",
"itens_equipamentos_prazo_garantia": "2024-11-15",
"itens_equipamentos_voltagem": "0",
"itens_equipamentos_valor_aquisicao": 1,
"itens_equipamentos_rfid_uid": "stringst",
"uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"itens_equipamentos_manutencao": true/false,
}
]
}
]
"""
formatted_response = []
# Mapeia os dados em uma estrutura agrupada
for item in data:
equipamento = item.relacao_equipamento
tipo_equipamento = equipamento.relacao_tipo_equipamento
setor = tipo_equipamento.relacao_setor
# Procura se já existe uma entrada para o equipamento
equipamento_entry = next(
(entry for entry in formatted_response if entry["equipamento_uuid"] == equipamento.uuid),
None
)
# Cria uma nova entrada caso não exista
if not equipamento_entry:
equipamento_entry = {
"equipamento_nome": equipamento.equipamento_nome,
"equipamento_uuid": equipamento.uuid,
"equipamento_fk_tipo_equipamento": equipamento.fk_tipo_equipamento_uuid,
"tipo_equipamento_nome": tipo_equipamento.tipo_equipamento_nome,
"tipo_equipamento_uuid": tipo_equipamento.uuid,
"tipo_equipamento_fk_setor": tipo_equipamento.fk_setor_uuid,
"setor_nome": setor.setor_nome,
"setor_uuid": setor.uuid,
"itens": []
}
formatted_response.append(equipamento_entry)
# Adiciona o item à lista de itens do equipamento
equipamento_entry["itens"].append({
"itens_equipamentos_ns": item.itens_equipamentos_ns,
"itens_equipamentos_patrimonio": item.itens_equipamentos_patrimonio,
"itens_equipamentos_data_compra": item.itens_equipamentos_data_compra,
"itens_equipamentos_prazo_garantia": item.itens_equipamentos_prazo_garantia,
"itens_equipamentos_voltagem": item.itens_equipamentos_voltagem,
"itens_equipamentos_valor_aquisicao": item.itens_equipamentos_valor_aquisicao,
"itens_equipamentos_rfid_uid": item.itens_equipamentos_rfid_uid,
"itens_equipamentos_manutencao": item.itens_equipamentos_manutencao,
"uuid": item.uuid
})
return formatted_response
# Dicionário de mapeamento para os formatadores
formatters_map = {
"itens_equipamentos": format_itens_equipamentos,
# Adicione outros formatadores aqui conforme necessário
}

655
app/database/models.py Normal file
View File

@ -0,0 +1,655 @@
# Importações de bibliotecas padrão
from __future__ import annotations
import enum
from datetime import datetime
from typing import List, Annotated
from uuid import UUID as UuidType
# from fastapi_users.db import SQLAlchemyBaseUserTable
from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID
# Importações de bibliotecas de terceiros
from sqlalchemy import Column, String, Boolean, ForeignKey, Table, Integer, Date, Text, Numeric, Enum
from sqlalchemy import DateTime, FetchedValue, func
from sqlalchemy.dialects.postgresql import UUID as UuidColumn
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import relationship, Mapped, mapped_column, with_polymorphic
from sqlalchemy.sql.expression import FunctionElement
from sqlalchemy.sql import expression
from uuid6 import uuid7
# from sqlalchemy.ext.asyncio import AsyncAttrs
# Importações do seu próprio projeto
from .session import Base
# EXTENSIONS = ["uuid-ossp", "postgis", "postgis_topology"]
#
# naming_convention = {
# "ix": "ix_ct_%(table_name)s_%(column_0_N_name)s",
# "uq": "uq_ct_%(table_name)s_%(column_0_N_name)s",
# "ck": "ck_ct_%(table_name)s_%(constraint_name)s",
# "fk": "fk_ct_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
# "pk": "pk_ct_%(table_name)s",
# }
#
#
# class Base(DeclarativeBase, AsyncAttrs):
# metadata = MetaData(naming_convention=naming_convention,
# # Multi Tennat
# schema="tenant"
# )
uuid = Annotated[UuidType, mapped_column(primary_key=True)]
str1 = Annotated[str, mapped_column(String(1), nullable=True)]
str2 = Annotated[str, mapped_column(String(2), nullable=True)]
str8 = Annotated[str, mapped_column(String(8), nullable=True)]
str10 = Annotated[str, mapped_column(String(10), nullable=True)]
str10_not_null = Annotated[str, mapped_column(String(10), nullable=False)]
str11 = Annotated[str, mapped_column(String(11), nullable=True)]
str14 = Annotated[str, mapped_column(String(14), nullable=True)]
str20 = Annotated[str, mapped_column(String(20), nullable=True)]
str30_not_null = Annotated[str, mapped_column(String(30), nullable=False)]
str39_uid = Annotated[str, mapped_column(String(39), nullable=True)]
str36_uuid = Annotated[str, mapped_column(String(36), nullable=False)]
str36_uuid_null = Annotated[str, mapped_column(String(36), nullable=True)]
str50 = Annotated[str, mapped_column(String(50), nullable=True)]
str50_null = Annotated[str, mapped_column(String(50), nullable=False)]
str100 = Annotated[str, mapped_column(String(100), nullable=True)]
str150 = Annotated[str, mapped_column(String(150), nullable=True)]
str200 = Annotated[str, mapped_column(String(200), nullable=True)]
url = Annotated[str, mapped_column(String(2083), nullable=True)]
intpk = Annotated[int, mapped_column(primary_key=True, index=True)]
valor_monetario = Annotated[float, mapped_column(Numeric(precision=10, scale=2), nullable=True)]
valor_monetario_not_null = Annotated[float, mapped_column(Numeric(precision=10, scale=2), nullable=False)]
data = Annotated[Date, mapped_column(Date, nullable=True)]
data_null = Annotated[Date, mapped_column(Date, nullable=True)]
data_idx = Annotated[Date, mapped_column(Date, index=True)]
text = Annotated[Text, mapped_column(Text)]
text_null = Annotated[Text, mapped_column(Text, nullable=True)]
boleano = Annotated[Boolean, mapped_column(Boolean, default=True)]
boleano_false = Annotated[Boolean, mapped_column(Boolean, server_default=expression.false())]
# ------------------------------------------------------INICIO MIXIN----------------------------------------------------
class utcnow(FunctionElement):
type = DateTime()
inherit_cache = True
@compiles(utcnow, "postgresql")
def pg_utcnow(element, compiler, **kw):
return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
class UuidMixin:
uuid: Mapped[UuidType] = mapped_column(
"uuid",
UuidColumn(as_uuid=True),
primary_key=True,
default=uuid7,
nullable=False,
)
class IdMixin:
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, nullable=False)
class AtivoMixin:
ativo = Column(Boolean, nullable=False, server_default=expression.true(), index=True)
data_ativacao: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=utcnow(),
)
data_desativacao: Mapped[datetime] = mapped_column(
DateTime,
nullable=True,
index=True,
onupdate=func.now(),
server_default=utcnow(),
server_onupdate=FetchedValue(),
)
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=utcnow(),
)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=True,
index=True,
onupdate=func.now(),
server_default=utcnow(),
server_onupdate=FetchedValue(),
)
# ------------------------------------------------------FIM MIXIN----------------------------------------------------
# ------------------------------------------------------MULTI TENNAT----------------------------------------------------
class Inquilino(UuidMixin, Base):
__tablename__ = "inquilinos"
__table_args__ = ({"schema": "shared"})
nome = Column(String(100), nullable=False, unique=False)
cpf_cnpj = Column(String(14), nullable=False, unique=True)
pessoa_celular = Column(String(20), nullable=True, unique=False)
# Relacionamento com a tabela de usuários
usuario: Mapped[List["RbacUser"]] = relationship(back_populates="inquilino", passive_deletes=True,
lazy="selectin",
join_depth=2)
# ---------------------------------------------------FIM MULTI TENNAT---------------------------------------------------
comercial_relacionamento_pessoa_empresa = Table('comercial_relacionamento_pessoa_empresa',
Base.metadata,
Column('relacao_comercial_uuid', UUID(as_uuid=True),
ForeignKey('comercial_relacoes_comercial.uuid',
ondelete="CASCADE")),
Column('pessoa_uuid', UUID(as_uuid=True),
ForeignKey('comercial_pessoas.uuid', ondelete="CASCADE")),
)
class ComercialTransacaoComercialEnum(enum.Enum):
PAGAMENTO = "PAGAMENTO"
RECEBIMENTO = "RECEBIMENTO"
AMBOS = "AMBOS"
class ComercialRelacaoComercial(Base, TimestampMixin, UuidMixin, AtivoMixin):
__tablename__ = "comercial_relacoes_comercial"
log_auditoria_habilitado = False
descricao_relacao_comercial: str = Column(String(30), nullable=False)
transacao_comercial: Mapped[ComercialTransacaoComercialEnum] = mapped_column(Enum(
ComercialTransacaoComercialEnum,
inherit_schema=True
),
nullable=False)
pessoa_relacao: Mapped[List["ComercialPessoa"]] = relationship(secondary=comercial_relacionamento_pessoa_empresa,
back_populates='rc',
passive_deletes=True,
lazy="selectin",
join_depth=1, )
class ComercialTipoEndereco(Base, TimestampMixin, UuidMixin, AtivoMixin):
__tablename__ = "comercial_tipos_endereco"
log_auditoria_habilitado = False
tipo_endereco_descricao: str = Column(String(30), nullable=False)
relacao_endereco_tp: Mapped[List["ComercialEndereco"]] = relationship(back_populates="relacao_tipo_endereco",
passive_deletes=True,
lazy="selectin",
cascade="all, delete-orphan",
join_depth=1)
class ComercialPessoa(UuidMixin, Base, TimestampMixin, AtivoMixin):
__tablename__ = "comercial_pessoas"
log_auditoria_habilitado = False
pessoa_status: bool = Column(Boolean, unique=False, default=True)
pessoa_telefone: Mapped[str20]
pessoa_celular: Mapped[str20]
pessoa_tipo: Mapped[str1]
pessoa_email: Mapped[str150]
pessoa_local_evento: bool = Column(Boolean, unique=False, default=False)
rc: Mapped[List["ComercialRelacaoComercial"]] = relationship(secondary=comercial_relacionamento_pessoa_empresa,
back_populates='pessoa_relacao',
passive_deletes=True,
lazy="selectin", join_depth=1)
enderecos: Mapped[List["ComercialEndereco"]] = relationship(back_populates="pessoa",
passive_deletes=True,
lazy="selectin",
join_depth=2)
# usuario: Mapped[List["RbacUser"]] = relationship(back_populates="pessoa", passive_deletes=True,
# lazy="selectin",
# join_depth=2)
relacao_conta: Mapped[List["FinanceiroConta"]] = relationship(back_populates="relacao_pessoa",
passive_deletes=True,
lazy="selectin",
cascade="all, delete-orphan",
join_depth=1)
__mapper_args__ = {"polymorphic_identity": "comercial_pessoas",
"polymorphic_on": "pessoa_tipo",
}
class ComercialJuridica(ComercialPessoa):
__tablename__ = "comercial_juridicas"
log_auditoria_habilitado = False
uuid: Mapped[uuid] = mapped_column(ForeignKey("comercial_pessoas.uuid", ondelete="CASCADE"), primary_key=True)
juridica_cnpj: Mapped[str14]
juridica_email_fiscal: Mapped[str100]
juridica_insc_est: Mapped[str50]
juridica_ins_mun: Mapped[str50]
juridica_razao_social: Mapped[str200]
juridica_representante: Mapped[str100]
__mapper_args__ = {
"polymorphic_identity": "0",
"polymorphic_load": "selectin"
}
class ComercialFisica(ComercialPessoa):
__tablename__ = "comercial_fisicas"
log_auditoria_habilitado = False
uuid: Mapped[uuid] = mapped_column(ForeignKey("comercial_pessoas.uuid", ondelete="CASCADE"), primary_key=True)
fisica_cpf: Mapped[str11]
fisica_rg: Mapped[str20]
fisica_genero: Mapped[str2]
fisica_nome: Mapped[str100]
__mapper_args__ = {
"polymorphic_identity": "1",
"polymorphic_load": "selectin"
}
class ComercialEndereco(Base, TimestampMixin, UuidMixin):
__tablename__ = "comercial_enderecos"
log_auditoria_habilitado = False
endereco_pessoa_status: bool = Column(Boolean, unique=False, default=True)
endereco_pessoa_descricao: Mapped[str50]
endereco_pessoa_numero: Mapped[str8]
endereco_pessoa_complemento: Mapped[str50]
endereco_pessoa_cep: Mapped[str8]
fk_pessoa_uuid: Mapped[UuidType] = mapped_column(ForeignKey("comercial_pessoas.uuid",
ondelete="CASCADE"),
nullable=False)
pessoa: Mapped["ComercialPessoa"] = relationship(back_populates="enderecos",
lazy="selectin",
join_depth=1)
fk_tipo_endereco_uuid: Mapped[UuidType] = mapped_column(ForeignKey("comercial_tipos_endereco.uuid",
ondelete="CASCADE"), nullable=False)
relacao_tipo_endereco: Mapped["ComercialTipoEndereco"] = relationship(back_populates="relacao_endereco_tp",
lazy="selectin",
join_depth=2)
# __________________________________________USUÁRIOS E PERMISSÃO DE ACESSO______________________________________________
rbac_papeis_usuario = Table(
'rbac_papeis_usuario', Base.metadata,
Column('user_uuid', UUID(as_uuid=True), ForeignKey('shared.rbac_usuarios.id'), primary_key=True),
Column('papel_uuid', UUID(as_uuid=True), ForeignKey('shared.rbac_papeis.uuid'), primary_key=True),
# Multi Tennat
schema='shared'
)
rbac_papel_permissoes = Table(
'rbac_papel_permissoes', Base.metadata,
Column('papel_uuid', UUID(as_uuid=True), ForeignKey('shared.rbac_papeis.uuid'), primary_key=True),
Column('permissao_id', Integer, ForeignKey('shared.rbac_permissoes.id'), primary_key=True),
# Multi Tennat
schema='shared'
)
class RbacPermissao(Base):
__tablename__ = "rbac_permissoes"
log_auditoria_habilitado = False
id = Column(Integer, primary_key=True, index=True)
nome: Mapped[str50] = mapped_column(nullable=False, unique=True)
papeis: Mapped[List["RbacPapel"]] = relationship(secondary=rbac_papel_permissoes,
back_populates='permissoes',
passive_deletes=True,
lazy="selectin",
join_depth=1, )
# Multi Tennat
__table_args__ = ({"schema": "shared"},)
class RbacPapel(UuidMixin, Base):
__tablename__ = "rbac_papeis"
log_auditoria_habilitado = False
# id = Column(Integer, primary_key=True, index=True, autoincrement=True)
nome: Mapped[str50] = mapped_column(nullable=False, unique=True)
permissoes: Mapped[List["RbacPermissao"]] = relationship(secondary=rbac_papel_permissoes,
back_populates='papeis',
passive_deletes=True,
lazy="selectin",
join_depth=2, )
usuarios: Mapped[List["RbacUser"]] = relationship(secondary=rbac_papeis_usuario,
back_populates='papeis',
passive_deletes=True,
lazy="selectin",
join_depth=1, )
# Multi Tennat
__table_args__ = ({"schema": "shared"},)
class RbacUser(SQLAlchemyBaseUserTableUUID, Base):
__tablename__ = "rbac_usuarios"
log_auditoria_habilitado = False
def __init__(self, **kwargs):
super().__init__(**kwargs)
papeis: Mapped[List[RbacPapel]] = relationship(secondary=rbac_papeis_usuario,
back_populates='usuarios',
passive_deletes=True,
lazy="selectin",
join_depth=1)
# Multi Tennat
fk_inquilino_uuid: Mapped[UuidType] = mapped_column(ForeignKey("shared.inquilinos.uuid", ondelete="CASCADE"),
nullable=False)
inquilino: Mapped["Inquilino"] = relationship(back_populates="usuario",
lazy="selectin",
passive_deletes=True,
join_depth=1)
nome_completo: Mapped[str100]
__table_args__ = ({"schema": "shared"},)
# __________________________________________FINAL USUÁRIOS E PERMISSÃO DE ACESSO________________________________________
# ________________________________________________CONTAS A PAGAR E RECEBER______________________________________________
class FinanceiroTipoContaEnum(enum.Enum):
PAGAR = "PAGAR"
RECEBER = "RECEBER"
class FinanceiroTipoMovimentacaoEnum(enum.Enum):
CREDITO = "CREDITO"
DEBITO = "DEBITO"
class FinanceiroStatus(Base, IdMixin):
__tablename__ = "financeiro_status"
log_auditoria_habilitado = False
status_nome_status: Mapped[str20]
status_descricao: Mapped[str200]
relacao_conta: Mapped[List["FinanceiroConta"]] = relationship(back_populates="relacao_status",
passive_deletes=True,
lazy="selectin", cascade="all, delete-orphan",
join_depth=1)
relacao_parcelas: Mapped[List["FinanceiroParcela"]] = relationship(back_populates="relacao_status",
passive_deletes=True,
lazy="selectin", cascade="all, delete-orphan",
join_depth=1)
class FinanceiroForma_Pagamento(Base, IdMixin):
__tablename__ = "financeiro_formas_pagamento"
log_auditoria_habilitado = False
formas_pagamento_descricao: Mapped[str20]
relacao_pagamentos: Mapped[List["FinanceiroPagamento"]] = relationship(back_populates="relacao_formas_pagamento",
passive_deletes=True, lazy="selectin",
cascade="all, delete-orphan", join_depth=1)
class FinanceiroCategoria(Base, IdMixin):
__tablename__ = "financeiro_categorias"
log_auditoria_habilitado = False
categorias_nome: Mapped[str20]
relacao_conta: Mapped[List["FinanceiroConta"]] = relationship(back_populates="relacao_categorias",
passive_deletes=True,
lazy="selectin", cascade="all, delete-orphan",
join_depth=1)
class FinanceiroCentro_Custo(Base, IdMixin):
__tablename__ = "financeiro_centros_custo"
log_auditoria_habilitado = False
centros_custo_nome: Mapped[str20]
centros_custo_descricao: Mapped[str100]
relacao_conta: Mapped[List["FinanceiroConta"]] = relationship(back_populates="relacao_centros_custo",
passive_deletes=True,
lazy="selectin", cascade="all, delete-orphan",
join_depth=1)
class FinanceiroParcela(Base, IdMixin):
__tablename__ = "financeiro_parcelas"
log_auditoria_habilitado = True
parcelas_numero_parcela: Mapped[int] = mapped_column(Integer, nullable=False)
parcelas_valor_parcela: Mapped[valor_monetario]
parcelas_valor_juros: Mapped[valor_monetario]
parcelas_valor_multa: Mapped[valor_monetario]
parcelas_valor_desconto: Mapped[valor_monetario]
parcelas_data_vencimento: Mapped[data_idx]
fk_contas_id: Mapped[int] = mapped_column(ForeignKey("financeiro_contas.id", ondelete="CASCADE"), nullable=False)
relacao_conta: Mapped["FinanceiroConta"] = relationship(back_populates="relacao_parcelas", lazy="selectin",
join_depth=2)
fk_status_id: Mapped[int] = mapped_column(ForeignKey("financeiro_status.id", ondelete="CASCADE"),
nullable=False)
relacao_status: Mapped["FinanceiroStatus"] = relationship(back_populates="relacao_parcelas", lazy="selectin",
join_depth=2)
relacao_pagamentos: Mapped[List["FinanceiroPagamento"]] = relationship(back_populates="relacao_parcela",
passive_deletes=True,
lazy="selectin",
cascade="all, delete-orphan",
join_depth=1)
class FinanceiroPagamento(Base, IdMixin):
__tablename__ = "financeiro_pagamentos"
log_auditoria_habilitado = True
data_pagamento: Mapped[data_idx]
valor_pago: Mapped[valor_monetario]
observacao: Mapped[text]
fk_parcelas_id: Mapped[int] = mapped_column(ForeignKey("financeiro_parcelas.id", ondelete="CASCADE"),
nullable=False)
relacao_parcela: Mapped["FinanceiroParcela"] = relationship(back_populates="relacao_pagamentos", lazy="selectin",
join_depth=2)
fk_contas_corrente_id: Mapped[int] = mapped_column(ForeignKey("financeiro_contas_corrente.id",
ondelete="CASCADE"), nullable=False)
relacao_contas_corrente: Mapped["FinanceiroConta_Corrente"] = relationship(back_populates="relacao_pagamentos",
lazy="selectin", join_depth=2)
fk_formas_pagamento_id: Mapped[int] = mapped_column(ForeignKey("financeiro_formas_pagamento.id",
ondelete="CASCADE"), nullable=False)
relacao_formas_pagamento: Mapped["FinanceiroForma_Pagamento"] = relationship(back_populates="relacao_pagamentos",
lazy="selectin", join_depth=2)
relacao_movimentacoes_conta: Mapped[List[
"FinanceiroMovimentacao_Conta"]] = relationship(back_populates="relacao_pagamentos",
passive_deletes=True,
lazy="selectin",
cascade="all, delete-orphan",
join_depth=1)
class FinanceiroConta(Base, IdMixin):
__tablename__ = "financeiro_contas"
log_auditoria_habilitado = True
contas_tipo_conta: Mapped[FinanceiroTipoContaEnum] = mapped_column(Enum(FinanceiroTipoContaEnum,
inherit_schema=True
),
nullable=False)
contas_data_emissao: Mapped[data]
contas_data_vencimento: Mapped[data]
contas_valor_total: Mapped[valor_monetario]
contas_valor_juros: Mapped[valor_monetario]
contas_valor_multa: Mapped[valor_monetario]
contas_valor_desconto: Mapped[valor_monetario]
contas_descricao: Mapped[str200]
fk_pessoas_uuid: Mapped[int] = mapped_column(ForeignKey("comercial_pessoas.uuid", ondelete="CASCADE"),
nullable=False)
relacao_pessoa: Mapped["ComercialPessoa"] = relationship(back_populates="relacao_conta", lazy="selectin",
join_depth=2)
fk_status_id: Mapped[int] = mapped_column(ForeignKey("financeiro_status.id", ondelete="CASCADE"),
nullable=False)
relacao_status: Mapped["FinanceiroStatus"] = relationship(back_populates="relacao_conta", lazy="selectin",
join_depth=2)
fk_categorias_id: Mapped[int] = mapped_column(ForeignKey("financeiro_categorias.id", ondelete="CASCADE"),
nullable=False)
relacao_categorias: Mapped["FinanceiroCategoria"] = relationship(back_populates="relacao_conta", lazy="selectin",
join_depth=2)
fk_centros_custo_id: Mapped[int] = mapped_column(ForeignKey("financeiro_centros_custo.id", ondelete="CASCADE"),
nullable=False)
relacao_centros_custo: Mapped["FinanceiroCentro_Custo"] = relationship(back_populates="relacao_conta",
lazy="selectin",
join_depth=2)
relacao_parcelas: Mapped[List["FinanceiroParcela"]] = relationship(back_populates="relacao_conta",
passive_deletes=True,
lazy="selectin", cascade="all, delete-orphan",
join_depth=1)
class FinanceiroConta_Corrente(Base, IdMixin):
__tablename__ = "financeiro_contas_corrente"
log_auditoria_habilitado = True
contas_corrente_nome_conta: Mapped[str50]
contas_corrente_saldo_inicial: Mapped[valor_monetario]
contas_corrente_data_criacao: Mapped[data_idx]
contas_corrente_descricao: Mapped[str100]
relacao_movimentacoes_conta: Mapped[List["FinanceiroMovimentacao_Conta"]] = relationship(
back_populates="relacao_contas_corrente",
passive_deletes=True, lazy="selectin",
cascade="all, delete-orphan", join_depth=1)
relacao_pagamentos: Mapped[List["FinanceiroPagamento"]] = relationship(back_populates="relacao_contas_corrente",
passive_deletes=True, lazy="selectin",
cascade="all, delete-orphan", join_depth=1)
class FinanceiroMovimentacao_Conta(Base, IdMixin):
__tablename__ = "financeiro_movimentacoes_conta"
log_auditoria_habilitado = True
movimentacoes_conta_tipo_movimentacao: Mapped[FinanceiroTipoMovimentacaoEnum] = mapped_column(
Enum(FinanceiroTipoMovimentacaoEnum,
# Multi Tennat
inherit_schema=True
# /Multi Tennat
),
nullable=False,
)
movimentacoes_conta_valor_movimentacao: Mapped[valor_monetario]
movimentacoes_conta_data_movimentacao: Mapped[data_idx]
movimentacoes_conta_descricao: Mapped[str200]
fk_contas_corrente_id: Mapped[int] = mapped_column(ForeignKey("financeiro_contas_corrente.id", ondelete="CASCADE"),
nullable=False)
relacao_contas_corrente: Mapped[
"FinanceiroConta_Corrente"] = relationship(back_populates="relacao_movimentacoes_conta",
lazy="selectin", join_depth=2)
fk_pagamentos_id: Mapped[int] = mapped_column(ForeignKey("financeiro_pagamentos.id", ondelete="CASCADE"),
nullable=False)
relacao_pagamentos: Mapped["FinanceiroPagamento"] = relationship(back_populates="relacao_movimentacoes_conta",
lazy="selectin", join_depth=2)
financeiro_conta_manutencao_equipamentos = Table('financeiro_conta_manutencao_equipamentos', Base.metadata,
Column('manutencao_uuid', UUID(as_uuid=True),
ForeignKey('estoque_manutencoes_equipamentos.uuid'),
primary_key=True),
Column('conta_id', Integer,
ForeignKey('financeiro_contas.id'), primary_key=True)
)
# ______________________________________________FIM CONTAS A PAGAR E RECEBER____________________________________________
# _____________________________________________________TABLEAS DE LOG___________________________________________________
class HistoricoAlteracoes(Base, UuidMixin):
__tablename__ = 'historico_alteracoes'
tabela: Mapped[str100]
data_modificacao: Mapped[datetime] = mapped_column(DateTime, nullable=False)
action: Mapped[str10] # 'update', 'delete'
usuario_id: Mapped[str36_uuid_null] # Assumindo que o "ID" do usuário é uma "string"
registro_id: Mapped[str36_uuid] # "ID" do registro que está sendo alterado
# Relacionamentos
updates: Mapped[List["HistoricoUpdate"]] = relationship(back_populates="alteracao", passive_deletes=True,
lazy="selectin", cascade="all, delete-orphan",
join_depth=1)
deletes: Mapped["HistoricoDelete"] = relationship(back_populates="alteracao", passive_deletes=True,
lazy="selectin", cascade="all, delete-orphan",
join_depth=1)
class HistoricoUpdate(Base, UuidMixin):
__tablename__ = 'historico_update'
fk_historico_alteracoes_uuid: Mapped[UuidType] = mapped_column(ForeignKey("historico_alteracoes.uuid",
ondelete="CASCADE"), nullable=False)
coluna: Mapped[str100]
valor_antigo: Mapped[str200]
valor_novo: Mapped[str200]
# Relacionamento com a tabela principal
alteracao: Mapped["HistoricoAlteracoes"] = relationship(back_populates="updates", lazy="selectin", join_depth=2)
class HistoricoDelete(Base, UuidMixin):
__tablename__ = 'historico_delete'
fk_historico_alteracoes_uuid: Mapped[UuidType] = mapped_column(ForeignKey("historico_alteracoes.uuid",
ondelete="CASCADE"), nullable=False)
registro_deletado: Mapped[text] # Aqui armazenamos o estado completo da linha deletada como JSON
# Relacionamento com a tabela principal
alteracao: Mapped["HistoricoAlteracoes"] = relationship(back_populates="deletes", lazy="selectin", join_depth=2)
# ___________________________________________________FIM TABLEAS DE LOG_________________________________________________
# ______________________________________________LOCALIZAÇÃO ARQUIVOS S3_________________________________________________
class S3Arquivo(Base, UuidMixin, TimestampMixin):
__tablename__ = "s3_arquivos"
arquivos_nome_original: Mapped[str200]
arquivos_nome_armazenado: Mapped[str200]
associacoes: Mapped["S3ArquivoAssociacao"] = relationship(back_populates="arquivos", passive_deletes=True,
lazy="selectin", cascade="all, delete-orphan",
join_depth=1)
class S3ArquivoAssociacao(Base, UuidMixin, TimestampMixin):
__tablename__ = "s3_arquivo_associacoes"
fk_arquivo_uuid: Mapped[UuidType] = mapped_column(ForeignKey("s3_arquivos.uuid",
ondelete="CASCADE"), nullable=False)
arquivo_associacoes_tabela_relacionada: Mapped[str100] # Nome da tabela associada
arquivo_associacoes_linha_uuid: Mapped[UuidType] # Uuid da linha na tabela associada
arquivos: Mapped["S3Arquivo"] = relationship(back_populates="associacoes", lazy="selectin", join_depth=2)
# ____________________________________________FIM LOCALIZAÇÃO ARQUIVOS S3_______________________________________________
# __________________________________________________CONFIGURAÇÃO HERANÇA________________________________________________
# ____________________________________________NECESSÁRIO FICA NO FIM DO CÓDIGO__________________________________________
# Definindo a consulta polimórfica
PESSOA_POLY = with_polymorphic(
ComercialPessoa, # Modelo base
[ComercialFisica, ComercialJuridica] # Subclasses
)
# ________________________________________________FIM CONFIGURAÇÃO HERANÇA______________________________________________

143
app/database/session.py Normal file
View File

@ -0,0 +1,143 @@
# Importações de bibliotecas padrão
import contextlib
from typing import AsyncIterator, Optional
from fastapi import Depends
# Importações de bibliotecas de terceiros
from sqlalchemy import MetaData
from sqlalchemy.ext.asyncio import (
AsyncConnection,
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
AsyncAttrs,
)
from sqlalchemy.orm import DeclarativeBase
from app.config import ECHO
import traceback
EXTENSIONS = ["uuid-ossp", "postgis", "postgis_topology"]
naming_convention = {
"ix": "ix_ct_%(table_name)s_%(column_0_N_name)s",
"uq": "uq_ct_%(table_name)s_%(column_0_N_name)s",
"ck": "ck_ct_%(table_name)s_%(constraint_name)s",
"fk": "fk_ct_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_ct_%(table_name)s",
}
class Base(DeclarativeBase, AsyncAttrs):
metadata = MetaData(naming_convention=naming_convention)
class DatabaseSessionManager:
def __init__(self):
self._engine: AsyncEngine | None = None
self._sessionmaker: async_sessionmaker | None = None
def init(self, host: str):
self._engine = create_async_engine(host, echo=ECHO)
self._sessionmaker = async_sessionmaker(autocommit=False, bind=self._engine)
async def close(self):
if self._engine is None:
raise Exception("DatabaseSessionManager is not initialized")
await self._engine.dispose()
self._engine = None
self._sessionmaker = None
def is_initialized(self) -> bool:
"""Verifica se o engine foi inicializado"""
return self._engine is not None
@contextlib.asynccontextmanager
async def connect(self) -> AsyncIterator[AsyncConnection]:
if self._engine is None:
raise Exception("DatabaseSessionManager is not initialized")
async with self._engine.begin() as connection:
try:
yield connection
except Exception:
await connection.rollback()
raise
@contextlib.asynccontextmanager
async def session(self) -> AsyncIterator[AsyncSession]:
if self._sessionmaker is None:
raise Exception("DatabaseSessionManager is not initialized")
session = self._sessionmaker()
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()
@contextlib.asynccontextmanager
async def session_with_tenant(self, tenant_schema: Optional[str]) -> AsyncIterator[AsyncSession]:
if self._engine is None:
raise Exception("DatabaseSessionManager is not initialized")
# Verifica e configura o schema_translate_map
if tenant_schema:
schema_engine = self._engine.execution_options(
schema_translate_map={None: str(tenant_schema)}
)
else:
schema_engine = self._engine.execution_options(
schema_translate_map=None
)
# Cria a sessão vinculada ao schema_engine
try:
session = self._sessionmaker(bind=schema_engine)
except Exception as e:
print(f"Erro ao configurar o bind da sessão: {e}")
raise
try:
yield session
except Exception as e:
await session.rollback()
print(f"Erro encontrado: {e}. Traceback: {traceback.format_exc()}")
raise
finally:
await session.close()
# print("Sessão encerrada")
async def create_all(self, connection: AsyncConnection):
await connection.run_sync(Base.metadata.create_all)
async def drop_all(self, connection: AsyncConnection):
await connection.run_sync(Base.metadata.drop_all)
# Multi Tenant
def get_engine(self):
if not self.is_initialized():
raise Exception("DatabaseSessionManager is not initialized")
return self._engine
def get_sessionmaker(self):
if self._sessionmaker is None:
raise Exception("DatabaseSessionManager is not initialized")
return self._sessionmaker
# Multi Tenant
sessionmanager = DatabaseSessionManager()
async def get_db():
async with sessionmanager.session() as session:
yield session

110
app/main.py Normal file
View File

@ -0,0 +1,110 @@
import asyncio
from fastapi import FastAPI, Request
from contextlib import asynccontextmanager
from fastapi.exceptions import ResponseValidationError, RequestValidationError
from pydantic import ValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.responses import JSONResponse
from app.database.session import sessionmanager
from app.routers import rotas
# Importação das Rotas
from app.config import URL_BD
from app.routers.router_registry import RouterRegistry
from fastapi.middleware.cors import CORSMiddleware
# from starlette.middleware.cors import CORSMiddleware
def init_app(init_db=True):
lifespan = None
if init_db:
sessionmanager.init(URL_BD)
@asynccontextmanager
async def lifespan(app_init: FastAPI):
yield
if sessionmanager.is_initialized(): # Usa o método público para checar o engine
await sessionmanager.close()
server = FastAPI(title="Sonora Tecnologia Admin", lifespan=lifespan)
server.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:4200",
"http://srv-captain--sistema/",
"https://srv-captain--sistema/",
"https://app.sonoraav.com.br",
"http://app.sonoraav.com.br",
], # Domínio do seu frontend
allow_credentials=True,
allow_methods=["*"], # Permitir todos os métodos (ou especifique "POST" se necessário)
allow_headers=["*"],
)
# Registro dinâmico das rotas
router_registry = RouterRegistry(server, rotas.routers)
router_registry.register_routers()
return server
# Definindo o objeto "app" para estar disponível em todos os contextos
app = init_app()
@app.exception_handler(ResponseValidationError)
async def response_validation_exception_handler(request: Request, exc: ResponseValidationError):
return JSONResponse(
status_code=500,
content={"message": "Erro ao validar a resposta do servidor."},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={"detail": exc.errors()},
)
@app.exception_handler(StarletteHTTPException)
async def starlette_http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"detail": "API Administrativa Eventos"},
)
@app.exception_handler(ValidationError)
async def pydantic_validation_exception_handler(request: Request, exc: ValidationError):
return JSONResponse(
status_code=422,
content={"detail": exc.errors()},
)
@app.exception_handler(asyncio.TimeoutError)
async def timeout_error_handler(request: Request, exc: asyncio.TimeoutError):
return JSONResponse(
status_code=504,
content={"detail": "A operação excedeu o tempo limite."},
)
if __name__ == "__main__":
# Código de inicialização que só roda se o arquivo for executado diretamente
app = init_app()
# Exemplo de inicialização do servidor (se necessário)
# import uvicorn
# uvicorn.run("app", host="0.0.0.0", port=8000)

View File

View File

@ -0,0 +1,117 @@
from alembic.runtime.migration import MigrationContext
from alembic.config import Config
from alembic.script import ScriptDirectory
from sqlalchemy.ext.asyncio import AsyncEngine
from sqlalchemy import select, insert
from app.database.models import Inquilino, RbacUser, RbacPapel, rbac_papeis_usuario
from app.database.session import sessionmanager
from app.rbac.auth import get_user_db, get_user_manager
from app.rbac.schemas import UserCreate
from fastapi_users.exceptions import UserAlreadyExists
async def check_migrations(engine: AsyncEngine):
"""
Verifica se todas as migrações foram aplicadas.
"""
alembic_config = Config("alembic.ini")
script = ScriptDirectory.from_config(alembic_config)
async with engine.begin() as conn:
def sync_check_migrations(sync_conn):
context = MigrationContext.configure(sync_conn)
current_revision = context.get_current_revision()
latest_revision = script.get_current_head()
if current_revision != latest_revision:
raise RuntimeError(
f"Database is not up-to-date. Current: {current_revision}, Latest: {latest_revision}. "
"Execute migrations before adding new tenants."
)
await conn.run_sync(sync_check_migrations)
async def create_user(session, fk_inquilino_uuid, email, password, is_superuser=False):
"""
Cria um usuário no sistema utilizando o gerenciador de usuários do FastAPI Users.
"""
try:
user_db = await get_user_db(session).__anext__()
user_manager = await get_user_manager(user_db).__anext__()
try:
user = await user_manager.create(
UserCreate(
email=email,
password=password,
is_superuser=is_superuser,
is_active=True,
fk_inquilino_uuid=fk_inquilino_uuid,
)
)
return user.id
except UserAlreadyExists:
result_user = await session.execute(select(RbacUser).filter_by(email=email))
existing_user = result_user.scalars().first()
raise RuntimeError(f"Usuário '{email}' já existe, seu ID é {existing_user.id}")
except Exception as e:
raise RuntimeError(f"Erro ao criar usuário '{email}': {e}")
async def tenant_create(nome: str, email: str, password: str, cpf_cnpj: str):
"""
Cria um novo tenant (inquilino) no sistema, configura o schema específico
e registra um usuário inicial relacionado ao inquilino.
"""
async with sessionmanager.session() as db:
try:
# Verificar se o tenant já existe
result = await db.execute(select(Inquilino).filter_by(cpf_cnpj=cpf_cnpj))
existing_tenant = result.scalars().first()
if existing_tenant:
raise RuntimeError(
f"Tenant com CPF/CNPJ '{cpf_cnpj}' já existe. Nome: {existing_tenant.nome}, "
f"UUID: {existing_tenant.uuid}"
)
# Criar o registro do tenant
tenant = Inquilino(nome=nome, cpf_cnpj=cpf_cnpj)
db.add(tenant)
await db.commit()
await db.refresh(tenant)
# Criar o usuário inicial
user_id = await create_user(
session=db,
fk_inquilino_uuid=tenant.uuid,
email=email,
password=password,
is_superuser=True,
)
# Nova sessão para associar o papel ao usuário
async with sessionmanager.session() as new_db:
# Buscar o papel "Super Administrador"
result_papel = await new_db.execute(select(RbacPapel).filter_by(nome="Super Administrador"))
papel = result_papel.scalars().first()
if not papel:
raise RuntimeError("Papel 'Super Administrador' não encontrado.")
# Relacionar o papel ao usuário
await new_db.execute(
insert(rbac_papeis_usuario).values(
user_uuid=user_id,
papel_uuid=papel.uuid,
)
)
await new_db.commit()
return tenant.uuid
except RuntimeError as e:
raise RuntimeError(f"Erro inesperado durante a criação do cliente: '{nome}': {e}")
except Exception as e:
raise RuntimeError(f"Erro inesperado durante a criação do cliente: '{nome}': {e}")

View File

@ -0,0 +1,47 @@
from contextlib import contextmanager
from typing import Optional
from app.database.session import sessionmanager
from sqlalchemy import MetaData
from app.database.models import Base
@contextmanager
def with_db(tenant_schema: Optional[str] = None):
"""
Gerencia uma sessão do banco de dados com suporte a schema_translate_map.
Args:
tenant_schema (Optional[str]): Nome do schema do tenant (opcional).
Yields:
AsyncSession: Sessão configurada para o schema especificado.
"""
if not sessionmanager.is_initialized():
raise Exception("DatabaseSessionManager is not initialized")
# Configura o schema_translate_map para o inquilino
if tenant_schema:
schema_translate_map = {None: tenant_schema}
else:
schema_translate_map = None
# Configura a conexão com o schema correto
connectable = sessionmanager.get_engine().execution_options(schema_translate_map=schema_translate_map)
# Cria uma sessão vinculada ao connectable configurado
session = sessionmanager.get_sessionmaker(bind=connectable)
try:
yield session
except Exception:
session.rollback()
raise
finally:
session.close()
def get_tenant_specific_metadata():
meta = MetaData(schema=None)
for table in Base.metadata.tables.values():
if table.schema is None:
table.to_metadata(meta)
return meta

View File

@ -0,0 +1,47 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import sessionmanager
from app.rbac.auth import current_active_user
from fastapi_users import models
from fastapi import Depends, HTTPException, status
from typing import AsyncIterator
async def get_tenant_schema(
user: models.UP = Depends(current_active_user), # Valida o token e obtém o usuário
) -> models.UP:
"""
Configura o schema do tenant no banco de dados e retorna apenas o usuário autenticado.
"""
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Usuário não autenticado.",
)
# Obtém o UUID do inquilino (assumindo que está no atributo fk_inquilino_uuid)
tenant_uuid = user.fk_inquilino_uuid
if not tenant_uuid:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inquilino não associado ao usuário.",
)
# Configura o schema do tenant usando `session_with_tenant`
async with sessionmanager.session_with_tenant(tenant_uuid) as session:
yield session
# Retorna o usuário autenticado para o verify_permissions
# return user
async def get_tenant_session(
tenant_schema: str = Depends(get_tenant_schema)
) -> AsyncIterator[AsyncSession]:
"""
Obtém uma sessão configurada para o schema do tenant.
"""
async with sessionmanager.session_with_tenant(tenant_schema) as session:
yield session

33
app/rbac/README.txt Normal file
View File

@ -0,0 +1,33 @@
Instruções para integração do módulo RBAC:
1. Copie a pasta 'rbac' para o diretório raiz do seu projeto.
2. Instale a dependencias
- pip install fastapi-users[sqlalchemy]
3. Adicione o modelo 'User' ao seu projeto:
- Copie o conteúdo do arquivo 'user_model_snippet.txt' para o seu arquivo de modelos.
4. Inclua as rotas protegidas no seu 'main.py':
- Importe e inclua as rotas:
from rbac.routes import router as rbac_router
app.include_router(rbac_router)
5. Configure as dependências e sessões do banco de dados conforme necessário.
6. Exemplo de como adicionar a verificação de permissão nas rotas:
```python
from fastapi import APIRouter, Depends
from rbac.permissions import verify_permissions
router = APIRouter()
@router.get("/admin-only")
async def admin_only_route(user: User = Depends(verify_permissions([1]))):
return {"message": "This is an admin-only route"}
7. Exemplo importação das rotas
from rbac.routes import router as rbac_router
app.include_router(rbac_router, prefix="/auth", tags=["auth"])

0
app/rbac/__init__.py Normal file
View File

46
app/rbac/auth.py Normal file
View File

@ -0,0 +1,46 @@
from fastapi import Depends
from fastapi_users import FastAPIUsers, UUIDIDMixin, BaseUserManager
from fastapi_users.authentication import BearerTransport, AuthenticationBackend, JWTStrategy
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import session
from app.database.models import RbacUser
import uuid
from app.config import SECRET
bearer_transport = BearerTransport(tokenUrl="autenticacao/login")
def get_jwt_strategy() -> JWTStrategy:
return JWTStrategy(
secret="SECRET_KEY",
lifetime_seconds=3600,
)
auth_backend = AuthenticationBackend(
name="jwt",
transport=bearer_transport,
get_strategy=get_jwt_strategy,
)
async def get_user_db(session_get_user_db: AsyncSession = Depends(session.get_db)):
yield SQLAlchemyUserDatabase(session_get_user_db, RbacUser)
# class UserManager(UUIDIDMixin, BaseUserManager[RbacUser, uuid.UUID]):
class UserManager(UUIDIDMixin, BaseUserManager[RbacUser, uuid.UUID]):
reset_password_token_secret = SECRET
verification_token_secret = SECRET
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
yield UserManager(user_db)
fastapi_users = FastAPIUsers[RbacUser, uuid.UUID](get_user_manager, [auth_backend])
current_active_user = fastapi_users.current_user(active=True)

View File

@ -0,0 +1,43 @@
import jwt
from fastapi_users.authentication import JWTStrategy
from fastapi_users.jwt import generate_jwt, decode_jwt
from fastapi_users import exceptions
from app.config import SECRET
class CustomJWTStrategy(JWTStrategy):
async def write_token(self, user) -> str:
# Coletar todas as permissões do usuário a partir de seus papéis
todas_as_permissoes = set() # Usamos um set para evitar duplicatas
if hasattr(user, 'papeis'):
for papel in user.papeis:
if hasattr(papel, 'permissoes'):
for permissao in papel.permissoes:
todas_as_permissoes.add(permissao.id) # Usar o ID da permissão
# Transformar o set em uma lista para o payload do token
lista_de_permissoes = list(todas_as_permissoes)
print("user id")
print(user.id)
# Aqui, adicionamos as claims personalizadas ao payload
data = {
"sub": str(user.id),
"permissions": lista_de_permissoes, # Acessa diretamente a lista de IDs de permissões coletadas
"aud": self.token_audience, # Audiência, conforme o padrão
}
token = generate_jwt(
data, self.encode_key, self.lifetime_seconds, algorithm=self.algorithm
)
return token
async def read_token(self, token: str, user_manager):
# Decodifica o token JWT usando a função padrão decode_jwt do fastapi_users
try:
payload = decode_jwt(token, SECRET, audience=self.token_audience)
return payload
except Exception as e:
raise ValueError(f"Token inválido: {str(e)}")

View File

@ -0,0 +1,103 @@
# import jwt
# from fastapi_users.jwt import decode_jwt, generate_jwt, SecretType
# from fastapi_users.manager import BaseUserManager
# from fastapi_users.authentication.strategy import JWTStrategy
# from fastapi_users import models, exceptions
# from typing import Optional, List
#
#
# class CustomJWTStrategy(JWTStrategy[models.UP, models.ID]):
# def __init__(
# self,
# secret: SecretType,
# lifetime_seconds: Optional[int],
# token_audience: List[str] = ["fastapi-users:auth"],
# algorithm: str = "HS256",
# public_key: Optional[SecretType] = None
# ):
# super().__init__(secret, lifetime_seconds, token_audience, algorithm, public_key)
#
# async def write_token(self, user: models.UP) -> str:
# todas_as_permissoes = set()
# if hasattr(user, 'papeis'):
# for papel in user.papeis:
# if hasattr(papel, 'permissoes'):
# todas_as_permissoes.update(permissao.id for permissao in papel.permissoes)
#
# data = {
# "sub": str(user.id),
# "permissions": list(todas_as_permissoes),
# "aud": self.token_audience,
# }
# return generate_jwt(data, self.encode_key, self.lifetime_seconds, algorithm=self.algorithm)
#
# async def read_token(
# self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID]
# ) -> Optional[models.UP]:
# if token is None:
# return None
#
# try:
# data = decode_jwt(
# token, self.decode_key, self.token_audience, algorithms=[self.algorithm]
# )
# user_id = data.get("sub")
# if user_id is None:
# return None
#
# permissions = data.get("permissions", [])
#
# except jwt.PyJWTError:
# return None
#
# try:
# parsed_id = user_manager.parse_id(user_id)
# user = await user_manager.get(parsed_id)
# if user:
# user.permissions = permissions
# return user
# except (exceptions.UserNotExists, exceptions.InvalidID):
# return None
import jwt
from fastapi_users.authentication import JWTStrategy
from fastapi_users.jwt import generate_jwt, decode_jwt
from fastapi_users import exceptions
from app.config import SECRET
class CustomJWTStrategy(JWTStrategy):
async def write_token(self, user) -> str:
# Coletar todas as permissões do usuário a partir de seus papéis
todas_as_permissoes = set() # Usamos um set para evitar duplicatas
if hasattr(user, 'papeis'):
for papel in user.papeis:
if hasattr(papel, 'permissoes'):
for permissao in papel.permissoes:
todas_as_permissoes.add(permissao.id) # Usar o ID da permissão
# Transformar o set em uma lista para o payload do token
lista_de_permissoes = list(todas_as_permissoes)
print("user id")
print(user.id)
# Aqui, adicionamos as claims personalizadas ao payload
data = {
"sub": str(user.id),
"permissions": lista_de_permissoes, # Acessa diretamente a lista de IDs de permissões coletadas
"aud": self.token_audience, # Audiência, conforme o padrão
}
token = generate_jwt(
data, self.encode_key, self.lifetime_seconds, algorithm=self.algorithm
)
return token
async def read_token(self, token: str, user_manager):
# Decodifica o token JWT usando a função padrão decode_jwt do fastapi_users
try:
payload = decode_jwt(token, SECRET, audience=self.token_audience)
return payload
except Exception as e:
raise ValueError(f"Token inválido: {str(e)}")

60
app/rbac/modelos.txt Normal file
View File

@ -0,0 +1,60 @@
from sqlalchemy import Column, String, Table, ForeignKey, Integer, Boolean
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship, declarative_base, Mapped, mapped_column
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyBaseUserTable
from typing import List
Base = declarative_base()
class Permissao(Base):
__tablename__ = "permissoes"
id = Column(Integer, primary_key=True, index=True)
nome: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
papeis: Mapped[List["Papel"]] = relationship(secondary='papel_permissoes',
back_populates='permissoes',
passive_deletes=True,
lazy="selectin",
join_depth=1)
class Papel(Base):
__tablename__ = "papeis"
id = Column(UUID(as_uuid=True), primary_key=True, index=True)
nome: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
permissoes: Mapped[List[Permissao]] = relationship(secondary='papel_permissoes',
back_populates='papeis',
passive_deletes=True,
lazy="selectin",
join_depth=1)
usuarios: Mapped[List["User"]] = relationship(secondary='papeis_usuario',
back_populates='papeis',
passive_deletes=True,
lazy="selectin",
join_depth=1)
class User(SQLAlchemyBaseUserTable, Base):
__tablename__ = "user"
id = Column(UUID(as_uuid=True), primary_key=True, index=True)
username = Column(String, nullable=False, unique=True)
papeis: Mapped[List[Papel]] = relationship(secondary='papeis_usuario',
back_populates='usuarios',
passive_deletes=True,
lazy="selectin",
join_depth=1)
is_active = Column(Boolean, default=True, nullable=False)
is_superuser = Column(Boolean, default=False, nullable=False)
papeis_usuario = Table(
'papeis_usuario', Base.metadata,
Column('user_uuid', UUID(as_uuid=True), ForeignKey('user.id'), primary_key=True),
Column('papel_uuid', UUID(as_uuid=True), ForeignKey('papeis.id'), primary_key=True)
)
papel_permissoes = Table(
'papel_permissoes', Base.metadata,
Column('papel_uuid', UUID(as_uuid=True), ForeignKey('papeis.id'), primary_key=True),
Column('permissao_id', Integer, ForeignKey('permissoes.id'), primary_key=True)
)

43
app/rbac/permissions.py Normal file
View File

@ -0,0 +1,43 @@
from typing import List
from fastapi import Depends, HTTPException, status
from fastapi_users import models
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.rbac.auth import current_active_user
from fastapi import Depends, HTTPException, status
from typing import List
from fastapi_users import models
from app.multi_tenant.tenant_utils import get_tenant_schema
def verify_permissions(required_permissions: List[int]):
async def permission_dependency(
user: models.UP = Depends(current_active_user),
):
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Usuário não autenticado.",
)
# Coleta todas as permissões do usuário
user_permissions = [perm.id for papel in user.papeis for perm in papel.permissoes]
# Se não houver permissões específicas necessárias, qualquer usuário autenticado tem acesso
if not required_permissions:
return user
# Verifica se o usuário possui pelo menos uma das permissões necessárias
if not any(perm in user_permissions for perm in required_permissions):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Você não tem as permissões necessárias para acessar este recurso.",
)
return user # Retorna o objeto `user` para acesso em outras funções
return permission_dependency

24
app/rbac/rbac.py Normal file
View File

@ -0,0 +1,24 @@
# from models import User, Permissao, Papel
from app.database.models import RbacUser, RbacPermissao, RbacPapel, rbac_papel_permissoes, rbac_papeis_usuario
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
class RBAC:
@staticmethod
async def has_permission(user: RbacUser, permission_id: int, session: AsyncSession) -> bool:
# Carregar permissões associadas aos papéis do usuário
result = await session.execute(
select(RbacPermissao)
.join(rbac_papel_permissoes)
.join(RbacPapel)
.join(rbac_papeis_usuario)
.where(rbac_papeis_usuario.c.user_uuid == user.id)
.where(rbac_papel_permissoes.c.permissao_id == permission_id)
)
permissoes = result.scalars().all()
# Verificar se a permissão está presente
return len(permissoes) > 0

63
app/rbac/routes_login.py Normal file
View File

@ -0,0 +1,63 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi_users.router import ErrorCode
from sqlalchemy.ext.asyncio import AsyncSession
from .auth import fastapi_users, auth_backend, get_user_manager
from app.database.models import RbacPapel
from app.rbac.schemas import UserRead, UserCreate, UserRoles
from fastapi_users.exceptions import UserAlreadyExists, InvalidPasswordException
from app.database.session import get_db
router = APIRouter(
prefix="/autenticacao",
tags=["Autenticação"], )
# Rotas de autenticação
@router.post("/register", response_model=UserRead)
async def register(user: UserCreate, roles: UserRoles, session: AsyncSession = Depends(get_db),
user_manager=Depends(get_user_manager)):
try:
created_user = await user_manager.create(user)
# Associação dos papéis ao usuário criado
for papel_id in roles.papeis:
papel = await session.get(RbacPapel, papel_id)
if papel:
created_user.papeis.append(papel)
else:
raise HTTPException(status_code=404, detail=f"Papel com ID {papel_id} não encontrado")
await session.commit()
await session.refresh(created_user)
return created_user
except UserAlreadyExists:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS,
)
except InvalidPasswordException as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"code": ErrorCode.REGISTER_INVALID_PASSWORD,
"reason": e.reason,
},
)
router.include_router(
fastapi_users.get_auth_router(auth_backend)
)
router.include_router(
fastapi_users.get_reset_password_router(),
)
router.include_router(
fastapi_users.get_verify_router(UserRead),
)

View File

@ -0,0 +1,15 @@
from fastapi import APIRouter
from .auth import fastapi_users
from app.rbac.schemas import UserRead, UserUpdate
router = APIRouter(
)
# Rotas de autenticação
router.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/usuarios",
tags=["Usuários"],
)

44
app/rbac/schemas.py Normal file
View File

@ -0,0 +1,44 @@
from pydantic import BaseModel, Field
from typing import Optional
from uuid import UUID
from fastapi_users import schemas
import uuid
from app.schemas.pessoa_schemas import PessoaBaseResponse
class PermissionRead(BaseModel):
id: int = Field(..., description="Unique identifier for the permission")
nome: str = Field(..., max_length=50, description="Name of the permission")
class Config:
from_attributes = True
class RoleRead(BaseModel):
uuid: UUID = Field(..., description="Unique identifier for the role")
nome: str = Field(..., max_length=50, description="Name of the role")
permissoes: list[PermissionRead] = Field(..., description="List of permissions associated with the role")
class Config:
from_attributes = True
class UserRead(schemas.BaseUser[uuid.UUID]):
nome_completo: str = Field(min_length=3, max_length=100)
fk_inquilino_uuid: UUID
papeis: list[RoleRead]
# pessoa: PessoaBaseResponse
class UserCreate(schemas.BaseUserCreate):
fk_inquilino_uuid: UUID
nome: str = Field(min_length=3, max_length=100)
class UserUpdate(schemas.BaseUserUpdate):
pass
class UserRoles(BaseModel):
papeis: list[UUID]

0
app/routers/__init__.py Normal file
View File

View File

@ -0,0 +1,56 @@
# Importações de bibliotecas padrão
from collections.abc import Callable
from typing import Type, TypeVar
# Importações de bibliotecas de terceiros
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
# Importações do seu próprio projeto
from app.database import models, RelationalTableRepository
from app.multi_tenant.tenant_utils import get_tenant_schema
# def get_repository_simple_table(
# model: type[models.Base],
# ) -> Callable[[AsyncSession], RepositoryBase.RepositoryBase]:
# def func(session_simple_table: AsyncSession = Depends(get_tenant_schema)): # Sem parênteses
# return RepositoryBase.RepositoryBase(model, session_simple_table)
#
# return func
def get_repository_relational_table(
model: type[models.Base],
) -> Callable[[AsyncSession], RelationalTableRepository.RelationalTableRepository]:
def func(session_relational_table: AsyncSession = Depends(get_tenant_schema)):
return RelationalTableRepository.RelationalTableRepository(model, session_relational_table)
return func
# def get_repository_s3(
# model: type[models.Base],
# ) -> Callable[[AsyncSession], RepositoryS3.RepositoryS3]:
# def func(session_relational_table: AsyncSession = Depends(get_tenant_schema)):
# return RepositoryS3.RepositoryS3(model, session_relational_table)
#
# return func
# Tipo genérico para repositórios
T = TypeVar("T")
def get_repository(
model: Type[models.Base],
repository_class: Type[T]
) -> Callable[[AsyncSession], T]:
"""
Função genérica para criar dependências de repositórios.
"""
def func(session: AsyncSession = Depends(get_tenant_schema)) -> T:
return repository_class(model, session)
return func

87
app/routers/rotas.py Normal file
View File

@ -0,0 +1,87 @@
from app.database import models
from app.routers.rotas_dinamicas import create_dynamic_router
from app.routers.router_pessoa import router as pessoa
from app.s3.router_s3 import router as s3
from app import schemas
from app.rbac.routes_login import router as fastapi_user
from app.rbac.routes_usuario_logado import router as fastapi_logado
# Instanciando o DynamicRouter com o repositório injetado diretamente nas funções
tipo_endereco = create_dynamic_router(
rota_prefix="/api/tipo_endereco",
rota_tags=["Tipo Endereço"],
db_model=models.ComercialTipoEndereco,
create=schemas.tipo_endereco_schemas.Create,
request=schemas.tipo_endereco_schemas.Request,
request_formatado=schemas.tipo_endereco_schemas.Request,
id_many=schemas.tipo_endereco_schemas.IdsRequest,
id_one=schemas.tipo_endereco_schemas.IdRequest,
update=schemas.tipo_endereco_schemas.UpdateRequest,
updates=schemas.tipo_endereco_schemas.UpdateManyRequest,
ativo=schemas.utils.RegistroAtivo,
permissao_total=1,
permissao_setor=2,
permissao_endpoint=31,
permissao_especifica_inicial=201,
ordenacao="tipo_endereco_descricao"
)
endereco = create_dynamic_router(
rota_prefix="/api/endereco",
rota_tags=["Endereço"],
db_model=models.ComercialEndereco,
create=schemas.endereco_schemas.Create,
request=schemas.endereco_schemas.Request,
request_formatado=schemas.endereco_schemas.Request,
id_many=schemas.endereco_schemas.IdsRequest,
id_one=schemas.endereco_schemas.IdRequest,
update=schemas.endereco_schemas.UpdateRequest,
updates=schemas.endereco_schemas.UpdateManyRequest,
ativo=schemas.utils.RegistroAtivo,
permissao_total=1,
permissao_setor=2,
permissao_endpoint=32,
permissao_especifica_inicial=301
)
papel = create_dynamic_router(
rota_prefix="/api/papeis",
rota_tags=["Papeis"],
db_model=models.RbacPapel,
create=schemas.papel_shemas.Create,
request=schemas.papel_shemas.Request,
request_formatado=schemas.papel_shemas.Request,
id_many=schemas.papel_shemas.IdsRequest,
id_one=schemas.papel_shemas.IdRequest,
update=schemas.papel_shemas.UpdateRequest,
updates=schemas.papel_shemas.UpdateManyRequest,
permissao_total=1,
permissao_setor=3,
permissao_endpoint=41,
permissao_especifica_inicial=1201
)
# Rotas não dinâmicas
pessoa_router = pessoa
fastapi_user_router = fastapi_user
fastapi_logado_router = fastapi_logado
s3_router = s3
# Lista de roteadores para serem registrados
routers = [
fastapi_user_router,
fastapi_logado_router,
tipo_endereco,
endereco,
pessoa_router,
papel,
s3_router,
]

View File

@ -0,0 +1,281 @@
# Importações de bibliotecas padrão
from typing import Annotated, Any, Sequence, List, Optional, Dict, Union
from enum import Enum
# Importações de bibliotecas de terceiros
from fastapi import APIRouter, Depends
from starlette import status
from starlette.responses import Response
from sqlalchemy import Row, RowMapping
from pydantic import BaseModel, Field
# Importações do seu próprio projeto
from app.routers.dependencies import get_repository
from app.database.RepositoryBase import RepositoryBase
from app.database.RelationalTableRepository import RelationalTableRepository
from app.rbac.permissions import verify_permissions
from app.database.models import RbacUser
from app.config import COLUNA
from app.database.formatar_retorno_bd import formatters_map
# Esquema para o corpo da requisição
class FilterCondition(BaseModel):
"""
Representa uma condição de filtro individual.
"""
column: str = Field(..., description="Caminho completo da coluna (exemplo: 'relacao_setor.setor_nome').")
value: Any = Field(..., description="Valor a ser usado no filtro.")
operator: str = Field("==", description="Operador lógico (exemplo: '==', '!=', '<', '>', '>=', '<=').")
logical: str = Field("AND", description="Operador lógico para combinações ('AND', 'OR' etc).")
class FilterRequest(BaseModel):
"""
Representa a requisição contendo filtros dinâmicos.
"""
filters: Optional[List[FilterCondition]] = Field(
None,
description="Lista de condições de filtro. Cada condição contém coluna, operador, valor, e operador lógico."
)
order_by: Optional[List[str]] = Field(
None, description="Lista de colunas para ordenação dos registros."
)
ascending: Optional[List[bool]] = Field(
None, description="Lista de direções da ordenação. True para ascendente, False para descendente. "
"Deve corresponder ao tamanho de 'order_by'."
)
relationships: Optional[List[str]] = Field(
None, description="Lista de nomes de relacionamentos para incluir na consulta."
)
def create_dynamic_router(
rota_prefix: str,
create,
request,
id_many,
id_one,
update,
updates,
db_model,
ativo: Optional = None,
rota_tags: Optional[List[Union[str, Enum]]] = None,
request_formatado: Optional = None,
permissao_total: Optional[int] = None,
permissao_endpoint: Optional[int] = None,
permissao_setor: Optional[int] = None,
permissao_especifica_inicial: Optional[int] = None,
ordenacao: Optional[str] = None,
formatter_keys: Optional[dict[str, str]] = None,
related_info_append: Optional[List[Dict[str, Any]]] = None, # Dados extras para append
related_info_add: Optional[List[Dict[str, Any]]] = None, # Dados extras para add
related_info_extra_columns: Optional[List[Dict[str, Any]]] = None # Dados extras para
):
# Verifica automaticamente se deve usar RelationalTableRepository
use_complex_repo = bool(related_info_append or related_info_add or related_info_extra_columns)
# Define o repositório com base na necessidade de relacionamentos complexos
if use_complex_repo:
repository_base = Annotated[
RelationalTableRepository[db_model],
Depends(get_repository(db_model, RelationalTableRepository)),
]
else:
repository_base = Annotated[
RepositoryBase[db_model],
Depends(get_repository(db_model, RepositoryBase)),
]
router = APIRouter(
prefix=rota_prefix,
tags=rota_tags,
responses={404: {"description": "Not found"}},
)
permissao_especifica_atual = permissao_especifica_inicial or 0
def get_permission_list():
"""Retorna a lista de permissões atualizada e incrementa a permissão específica."""
nonlocal permissao_especifica_atual
# Construir a lista de permissões baseada nos valores informados
permission_list = [
permissao_total,
permissao_endpoint,
permissao_setor,
permissao_especifica_atual if permissao_especifica_inicial is not None else None
]
# Incrementa a permissão específica somente se foi inicialmente definida
if permissao_especifica_inicial is not None:
permissao_especifica_atual += 1
# Filtrar permissões nulas
final_permissions = [perm for perm in permission_list if perm is not None]
return final_permissions
@router.post("/add_one", status_code=status.HTTP_201_CREATED, response_model=request,
# dependencies=[Depends(verify_permissions(get_permission_list()))]
)
async def create_one(data: create, repository: repository_base,
_user: RbacUser = Depends(verify_permissions(get_permission_list()))) -> db_model:
if use_complex_repo:
resultado = await repository.create(
data_one=data,
db_data=db_model,
related_info_append=related_info_append,
related_info_add=related_info_add,
related_info_extra_columns=related_info_extra_columns
)
else:
resultado = await repository.create(data_one=data)
return resultado
@router.post("/add_many", status_code=status.HTTP_201_CREATED, response_model=list[request])
async def create_many(datas: List[create], repository: repository_base,
_user: RbacUser = Depends(verify_permissions(get_permission_list()))) -> (
list[db_model] | bool):
if use_complex_repo:
resultado = await repository.create_many(
data=datas,
db_data=db_model,
return_models=True,
related_info_append=related_info_append,
related_info_add=related_info_add,
related_info_extra_columns=related_info_extra_columns
)
else:
resultado = await repository.create_many(data=datas, return_models=True)
return resultado
permission_list_iguais = get_permission_list() # Obtém a lista de permissões para as funções get_all e get_filter
if request_formatado and formatter_keys and "get_all" in formatter_keys:
formatter_function = formatters_map[formatter_keys["get_all"]]
@router.post("/get_all", status_code=status.HTTP_200_OK, response_model=list[request_formatado])
async def get_all(repository: repository_base,
_user: RbacUser = Depends(verify_permissions(permission_list_iguais))) \
-> Sequence[Row[Any] | RowMapping | Any]:
resultado = await repository.get_many_by_ids(coluna=COLUNA, order_by=ordenacao)
formatado = formatter_function(resultado)
return formatado
@router.post("/get_filter", status_code=status.HTTP_200_OK, response_model=list[request_formatado])
async def get_all(repository: repository_base, filtro: Optional[FilterRequest] = None,
_user: RbacUser = Depends(verify_permissions(permission_list_iguais))) -> (
Sequence)[Row[Any] | RowMapping | Any]:
if not filtro:
resultado = await repository.get_many_by_ids(coluna=COLUNA, order_by=ordenacao)
formatado = formatter_function(resultado)
# Chamando a função do repositório com os filtros e ordenação
else:
resultado = await repository.get_filter(
coluna=COLUNA,
filters=filtro.filters,
order_by=filtro.order_by,
ascending=filtro.ascending
)
formatado = formatter_function(resultado)
return formatado
else:
@router.post("/get_all", status_code=status.HTTP_200_OK, response_model=list[request])
async def get_all(repository: repository_base,
_user: RbacUser = Depends(verify_permissions(permission_list_iguais))) -> (
Sequence)[Row[Any] | RowMapping | Any]:
resultado = await repository.get_many_by_ids(coluna=COLUNA, order_by=ordenacao)
return resultado
@router.post("/get_filter", status_code=status.HTTP_200_OK, response_model=list[request])
async def get_all(repository: repository_base, filtro: Optional[FilterRequest] = None,
_user: RbacUser = Depends(verify_permissions(permission_list_iguais))) -> (
Sequence)[Row[Any] | RowMapping | Any]:
if not filtro:
resultado = await repository.get_many_by_ids(coluna=COLUNA, order_by=ordenacao)
# Chamando a função do repositório com os filtros e ordenação
else:
resultado = await repository.get_filter(
coluna=COLUNA,
filters=filtro.filters,
order_by=filtro.order_by,
ascending=filtro.ascending
)
return resultado
@router.post("/get_many", status_code=status.HTTP_200_OK, response_model=List[request])
async def get_many(data: id_many, repository: repository_base,
_user: RbacUser = Depends(verify_permissions(get_permission_list()))) -> (
Sequence[Row[Any] | RowMapping | Any]):
resultado = await repository.get_many_by_ids(uuids=data.uuids, coluna=COLUNA, order_by=ordenacao)
return resultado
@router.post("/get_one", status_code=status.HTTP_200_OK, response_model=request)
async def get_one(data: id_one, repository: repository_base,
_user: RbacUser = Depends(verify_permissions(get_permission_list()))) -> db_model:
resultado = await repository.get_one_by_id(uuid=data.uuid, coluna=COLUNA)
return resultado
@router.put("/update_one", status_code=status.HTTP_201_CREATED, response_model=request)
async def update_one(data: update, repository: repository_base,
_user: RbacUser = Depends(verify_permissions(get_permission_list()))) -> db_model:
if use_complex_repo:
resultado = await repository.update_by_id(
update=data,
coluna=COLUNA,
db_data=db_model,
related_info_append=related_info_append,
related_info_add=related_info_add,
related_info_extra_columns=related_info_extra_columns
)
else:
resultado = await repository.update_by_id(update=data, coluna=COLUNA)
return resultado
if not use_complex_repo:
@router.put("/update_many", status_code=status.HTTP_201_CREATED, response_model=List[request])
async def update_many(data: List[updates], repository: repository_base,
_user: RbacUser = Depends(verify_permissions(get_permission_list()))) -> (
list[db_model] | db_model):
resultado = await repository.update_many_by_ids(updates=data, coluna=COLUNA, return_models=True)
return resultado
else:
# Não registramos a rota update_many, mas consumimos o efeito colateral da chamada
# de get_permission_list() para que o contador seja incrementado.
_loopPermissoes = get_permission_list()
if ativo is not None:
@router.put("/desativar", status_code=status.HTTP_201_CREATED, response_model=request)
async def desativar(data: ativo, repository: repository_base,
_user: RbacUser = Depends(verify_permissions(get_permission_list()))) -> db_model:
resultado = await repository.desativar_registro(update=data, coluna=COLUNA)
return resultado
@router.put("/ativar", status_code=status.HTTP_201_CREATED, response_model=request)
async def ativar(data: ativo, repository: repository_base,
_user: RbacUser = Depends(verify_permissions([permissao_total]))) -> db_model:
resultado = await repository.ativar_registro(update=data, coluna=COLUNA)
return resultado
else:
# Não registramos a rota desativar, mas consumimos o efeito colateral da chamada
# de get_permission_list() para que o contador seja incrementado.
_loopPermissoes = get_permission_list()
@router.delete("/delete_one", status_code=status.HTTP_204_NO_CONTENT)
async def delete_one(data: id_one, repository: repository_base,
_user: RbacUser = Depends(verify_permissions([permissao_total]))) -> Response:
await repository.remove_by_id(uuid=data.uuid, coluna=COLUNA)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/delete_many", status_code=status.HTTP_204_NO_CONTENT)
async def delete_many(data: id_many, repository: repository_base,
_user: RbacUser = Depends(verify_permissions([permissao_total]))) -> Response:
await repository.remove_many_by_ids(uuids=data.uuids, coluna=COLUNA)
return Response(status_code=status.HTTP_204_NO_CONTENT)
return router

View File

@ -0,0 +1,211 @@
# Importações de bibliotecas padrão
from typing import Union, Annotated, List, Any, Sequence
# Importações de bibliotecas de terceiros
from fastapi import APIRouter, Depends
from starlette import status
from sqlalchemy import Row, RowMapping
# Importações do seu próprio projeto
from app.database.RelationalTableRepository import RelationalTableRepository
from app.database.models import RbacUser
from app.rbac.permissions import verify_permissions
from app.schemas import pessoa_schemas
from starlette.responses import Response
from app.routers.dependencies import get_repository
from app.database import models as db_models
from app.config import COLUNA
# Variáveis comuns
FILE_SCHEMA = pessoa_schemas
PAYLOAD_PJ = FILE_SCHEMA.JuridicaCreate
PAYLOAD_PF = FILE_SCHEMA.FisicaCreate
UPDATE_REQUEST_SCHEMA_PJ = FILE_SCHEMA.PessoaJuridicaUpdate
UPDATE_REQUEST_SCHEMA_PF = FILE_SCHEMA.PessoaFisicaUpdate
ID_REQUEST_SCHEMA = FILE_SCHEMA.IdRequestPessoa
IDS_REQUEST_SCHEMA = FILE_SCHEMA.IdsRequestPessoas
RESPONSE_SCHEMA_PJ = FILE_SCHEMA.RequestPJ
RESPONSE_SCHEMA_PF = FILE_SCHEMA.RequestPF
RESPONSE_SCHEMA_PESSOA = FILE_SCHEMA.RequestPessoa
VALIDA_GET_ALL = FILE_SCHEMA.ValidaGetAll
DB_MODEL_PJ = db_models.ComercialJuridica
DB_MODEL_PF = db_models.ComercialFisica
DB_MODEL_ENDERECO = db_models.ComercialEndereco
DB_MODEL_PESSOA = db_models.ComercialPessoa
DB_MODEL_TIPO_ENDERECO = db_models.ComercialTipoEndereco
DB_MODEL_RELACAO_COMERCIAL = db_models.ComercialRelacaoComercial
DB_MODEL_PESSOA_POLY = db_models.PESSOA_POLY
ROTA_PREFIX = "/api/pessoa"
ROTA_TAGS = "Cadastro Pessoa"
MENSAGEM_ERRO = "Erro ao consulta pessoa"
MENSAGEM_SUCESSO = "Pessoa deletada com sucesso"
# Variáveis tabelas Relacionadas
related_info_append = [
{"key": "rc", "related_model": DB_MODEL_RELACAO_COMERCIAL, "foreign_key_field": "uuid"},
# outros relacionamentos que precisam de append
]
related_info_add = [
{
"key": 'enderecos',
"foreign_key": "fk_pessoa_uuid",
"related_model": DB_MODEL_ENDERECO,
"relations": [
{"related_model_fk": DB_MODEL_TIPO_ENDERECO, "foreign_key_fk": "fk_tipo_endereco_uuid"}
]
},
# outros relacionamentos que precisam de add
]
PjRepository = Annotated[
RelationalTableRepository[DB_MODEL_PJ],
Depends(get_repository(DB_MODEL_PJ, RelationalTableRepository))
]
PfRepository = Annotated[
RelationalTableRepository[DB_MODEL_PF],
Depends(get_repository(DB_MODEL_PF, RelationalTableRepository))
]
PessoaPolyRepository = Annotated[
RelationalTableRepository[DB_MODEL_PESSOA_POLY],
Depends(get_repository(DB_MODEL_PESSOA_POLY, RelationalTableRepository))
]
PessoaRepository = Annotated[
RelationalTableRepository[DB_MODEL_PESSOA],
Depends(get_repository(DB_MODEL_PESSOA, RelationalTableRepository))
]
# RelacaoComercialRepository = Annotated[
# RelationalTableRepository[DB_MODEL_RELACAO_COMERCIAL],
# Depends(get_repository(DB_MODEL_RELACAO_COMERCIAL, RelationalTableRepository))
# ]
#
# TipoEnderecoRepository = Annotated[
# RelationalTableRepository[DB_MODEL_TIPO_ENDERECO],
# Depends(get_repository(DB_MODEL_TIPO_ENDERECO, RelationalTableRepository))
# ]
router = APIRouter(
prefix=ROTA_PREFIX,
tags=[ROTA_TAGS],
responses={404: {"description": "Not found"}},
)
@router.post("/add_one", status_code=201, response_model=RESPONSE_SCHEMA_PESSOA)
async def create_one(data: Union[PAYLOAD_PJ, PAYLOAD_PF], repository_pj: PjRepository,
repository_pf: PfRepository,
_user: RbacUser = Depends(verify_permissions([1, 2, 33, 401]))):
if isinstance(data, PAYLOAD_PJ):
pessoa = await repository_pj.create(data_one=data, db_data=DB_MODEL_PJ,
related_info_add=related_info_add,
related_info_append=related_info_append)
return pessoa
elif isinstance(data, PAYLOAD_PF):
pessoa = await repository_pf.create(data_one=data, db_data=DB_MODEL_PF,
related_info_add=related_info_add,
related_info_append=related_info_append)
return pessoa
@router.post("/add_many", status_code=status.HTTP_201_CREATED, response_model=list[RESPONSE_SCHEMA_PESSOA])
async def create_many(datas: List[Union[PAYLOAD_PJ, PAYLOAD_PF]], repository_pj: PjRepository,
repository_pf: PfRepository,
_user: RbacUser = Depends(verify_permissions([1, 2, 33, 402]))):
# Separando dados em listas de pessoas jurídicas e físicas
data_pj = [data for data in datas if isinstance(data, PAYLOAD_PJ)]
data_pf = [data for data in datas if isinstance(data, PAYLOAD_PF)]
pessoas_fisicas_dto = []
pessoas_juridicas_dto = []
# Criando pessoas físicas
if data_pf:
pessoas_fisicas = await repository_pf.create_many(data=data_pf, return_models=True,
db_data=DB_MODEL_PF,
related_info_add=related_info_add,
related_info_append=related_info_append)
pessoas_fisicas_dto = [RESPONSE_SCHEMA_PF.model_validate(pf, from_attributes=True) for pf in
pessoas_fisicas]
# Criando pessoas jurídicas
if data_pj:
pessoas_juridicas = await repository_pj.create_many(data=data_pj, return_models=True,
db_data=DB_MODEL_PJ,
related_info_add=related_info_add,
related_info_append=related_info_append)
pessoas_juridicas_dto = [RESPONSE_SCHEMA_PJ.model_validate(pj, from_attributes=True) for pj in
pessoas_juridicas]
resultado = pessoas_fisicas_dto + pessoas_juridicas_dto
return resultado
@router.post("/get_all", status_code=status.HTTP_200_OK, response_model=list[RESPONSE_SCHEMA_PESSOA])
async def get_all(repository: PessoaPolyRepository,
_user: RbacUser = Depends(verify_permissions([1, 2, 33, 403]))) -> (
Sequence)[Row[Any] | RowMapping | Any]:
resultado = await repository.get_many_by_ids(coluna=COLUNA)
return resultado
# Testado ok
@router.post("/get_many", status_code=status.HTTP_200_OK, response_model=List[RESPONSE_SCHEMA_PESSOA])
async def get_many(data: IDS_REQUEST_SCHEMA, repository: PessoaPolyRepository,
_user: RbacUser = Depends(verify_permissions([1, 2, 33, 404]))
) -> Sequence[Row[Any] | RowMapping | Any]:
resultado = await repository.get_many_by_ids(uuids=data.uuids, coluna=COLUNA)
return resultado
# Testado ok
@router.post("/get_one", status_code=status.HTTP_200_OK, response_model=RESPONSE_SCHEMA_PESSOA)
async def get_one(data: ID_REQUEST_SCHEMA, repository: PessoaPolyRepository,
_user: RbacUser = Depends(verify_permissions([1, 2, 33, 405]))
) -> RESPONSE_SCHEMA_PESSOA:
resultado = await repository.get_one_by_id(uuid=data.uuid, coluna=COLUNA)
return resultado
@router.put("/update_one", status_code=status.HTTP_201_CREATED, response_model=RESPONSE_SCHEMA_PESSOA)
async def update_one(data: Union[UPDATE_REQUEST_SCHEMA_PJ, UPDATE_REQUEST_SCHEMA_PF], repository_pj: PjRepository,
repository_pf: PfRepository,
_user: RbacUser = Depends(verify_permissions([1, 2, 33, 406]))):
if isinstance(data, UPDATE_REQUEST_SCHEMA_PJ):
resultado = await repository_pj.update_by_id(update=data, coluna=COLUNA,
db_data=DB_MODEL_PJ,
related_info_add=related_info_add,
related_info_append=related_info_append)
return resultado
elif isinstance(data, UPDATE_REQUEST_SCHEMA_PF):
resultado = await repository_pf.update_by_id(update=data, coluna=COLUNA,
db_data=DB_MODEL_PF,
related_info_add=related_info_add,
related_info_append=related_info_append)
return resultado
@router.delete("/delete_one", status_code=status.HTTP_204_NO_CONTENT)
async def delete_one(data: ID_REQUEST_SCHEMA, repository: PessoaRepository,
_user: RbacUser = Depends(verify_permissions([1, 2, 33, 408]))) -> Response:
await repository.remove_by_id(uuid=data.uuid, coluna=COLUNA)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/delete_many", status_code=status.HTTP_204_NO_CONTENT)
async def delete_many(data: IDS_REQUEST_SCHEMA, repository: PessoaRepository,
_user: RbacUser = Depends(verify_permissions([1, 2, 33, 409]))) -> Response:
await repository.remove_many_by_ids(uuids=data.uuids, coluna=COLUNA)
return Response(status_code=status.HTTP_204_NO_CONTENT)

View File

@ -0,0 +1,13 @@
# app/utils/router_registry.py
from fastapi import FastAPI, APIRouter
from typing import List
class RouterRegistry:
def __init__(self, app: FastAPI, routers: List[APIRouter]):
self.app = app
self.routers = routers
def register_routers(self):
for router in self.routers:
self.app.include_router(router)

271
app/s3/RepositoryS3.py Normal file
View File

@ -0,0 +1,271 @@
# Importações de bibliotecas padrão
from typing import Generic, TypeVar
from uuid import uuid4, UUID
import os
# Importações de bibliotecas de terceiros
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from fastapi import HTTPException
# Importações integração S3
import boto3
import app.config as config
from starlette.responses import StreamingResponse
from botocore.exceptions import ClientError
from boto3.exceptions import Boto3Error
from app.s3 import schema_s3
# Importações do seu próprio projeto
from app.database.RepositoryBase import (
RepositoryBase,
)
from app.database import models
Model = TypeVar("Model", bound=models.Base)
Schema = TypeVar("Schema", bound=BaseModel)
s3_client = boto3.client(
"s3",
aws_access_key_id=config.S3_ACCESS_KEY_ID,
aws_secret_access_key=config.S3_SECRET_ACCESS_KEY,
endpoint_url=config.S3_ENDPOINT_URL,
region_name=config.S3_REGION_NAME,
)
def get_s3_path(inquilino: UUID, nome_arquivo: str) -> str:
"""
Gera o caminho completo no S3 para um arquivo de um inquilino específico.
"""
return f"{inquilino}/{nome_arquivo}"
class RepositoryS3(RepositoryBase[Model], Generic[Model]):
def __init__(self, model: type[Model], session: AsyncSession) -> None:
super().__init__(model, session)
async def get_file_record_by_uuid(self, uuid: UUID) -> models.S3Arquivo:
"""
Busca o registro de um arquivo pelo UUID no banco de dados.
Levanta uma exceção HTTP 404 se o arquivo não for encontrado.
"""
try:
# Chamar a função herdada para buscar o arquivo pelo UUID
arquivo = await self.get_one_by_id(uuid, coluna="uuid")
if not arquivo:
raise HTTPException(status_code=404, detail="Arquivo não encontrado no banco de dados")
return arquivo
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao buscar o arquivo no banco: {str(e)}")
async def upload_to_s3(self, conteudo, nome_original, tipo_conteudo, inquilino: UUID):
"""
Faz upload do conteúdo para o S3 e salva apenas o nome do arquivo no banco.
Apaga o arquivo do S3 em caso de erro ao salvar no banco.
Agora o nome do arquivo é um UUID com a extensão do arquivo original.
"""
s3_path = None # Inicializa a variável no escopo superior
try:
# Obter a extensão do arquivo original
_, file_extension = os.path.splitext(nome_original)
file_extension = file_extension.lower() # Garantir que a extensão esteja em minúsculo
# Gerar um nome único para o arquivo (UUID + extensão)
unique_filename = f"{uuid4()}{file_extension}"
# Gerar o caminho completo no S3
s3_path = get_s3_path(inquilino, unique_filename)
# Fazer upload do arquivo para o S3
s3_client.upload_fileobj(
conteudo,
config.S3_BUCKET_NAME,
s3_path,
ExtraArgs={
"ContentType": tipo_conteudo,
# Nenhum metadado adicional salvo
},
)
# Salvar apenas o nome do arquivo no banco
arquivo_data = schema_s3.ArquivoCreate(
arquivos_nome_original=nome_original,
arquivos_nome_armazenado=unique_filename
)
novo_arquivo = await self.create(arquivo_data)
return novo_arquivo
except Boto3Error as e:
raise HTTPException(status_code=500, detail=f"Erro no S3: {str(e)}")
except Exception as e:
# Apagar o arquivo do S3 em caso de erro no banco de dados
if s3_path: # Verifica se s3_path foi atribuído
try:
s3_client.delete_object(Bucket=config.S3_BUCKET_NAME, Key=s3_path)
except Exception as delete_error:
print(f"Erro ao apagar arquivo do S3: {str(delete_error)}")
# Relançar a exceção original
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
async def get_presigned_url(self, uuid: UUID, inquilino: UUID) -> dict:
"""
Gera uma URL pré-assinada para download do arquivo, com o nome original configurado.
"""
try:
# Buscar o registro do arquivo usando a função intermediária
arquivo = await self.get_file_record_by_uuid(uuid)
# Obter o nome armazenado e original do arquivo
file_name = arquivo.arquivos_nome_armazenado
original_filename = arquivo.arquivos_nome_original
# Gerar o caminho completo no S3
s3_path = get_s3_path(inquilino, file_name)
# Gerar a URL pré-assinada para download
presigned_url = s3_client.generate_presigned_url(
"get_object",
Params={
"Bucket": config.S3_BUCKET_NAME,
"Key": s3_path,
"ResponseContentDisposition": f'attachment; filename="{original_filename}"'
},
ExpiresIn=3600, # URL válida por 1 hora
)
return {"url": presigned_url}
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3")
raise HTTPException(status_code=500, detail=f"Erro ao gerar URL: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
async def generate_presigned_url(self, uuid: UUID, inquilino: UUID) -> dict:
"""
Gera uma URL pré-assinada para acessar o arquivo no S3 (sem download automático).
"""
try:
# Buscar o registro do arquivo usando a função intermediária
arquivo = await self.get_file_record_by_uuid(uuid)
# Obter o nome armazenado do arquivo
file_name = arquivo.arquivos_nome_armazenado
# Gerar o caminho completo no S3
s3_path = get_s3_path(inquilino, file_name)
# Gerar uma URL pré-assinada
presigned_url = s3_client.generate_presigned_url(
"get_object",
Params={
"Bucket": config.S3_BUCKET_NAME,
"Key": s3_path
},
ExpiresIn=3600, # URL válida por 1 hora
)
return {"url": presigned_url}
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3")
raise HTTPException(status_code=500, detail=f"Erro ao gerar URL: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
async def get_file(self, uuid: UUID, inquilino: UUID) -> StreamingResponse:
"""
Retorna um arquivo específico para download com o nome original configurado.
"""
try:
# Buscar o registro do arquivo usando a função intermediária
arquivo = await self.get_file_record_by_uuid(uuid)
# Obter o nome armazenado e original do arquivo
file_name = arquivo.arquivos_nome_armazenado
original_filename = arquivo.arquivos_nome_original
# Gerar o caminho completo no S3
s3_path = get_s3_path(inquilino, file_name)
# Obter o objeto do S3
response = s3_client.get_object(Bucket=config.S3_BUCKET_NAME, Key=s3_path)
# Retornar o arquivo como um fluxo
return StreamingResponse(
response["Body"],
media_type=response["ContentType"], # Tipo do conteúdo, ex.: image/jpeg
headers={"Content-Disposition": f'attachment; filename="{original_filename}"'}
)
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3")
raise HTTPException(status_code=500, detail=f"Erro ao acessar o arquivo: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
async def get_file_inline(self, uuid: UUID, inquilino: UUID) -> StreamingResponse:
"""
Retorna um arquivo específico para exibição inline (sem download automático).
"""
try:
# Buscar o registro do arquivo usando a função intermediária
arquivo = await self.get_file_record_by_uuid(uuid)
# Obter o nome armazenado do arquivo
file_name = arquivo.arquivos_nome_armazenado
# Gerar o caminho completo no S3
s3_path = get_s3_path(inquilino, file_name)
# Obter o objeto do S3
response = s3_client.get_object(Bucket=config.S3_BUCKET_NAME, Key=s3_path)
# Retornar o arquivo como um fluxo para exibição inline
return StreamingResponse(
response["Body"],
media_type=response["ContentType"], # Tipo do conteúdo, ex.: image/jpeg
)
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3")
raise HTTPException(status_code=500, detail=f"Erro ao acessar o arquivo: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
async def delete_file_from_s3(self, uuid: UUID, inquilino: UUID):
"""
Remove um arquivo do S3 e o registro correspondente no banco de dados, usando o UUID como identificador.
"""
try:
# Buscar o registro do arquivo usando o UUID
arquivo = await self.get_file_record_by_uuid(uuid)
# Obter o nome armazenado do arquivo
file_name = arquivo.arquivos_nome_armazenado
# Gerar o caminho completo no S3
s3_path = get_s3_path(inquilino, file_name)
# Deletar o arquivo do S3
s3_client.delete_object(Bucket=config.S3_BUCKET_NAME, Key=s3_path)
# Remover o registro do banco de dados
result = await self.remove_by_id(uuid, coluna="uuid")
return {"message": "Arquivo deletado com sucesso", "details": result}
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3")
raise HTTPException(status_code=500, detail=f"Erro ao deletar arquivo: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")

0
app/s3/__init__.py Normal file
View File

170
app/s3/router_s3.py Normal file
View File

@ -0,0 +1,170 @@
# Importações de bibliotecas padrão
from typing import Annotated
# Importações de bibliotecas de terceiros
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
from starlette import status
# Importações do seu próprio projeto
from app.routers.dependencies import get_repository
from app.s3.RepositoryS3 import RepositoryS3
from app.rbac.permissions import verify_permissions
from app.database.models import RbacUser, S3Arquivo
from app.s3 import schema_s3
db_model = S3Arquivo
rota_prefix = "/api/s3/arquivos"
rota_tags = "S3 Arquivos"
repository_base = Annotated[
RepositoryS3[db_model],
Depends(get_repository(db_model, RepositoryS3)),
]
router = APIRouter(
prefix=rota_prefix,
tags=[rota_tags],
responses={404: {"description": "Not found"}},
)
@router.post("/upload", status_code=status.HTTP_200_OK, response_model=schema_s3.ArquivoRetorno)
async def upload_to_s3(
repository: repository_base,
file: UploadFile = File(...),
# repository: S3Repository[db_model] = Depends(get_repository_simple_table(S3Arquivo)),
user: RbacUser = Depends(verify_permissions([1, 2, 33, 401]))):
"""
Faz upload de uma imagem para o bucket restrito e registra no banco de dados.
"""
try:
# Chamar a função do repositório para realizar o upload e salvar no banco
resultado = await repository.upload_to_s3(
conteudo=file.file,
nome_original=file.filename,
tipo_conteudo=file.content_type,
inquilino=user.fk_inquilino_uuid
)
return resultado
except HTTPException as e:
raise e # Relevanta a exceção HTTP já tratada no repositório
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
@router.post("/url/")
async def get_presigned_url(
request: schema_s3.FileNameRequest,
repository: repository_base,
user: RbacUser = Depends(verify_permissions([1, 2, 33, 401])), # Permissões necessárias
):
"""
Gera uma URL pré-assinada para download do arquivo, com o nome original configurado.
"""
try:
# Chamar a função do repositório para gerar a URL pré-assinada
presigned_url = await repository.get_presigned_url(
uuid=request.uuid,
inquilino=user.fk_inquilino_uuid
)
return {"url": presigned_url}
except HTTPException as e:
raise e # Relança exceções HTTP já tratadas no repositório
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
@router.post("/url/simple/")
async def generate_presigned_url(
request: schema_s3.FileNameRequest,
repository: repository_base,
user: RbacUser = Depends(verify_permissions([1, 2, 33, 401])), # Permissões necessárias
):
"""
Gera uma URL pré-assinada para acessar o arquivo no MinIO (sem download automático).
"""
try:
# Chamar a função do repositório para gerar a URL pré-assinada
presigned_url = await repository.generate_presigned_url(
uuid=request.uuid,
inquilino=user.fk_inquilino_uuid # Passa diretamente o UUID do inquilino
)
return {"url": presigned_url}
except HTTPException as e:
raise e # Relança exceções HTTP já tratadas no repositório
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
@router.post("/")
async def get_file(
request: schema_s3.FileNameRequest,
repository: repository_base,
user: RbacUser = Depends(verify_permissions([1, 2, 33, 401])), # Permissões necessárias
):
"""
Retorna uma imagem específica para download.
O usuário precisa estar autenticado para acessar.
"""
try:
# Chamar a função do repositório para obter a imagem como streaming
response = await repository.get_file(
uuid=request.uuid,
inquilino=user.fk_inquilino_uuid # Passa diretamente o UUID do inquilino
)
return response
except HTTPException as e:
raise e # Relança exceções HTTP já tratadas no repositório
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
@router.post("/inline/")
async def get_file_inline(
request: schema_s3.FileNameRequest,
repository: repository_base,
user: RbacUser = Depends(verify_permissions([1, 2, 33, 401])), # Permissões necessárias
):
"""
Retorna uma imagem ou arquivo específico para exibição inline (sem download automático).
O usuário precisa estar autenticado para acessar.
"""
try:
# Chamar a função do repositório para obter o streaming da imagem
response = await repository.get_file_inline(
uuid=request.uuid,
inquilino=user.fk_inquilino_uuid # Passa diretamente o UUID do inquilino
)
return response
except HTTPException as e:
raise e # Relança exceções HTTP já tratadas no repositório
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
@router.delete("/")
async def delete_file(
request: schema_s3.FileNameRequest,
repository: repository_base,
user: RbacUser = Depends(verify_permissions([1, 2, 33, 401])), # Permissões necessárias
):
"""
Exclui um arquivo do S3 e remove seu registro do banco de dados.
"""
try:
# Chamar a função do repositório para excluir o arquivo
result = await repository.delete_file_from_s3(
uuid=request.uuid,
inquilino=user.fk_inquilino_uuid # Passa diretamente o UUID do inquilino
)
return result
except HTTPException as e:
raise e # Relança exceções HTTP já tratadas no repositório
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")

View File

@ -0,0 +1,122 @@
import boto3
import app.config as config
import uuid
from fastapi import APIRouter, UploadFile, HTTPException
from starlette import status
from starlette.responses import StreamingResponse
from botocore.exceptions import ClientError
from app.s3 import schema_s3
s3_client = boto3.client(
"s3",
aws_access_key_id=config.S3_ACCESS_KEY_ID,
aws_secret_access_key=config.S3_SECRET_ACCESS_KEY,
endpoint_url=config.S3_ENDPOINT_URL,
region_name=config.S3_REGION_NAME,
)
router = APIRouter(
prefix="/api/s3",
tags=["S3 Manipulação Arquivos"],
responses={404: {"description": "Not found"}},
)
@router.post("/upload", status_code=status.HTTP_200_OK)
async def upload_to_s3(file: UploadFile):
"""
Faz upload de uma imagem para o bucket restrito.
"""
try:
unique_filename = f"{uuid.uuid4()}-{file.filename}"
s3_client.upload_fileobj(
file.file,
config.S3_BUCKET_NAME,
unique_filename,
ExtraArgs={
"ContentType": file.content_type,
"Metadata": {"original_filename": file.filename}
},
)
return {"message": "Arquivo salvo com sucesso", "nome do arquivo": unique_filename}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/images/url/")
async def get_presigned_url(request: schema_s3.FileNameRequest):
"""
Gera uma URL pré-assinada para download do arquivo, com o nome original configurado.
"""
try:
file_name = request.file_name # Nome do arquivo com UUID
# Obter os metadados do arquivo para o nome original
response = s3_client.head_object(Bucket=config.S3_BUCKET_NAME, Key=file_name)
metadata = response.get("Metadata", {})
original_filename = metadata.get("original_filename", file_name)
# Gerar uma URL pré-assinada para download
presigned_url = s3_client.generate_presigned_url(
"get_object",
Params={
"Bucket": config.S3_BUCKET_NAME,
"Key": file_name,
"ResponseContentDisposition": f'attachment; filename="{original_filename}"'
},
ExpiresIn=3600, # URL válida por 1 hora
)
return {"url": presigned_url}
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise HTTPException(status_code=404, detail="Arquivo não encontrado")
raise HTTPException(status_code=500, detail="Erro ao gerar URL")
@router.post("/images/url/simple/")
async def generate_presigned_url(request: schema_s3.FileNameRequest):
"""
Gera uma URL pré-assinada para acessar o arquivo no MinIO (sem download automático).
"""
try:
file_name = request.file_name # Nome do arquivo com UUID
# Gerar uma URL pré-assinada sem configurar o download automático
presigned_url = s3_client.generate_presigned_url(
"get_object",
Params={
"Bucket": config.S3_BUCKET_NAME,
"Key": file_name
},
ExpiresIn=3600, # URL válida por 1 hora
)
return {"url": presigned_url}
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise HTTPException(status_code=404, detail="File not found")
raise HTTPException(status_code=500, detail="Error generating presigned URL")
@router.post("/images/")
async def get_image(request: schema_s3.FileNameRequest):
"""
Retorna uma imagem específica para download.
O usuário precisa estar autenticado para acessar.
"""
try:
file_name = request.file_name
# Baixar o objeto do MinIO como um fluxo
response = s3_client.get_object(Bucket=config.S3_BUCKET_NAME, Key=file_name)
metadata = response.get("Metadata", {})
original_filename = metadata.get("original_filename", file_name) # Nome original ou o atual
return StreamingResponse(
response["Body"],
media_type=response["ContentType"], # Tipo de arquivo, ex.: image/jpeg
headers={"Content-Disposition": f'attachment; filename="{original_filename}"'}
)
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise HTTPException(status_code=404, detail="File not found")
raise HTTPException(status_code=500, detail="Error accessing file")

17
app/s3/schema_s3.py Normal file
View File

@ -0,0 +1,17 @@
from pydantic import BaseModel, ConfigDict
from uuid import UUID
# Modelo para a entrada do nome do arquivo
class FileNameRequest(BaseModel):
uuid: UUID
class ArquivoCreate(BaseModel):
model_config = ConfigDict(from_attributes=True)
arquivos_nome_original: str
arquivos_nome_armazenado: str
class ArquivoRetorno(ArquivoCreate):
uuid: UUID

5
app/schemas/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from . import endereco_schemas
from . import papel_shemas
from . import pessoa_schemas
from . import tipo_endereco_schemas
from . import utils

View File

@ -0,0 +1,64 @@
# Importações de bibliotecas padrão
from typing import Annotated, Optional
from uuid import UUID
# Importações de bibliotecas de terceiros
from pydantic import BaseModel, Field, ConfigDict
from .utils import UuidMixinSchema, TimestampMixinSchema, UuidsMixinSchema
from app.schemas.tipo_endereco_schemas import Consulta as ConsultaSchema
class EnderecoCreate(BaseModel):
model_config = ConfigDict(from_attributes=True)
endereco_pessoa_status: bool
endereco_pessoa_descricao: str = Field(min_length=3, max_length=50)
endereco_pessoa_numero: str = Field(min_length=1, max_length=8)
endereco_pessoa_complemento: str = Field(min_length=3, max_length=50, default="S/N")
endereco_pessoa_cep: str = Field(min_length=8, max_length=8)
fk_tipo_endereco_uuid: UUID
class Create(EnderecoCreate):
fk_pessoa_uuid: UUID
class Request(TimestampMixinSchema, Create, UuidMixinSchema):
uuid: UUID | None = None
fk_pessoa_uuid: UUID | None = None
relacao_tipo_endereco: ConsultaSchema | None = None
# relacao_tipo_endereco: UUIDSchema
class EnderecoRequest(Request):
pass
class UpdateSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
uuid: UUID | None = None
endereco_pessoa_status: bool
endereco_pessoa_descricao: Optional[Annotated[str, Field(max_length=50)]] = None
endereco_pessoa_numero: Optional[Annotated[str, Field(max_length=8)]] = None
endereco_pessoa_complemento: Optional[Annotated[str, Field(max_length=50)]] = None
endereco_pessoa_cep: Optional[Annotated[str, Field(max_length=8)]] = None
fk_tipo_endereco_uuid: UUID | None = None
class EnderecoBaseUpdate(UpdateSchema):
pass
class IdRequest(UuidMixinSchema):
pass
class IdsRequest(UuidsMixinSchema):
pass
class UpdateRequest(UpdateSchema, IdRequest):
pass
class UpdateManyRequest(UpdateSchema, IdRequest):
pass

View File

@ -0,0 +1,46 @@
# Importações de bibliotecas padrão
from datetime import datetime
from typing import Optional, List
from uuid import UUID as UuidType
# Importações de bibliotecas de terceiros
from pydantic import BaseModel, ConfigDict
# Importações do seu próprio projeto
from .utils import UuidMixinSchema, TimestampMixinSchema, UuidsMixinSchema
class PermissaoModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
class Create(BaseModel):
model_config = ConfigDict(from_attributes=True)
nome: str
permissoes: List[PermissaoModel]
class Request(Create):
permissoes: List[PermissaoModel]
class UpdateSchema(BaseModel):
nome: Optional[str] = None
class IdRequest(UuidMixinSchema):
pass
class IdsRequest(UuidsMixinSchema):
pass
class UpdateRequest(UpdateSchema, IdRequest):
pass
class UpdateManyRequest(UpdateSchema, IdRequest):
pass

View File

@ -0,0 +1,118 @@
# Importações de bibliotecas padrão
from typing import Union, List, Literal, Annotated, Optional
from uuid import UUID
# Importações de bibliotecas de terceiros
from pydantic import BaseModel, Field, ConfigDict, EmailStr
from app.schemas.utils import UUIDSchema
from app.schemas.endereco_schemas import EnderecoCreate, EnderecoRequest, EnderecoBaseUpdate
class PessoaPayload(BaseModel):
model_config = ConfigDict(from_attributes=True)
pessoa_status: bool = Field(default=True)
pessoa_telefone: str = Field(min_length=8, max_length=20)
pessoa_celular: str = Field(min_length=9, max_length=20)
pessoa_email: EmailStr = Field(min_length=8, max_length=50)
pessoa_local_evento: bool = Field(default=False)
class FisicaCreate(PessoaPayload):
pessoa_tipo: Literal["1"]
fisica_cpf: str = Field(min_length=11, max_length=11) # Em produção usar validadores prontos
fisica_rg: str = Field(min_length=5, max_length=20)
fisica_genero: Literal["M", "F", "HT", "MT", "T", "NB", "O"]
fisica_nome: str = Field(min_length=3, max_length=100)
enderecos: List[EnderecoCreate] | None = None
rc: List[UUIDSchema] | None = None
class JuridicaCreate(PessoaPayload):
pessoa_tipo: Literal["0"]
juridica_cnpj: str = Field(min_length=14, max_length=14) # Em produção usar validadores prontos
juridica_email_fiscal: EmailStr = Field(min_length=8, max_length=50)
juridica_insc_est: Optional[Annotated[str, Field(min_length=5, max_length=50)]] = None
juridica_ins_mun: Optional[Annotated[str, Field(min_length=5, max_length=50)]] = None
juridica_razao_social: str = Field(min_length=5, max_length=200)
juridica_representante: str = Field(min_length=3, max_length=100)
enderecos: List[EnderecoCreate] | None = None
rc: List[UUIDSchema] | None = None
class PessoaBaseResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
uuid: UUID
pessoa_status: bool
pessoa_telefone: str | None = None
pessoa_celular: str | None = None
pessoa_email: str | None = None
pessoa_local_evento: bool
class RequestPJ(PessoaBaseResponse):
pessoa_tipo: Literal["0"]
juridica_cnpj: str | None = None
juridica_email_fiscal: str | None = None
juridica_insc_est: str | None = None
juridica_ins_mun: str | None = None
juridica_razao_social: str | None = None
juridica_representante: str | None = None
enderecos: List[EnderecoRequest] = []
class RequestPF(PessoaBaseResponse):
pessoa_tipo: Literal["1"]
fisica_cpf: str | None = None
fisica_rg: str | None = None
fisica_genero: str | None = None
fisica_nome: str
enderecos: List[EnderecoRequest] = []
RequestPessoa = Annotated[
Union[RequestPF, RequestPJ],
Field(discriminator='pessoa_tipo')
]
class IdRequestPessoa(BaseModel):
uuid: UUID = None
class IdsRequestPessoas(BaseModel):
uuids: List[UUID] = None
class PessoaBaseUpdate(IdRequestPessoa):
pessoa_status: bool
pessoa_telefone: Optional[Annotated[str, Field(min_length=8, max_length=20)]] = None
pessoa_celular: Optional[Annotated[str, Field(min_length=8, max_length=20)]] = None
pessoa_email: Optional[Annotated[EmailStr, Field(min_length=8, max_length=50)]] = None
pessoa_local_evento: bool
pessoa_tipo: Optional[Literal["0", "1"]] = None
class PessoaFisicaUpdate(PessoaBaseUpdate):
fisica_cpf: str = Field(min_length=11, max_length=11)
fisica_rg: Optional[Annotated[str, Field(min_length=5, max_length=20)]] = None
fisica_genero: Optional[Literal["M", "F", "HT", "MT", "T", "NB", "O"]] = None
fisica_nome: Optional[Annotated[str, Field(min_length=3, max_length=100)]] = None
enderecos: List[EnderecoBaseUpdate] | None = None
rc: List[UUIDSchema] | None = None
class PessoaJuridicaUpdate(PessoaBaseUpdate):
juridica_cnpj: str = Field(min_length=14, max_length=14)
juridica_email_fiscal: Optional[Annotated[EmailStr, Field(min_length=8, max_length=50)]] = None
juridica_insc_est: Optional[Annotated[str, Field(min_length=5, max_length=50)]] = None
juridica_ins_mun: Optional[Annotated[str, Field(min_length=5, max_length=50)]] = None
juridica_razao_social: Optional[Annotated[str, Field(min_length=5, max_length=200)]] = None
juridica_representante: Optional[Annotated[str, Field(min_length=3, max_length=100)]] = None
enderecos: List[EnderecoBaseUpdate] | None = None
rc: List[UUIDSchema] | None = None
class ValidaGetAll(BaseModel):
validador: Literal["0", "1"] | None = None

View File

@ -0,0 +1,44 @@
# Importações de bibliotecas padrão
from typing import Optional
# Importações de bibliotecas de terceiros
from pydantic import BaseModel, ConfigDict, Field
# Importações do seu próprio projeto
from .utils import UuidMixinSchema, TimestampMixinSchema, UuidsMixinSchema
from uuid import UUID
class Create(BaseModel):
model_config = ConfigDict(from_attributes=True)
tipo_endereco_descricao: str = Field(min_length=3, max_length=30)
class Request(TimestampMixinSchema, Create, UuidMixinSchema):
pass
class Consulta(BaseModel):
model_config = ConfigDict(from_attributes=True)
uuid: UUID
tipo_endereco_descricao: str | None = None
class UpdateSchema(BaseModel):
tipo_endereco_descricao: Optional[str] = Field(min_length=3, max_length=30, default=None)
class IdRequest(UuidMixinSchema):
pass
class IdsRequest(UuidsMixinSchema):
pass
class UpdateRequest(UpdateSchema, IdRequest):
pass
class UpdateManyRequest(UpdateSchema, IdRequest):
pass

View File

@ -0,0 +1,68 @@
# Importações de bibliotecas padrão
import re
from typing import List
from uuid import UUID
# Importações de bibliotecas de terceiros
from pydantic import BaseModel, field_validator, EmailStr, ConfigDict
class Papeis(BaseModel):
model_config = ConfigDict(from_attributes=True)
uuid: UUID
class PapeisResponse(Papeis):
nome: str
class UsuarioBase(BaseModel):
model_config = ConfigDict(from_attributes=True)
username: str
full_name: str
email: EmailStr
papeis: List[Papeis]
# @field_validator('username')
# def validate_username(cls, value):
# if not re.match('^([a-z]|[0-9]|@)+$', value):
# raise ValueError('Username format invalid')
# return value
class UsuarioCreate(UsuarioBase):
password: str
class UsuarioResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
username: str
full_name: str
email: EmailStr
papeis: List[PapeisResponse]
# @field_validator('username')
# def validate_username(cls, value):
# if not re.match('^([a-z]|[0-9]|@)+$', value):
# raise ValueError('Username format invalid')
# return value
class UsuarioRequest(UsuarioBase):
pass
class UsuarioLogin(BaseModel):
username: str
password: str
# @field_validator('username')
# def validate_username(cls, value):
# if not re.match('^([a-z]|[0-9]|@)+$', value):
# raise ValueError('Username format invalid')
# return value
class Token(BaseModel):
access_token: str
token_type: str

29
app/schemas/utils.py Normal file
View File

@ -0,0 +1,29 @@
# Importações de bibliotecas padrão
from datetime import datetime
from typing import List, Optional
from uuid import UUID as UuidType
# Importações de bibliotecas de terceiros
from pydantic import BaseModel, ConfigDict
class UuidMixinSchema(BaseModel):
uuid: UuidType = None
class UuidsMixinSchema(BaseModel):
uuids: List[UuidType] = None
class TimestampMixinSchema(BaseModel):
created_at: datetime | None = None
updated_at: datetime | None = None
class UUIDSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
uuid: UuidType
class RegistroAtivo(UuidMixinSchema):
ativo: Optional[bool] = None

139
app/schemas/validacoes.py Normal file
View File

@ -0,0 +1,139 @@
from datetime import date
from decimal import Decimal
from fastapi import HTTPException
class ValidacoesUtil:
@classmethod
def validar_valor_monetario(cls, value: float, precision: int = 10, scale: int = 2):
decimal_value = Decimal(value).quantize(Decimal(f'1.{"0" * scale}'))
if len(decimal_value.as_tuple().digits) > precision:
raise ValueError(f"O valor excede {precision} dígitos, incluindo, {scale} casas decimais.")
return float(decimal_value)
# Você pode adicionar outras validações aqui
@classmethod
def validar_se_positivo(cls, value: float):
if value <= 0:
raise HTTPException(status_code=400, detail="O valor deve ser positivo.")
return value
@classmethod
def data_no_passado(cls, data: date):
"""
Valida se a data é no passado (antes de hoje).
"""
if data >= date.today():
raise HTTPException(status_code=400, detail="A data deve estar no passado.")
return data
@classmethod
def data_no_passado_ou_hoje(cls, data: date):
"""
Valida se a data é no passado ou hoje (igual ou antes de hoje).
"""
if data > date.today():
raise HTTPException(status_code=400, detail="A data não pode ser no futuro.")
return data
@classmethod
def data_no_futuro(cls, data: date):
"""
Valida se a data é no futuro (depois de hoje).
"""
if data <= date.today():
raise HTTPException(status_code=400, detail="A data deve ser no futuro.")
return data
@classmethod
def data_no_futuro_ou_hoje(cls, data: date):
"""
Valida se a data é hoje ou no futuro (igual ou depois de hoje).
"""
if data < date.today():
raise HTTPException(status_code=400, detail="A data não pode estar no passado.")
return data
@classmethod
def data_final_maior(cls, data_final: date, data_inicial: date):
"""
Valida se a data final é posterior à data inicial.
"""
if data_final <= data_inicial:
raise HTTPException(status_code=400, detail="A data final deve ser posterior à data inicial.")
return data_final
@classmethod
def data_final_maior_ou_igual(cls, data_final: date, data_inicial: date):
"""
Valida se a data final é igual ou posterior à data inicial.
"""
if data_final < data_inicial:
raise HTTPException(status_code=400, detail="A data final não pode ser anterior à data inicial.")
return data_final
@classmethod
def data_final_menor(cls, data_final: date, data_inicial: date):
"""
Valida se a data final é anterior à data inicial.
"""
if data_final >= data_inicial:
raise HTTPException(status_code=400, detail="A data final deve ser anterior à data inicial.")
return data_final
@classmethod
def data_final_menor_ou_igual(cls, data_final: date, data_inicial: date):
"""
Valida se a data final é igual ou anterior à data inicial.
"""
if data_final > data_inicial:
raise HTTPException(status_code=400, detail="A data final não pode ser posterior à data inicial.")
return data_final
@classmethod
def validate_intervalo_minimo(cls, data_inicial: date, data_final: date, dias_minimos: int):
"""
Valida se um intervalo mínimo de dias entre as duas datas.
"""
if (data_final - data_inicial).days < dias_minimos:
raise HTTPException(status_code=400,
detail=f"O intervalo entre as datas deve ser de pelo menos {dias_minimos} dias.")
return data_final
@classmethod
def validate_intervalo_maximo(cls, data_inicial: date, data_final: date, dias_maximos: int):
"""
Valida se o intervalo entre duas datas não excede um número máximo de dias.
"""
if (data_final - data_inicial).days > dias_maximos:
raise HTTPException(status_code=400,
detail=f"O intervalo entre as datas não pode ser maior que {dias_maximos} dias.")
return data_final
@classmethod
def validate_data_intervalo_anos(cls, data: date, ano_inicial: int, ano_final: int):
"""
Valida se a data está dentro de um intervalo de anos permitido.
"""
if not (ano_inicial <= data.year <= ano_final):
raise HTTPException(status_code=400, detail=f"A data deve estar entre os anos {ano_inicial} e {ano_final}.")
return data
@classmethod
def validate_data_expirada(cls, data_expiracao: date):
"""
Verifica se a data de expiração passou.
"""
if data_expiracao < date.today():
raise HTTPException(status_code=400, detail="A data de expiração já passou.")
return data_expiracao
@classmethod
def validate_dia_util(cls, data: date):
"""
Valida se a data cai em um dia útil (segunda a sexta-feira).
"""
if data.weekday() > 4: # Dias 5 e 6 são sábado e domingo
raise HTTPException(status_code=400, detail="A data deve ser em um dia útil.")
return data

0
app/scripts/__init__.py Normal file
View File

View File

@ -0,0 +1,41 @@
import contextlib
from app.rbac.auth import get_user_db, get_user_manager
from app.rbac.schemas import UserCreate
from app.database.session import get_db
from sqlalchemy import select
from fastapi_users.exceptions import UserAlreadyExists
from app.database.models import RbacUser
get_async_session_context = contextlib.asynccontextmanager(get_db)
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
async def create_user(email: str, password: str, full_name: str, username: str, is_superuser: bool = False):
async with get_async_session_context() as session:
async with get_user_db_context(session) as user_db:
async with get_user_manager_context(user_db) as user_manager:
try:
user = await user_manager.create(
UserCreate(
email=email,
password=password,
username=username,
full_name=full_name,
is_superuser=is_superuser,
is_active=True
)
)
return user.id
except UserAlreadyExists:
print(f"Usuário {email} já existe")
result_user = await session.execute(select(RbacUser).filter_by(email=email))
existing_user = result_user.scalars().first()
return existing_user.id
async def create_initial_users():
user_id = await create_user(email="admin@sonora.com", password="admin", is_superuser=True,
username="Admin", full_name="Admin")
return user_id

View File

@ -0,0 +1,37 @@
import contextlib
from fastapi_users.exceptions import UserAlreadyExists
from sqlalchemy import select
from app.database.session import get_db
from app.rbac.auth import get_user_db, get_user_manager
from app.rbac.schemas import UserCreate
from app.database.models import RbacUser
get_async_session_context = contextlib.asynccontextmanager(get_db)
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
async def create_user(email: str, password: str, full_name: str, username: str, is_superuser: bool = False):
try:
async with get_async_session_context() as session:
async with get_user_db_context(session) as user_db:
async with get_user_manager_context(user_db) as user_manager:
user = await user_manager.create(
UserCreate(
email=email,
password=password,
username=username,
full_name=full_name,
is_superuser=is_superuser,
is_active=True
)
)
print(f"User created: {user}")
return user.id
except UserAlreadyExists:
print(f"User {email} already exists")
async with session.begin():
result_user = await session.execute(select(RbacUser).filter_by(email=email))
existing_user = result_user.scalars().first()
return existing_user.id

View File

@ -0,0 +1,76 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import schema as sa_schema
from app.database.models import Inquilino # Modelos `Inquilino`
from app.database.session import sessionmanager
from tenant import get_tenant_specific_metadata
from app.rbac.auth import get_user_db, get_user_manager
from app.rbac.schemas import UserCreate
from fastapi_users.exceptions import UserAlreadyExists
from sqlalchemy import select
from app.database.models import RbacUser
import contextlib
# Funções auxiliares para criação de usuários
get_async_session_context = contextlib.asynccontextmanager(sessionmanager.session)
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
async def create_user(email: str, password: str, is_superuser: bool = False, pessoa_uuid: str = None):
"""
Cria um usuário no sistema utilizando o gerenciador de usuários do FastAPI Users.
"""
async with get_async_session_context() as session:
async with get_user_db_context(session) as user_db:
async with get_user_manager_context(user_db) as user_manager:
try:
user = await user_manager.create(
UserCreate(
email=email,
password=password,
is_superuser=is_superuser,
is_active=True,
fk_pessoa_uuid=pessoa_uuid,
)
)
return user.id
except UserAlreadyExists:
print(f"Usuário {email} já existe")
result_user = await session.execute(select(RbacUser).filter_by(email=email))
existing_user = result_user.scalars().first()
return existing_user.id
async def tenant_create(nome: str, host: str, email: str, password: str) -> None:
"""
Cria um novo tenant (inquilino) no sistema, configura o schema específico
e registra um usuário inicial relacionado ao inquilino.
"""
async with sessionmanager.session() as db: # Obtendo uma sessão do gerenciador
# Criar o inquilino na tabela `inquilinos`
tenant = Inquilino(nome=nome)
db.add(tenant)
await db.commit() # Commit necessário para o UUID ser gerado pelo banco
# Obter o UUID gerado
await db.refresh(tenant)
schema_name = str(tenant.uuid)
# Criar o schema do inquilino
await db.execute(sa_schema.CreateSchema(schema_name))
# Criar as tabelas específicas do tenant no novo schema
tenant_metadata = get_tenant_specific_metadata()
await db.run_sync(tenant_metadata.create_all)
# Criar o usuário inicial para o tenant
user_id = await create_user(
email=email,
password=password,
is_superuser=True,
pessoa_uuid=None, # Ajustar caso seja necessário vincular com uma pessoa
)
print(f"Usuário inicial {email} criado com ID {user_id}")
print(f"Tenant '{nome}' criado com sucesso no schema '{schema_name}'!")

View File

@ -0,0 +1,214 @@
from app.database.models import RbacPermissao, RbacPapel
from app.database.session import sessionmanager
from sqlalchemy import select
async def add_or_update_entity(session, model, filters, data):
result = await session.execute(select(model).filter_by(**filters))
entity = result.scalars().first()
if not entity:
entity = model(**data)
session.add(entity)
else:
for key, value in data.items():
setattr(entity, key, value)
return entity
async def add_permissions_to_role(session, papel_data, permissoes_ids):
for permissao_id in permissoes_ids:
result_permissao = await session.execute(select(RbacPermissao).filter_by(id=permissao_id))
permissao_data = result_permissao.scalars().first()
if permissao_data and permissao_data not in papel_data.permissoes:
papel_data.permissoes.append(permissao_data)
for permissao in papel_data.permissoes:
if permissao.id not in permissoes_ids:
papel_data.permissoes.remove(permissao)
async def remove_unused_entities(session, model, valid_ids, id_field="id"):
result_all = await session.execute(select(model))
all_entities = result_all.scalars().all()
for entity in all_entities:
entity_id = str(getattr(entity, id_field))
if entity_id not in valid_ids:
await session.delete(entity)
await session.flush()
async def process_permissions(session, endpoint_permissao1):
ignored_permissions = []
for permissao in endpoint_permissao1:
if permissao["id"] is None or permissao["nome"] is None:
ignored_permissions.append(permissao)
continue
await add_or_update_entity(session, RbacPermissao, {"id": permissao["id"]}, permissao)
permissoes_ids = [str(permissao["id"]) for permissao in endpoint_permissao1]
await remove_unused_entities(session, RbacPermissao, permissoes_ids, id_field="id")
return ignored_permissions
async def process_roles(session, endpoint_papel):
ignored_roles = []
for papel in endpoint_papel:
if papel.get("nome") is None:
ignored_roles.append(papel)
continue
papel_data = await add_or_update_entity(session, RbacPapel, {"nome": papel["nome"]}, {"nome": papel["nome"]})
await session.flush()
await session.refresh(papel_data)
permissoes_ids = papel["permissoes"] if papel["permissoes"] is not None else []
await add_permissions_to_role(session, papel_data, permissoes_ids)
papeis_nomes = [papel["nome"] for papel in endpoint_papel]
await remove_unused_entities(session, RbacPapel, papeis_nomes, id_field="nome")
return ignored_roles
async def initialize_permissions():
async with sessionmanager.session() as session:
# Definindo as permissões
endpoint_permissao = [
{"id": 1, "nome": "Permissão Total"},
{"id": 2, "nome": "Permissão Setor Comercial"},
{"id": 3, "nome": "Permissão RBAC"},
{"id": 4, "nome": "Permissão Setor Estoque"},
{"id": 5, "nome": "Permissão Setor Financeiro"},
{"id": 30, "nome": "Permissão Relação Comercial"},
{"id": 31, "nome": "Permissão Tipo Endereço"},
{"id": 32, "nome": "Permissão Endereço"},
{"id": 33, "nome": "Permissão Pessoa"},
{"id": 34, "nome": "Permissão Usuários"},
{"id": 35, "nome": "Permissão Papel"},
{"id": 36, "nome": "Permissão Setor"},
{"id": 37, "nome": "Permissão Tipo Equipamento"},
{"id": 38, "nome": "Permissão Equipamento"},
{"id": 39, "nome": "Permissão Itens Equipamento"},
{"id": 40, "nome": "Permissão Manutenção Equipamento"},
{"id": 41, "nome": "Permissão Papeis"},
{"id": 101, "nome": "Relação Comercial Criar"},
{"id": 102, "nome": "Relação Comercial Criar Muitos"},
{"id": 103, "nome": "Relação Comercial Buscar Todos"},
{"id": 104, "nome": "Relação Comercial Buscar Vários"},
{"id": 105, "nome": "Relação Comercial Buscar"},
{"id": 106, "nome": "Relação Comercial Atualizar"},
{"id": 107, "nome": "Relação Comercial Atualizar Vários"},
{"id": 108, "nome": "Relação Comercial Desativar"},
{"id": 201, "nome": "Tipo Endereço Criar"},
{"id": 202, "nome": "Tipo Endereço Criar Muitos"},
{"id": 203, "nome": "Tipo Endereço Buscar Todos"},
{"id": 204, "nome": "Tipo Endereço Buscar Vários"},
{"id": 205, "nome": "Tipo Endereço Buscar"},
{"id": 206, "nome": "Tipo Endereço Atualizar"},
{"id": 207, "nome": "Tipo Endereço Atualizar Vários"},
{"id": 208, "nome": "Tipo Endereço Desativar"},
{"id": 301, "nome": "Endereço Criar"},
{"id": 302, "nome": "Endereço Criar Muitos"},
{"id": 303, "nome": "Endereço Buscar Todos"},
{"id": 304, "nome": "Endereço Buscar Vários"},
{"id": 305, "nome": "Endereço Buscar"},
{"id": 306, "nome": "Endereço Atualizar"},
{"id": 307, "nome": "Endereço Atualizar Vários"},
{"id": 308, "nome": "Endereço Desativar"},
{"id": 401, "nome": "Pessoa Criar"},
{"id": 402, "nome": "Pessoa Criar Muitos"},
{"id": 403, "nome": "Pessoa Buscar Todos"},
{"id": 404, "nome": "Pessoa Buscar Vários"},
{"id": 405, "nome": "Pessoa Buscar"},
{"id": 406, "nome": "Pessoa Atualizar"},
{"id": 408, "nome": "Pessoa Desativar"},
{"id": 501, "nome": "Usuário Criar"},
{"id": 502, "nome": "Usuário Criar Muitos"},
{"id": 503, "nome": "Usuário Buscar Todos"},
{"id": 504, "nome": "Usuário Buscar Vários"},
{"id": 505, "nome": "Usuário Buscar"},
{"id": 506, "nome": "Usuário Atualizar"},
{"id": 507, "nome": "Usuário Atualizar Vários"},
{"id": 508, "nome": "Usuário Desativar"},
{"id": 601, "nome": "Papel Criar"},
{"id": 602, "nome": "Papel Criar Muitos"},
{"id": 603, "nome": "Papel Buscar Todos"},
{"id": 604, "nome": "Papel Buscar Vários"},
{"id": 605, "nome": "Papel Buscar"},
{"id": 606, "nome": "Papel Atualizar"},
{"id": 607, "nome": "Papel Atualizar Vários"},
{"id": 608, "nome": "Papel Desativar"},
{"id": 701, "nome": "Setor Criar"},
{"id": 702, "nome": "Setpr Criar Muitos"},
{"id": 703, "nome": "Setor Buscar Todos"},
{"id": 704, "nome": "Setor Buscar Vários"},
{"id": 705, "nome": "Setor Buscar"},
{"id": 706, "nome": "Setor Atualizar"},
{"id": 707, "nome": "Setor Atualizar Vários"},
{"id": 708, "nome": "Setor Desativar"},
{"id": 801, "nome": "Tipo Equipamento Criar"},
{"id": 802, "nome": "Tipo Equipamento Criar Muitos"},
{"id": 803, "nome": "Tipo Equipamento Buscar Todos"},
{"id": 804, "nome": "Tipo Equipamento Buscar Vários"},
{"id": 805, "nome": "Tipo Equipamento Buscar"},
{"id": 806, "nome": "Tipo Equipamento Atualizar"},
{"id": 807, "nome": "Tipo Equipamento Atualizar Vários"},
{"id": 808, "nome": "Tipo Equipamento Desativar"},
{"id": 901, "nome": "Equipamento Criar"},
{"id": 902, "nome": "Equipamento Criar Muitos"},
{"id": 903, "nome": "Equipamento Buscar Todos"},
{"id": 904, "nome": "Equipamento Buscar Vários"},
{"id": 905, "nome": "Equipamento Buscar"},
{"id": 906, "nome": "Equipamento Atualizar"},
{"id": 907, "nome": "Equipamento Atualizar Vários"},
{"id": 908, "nome": "Equipamento Desativar"},
{"id": 1001, "nome": "Itens Equipamento Criar"},
{"id": 1002, "nome": "Itens Equipamento Criar Muitos"},
{"id": 1003, "nome": "Itens Equipamento Buscar Todos"},
{"id": 1004, "nome": "Itens Equipamento Buscar Vários"},
{"id": 1005, "nome": "Itens Equipamento Buscar"},
{"id": 1006, "nome": "Itens Equipamento Atualizar"},
{"id": 1007, "nome": "Itens Equipamento Atualizar Vários"},
{"id": 1008, "nome": "Itens Equipamento Desativar"},
{"id": 1101, "nome": "Manutenção Equipamento Criar"},
{"id": 1102, "nome": "Manutenção Equipamento Criar Muitos"},
{"id": 1103, "nome": "Manutenção Equipamento Buscar Todos"},
{"id": 1104, "nome": "Manutenção Equipamento Buscar Vários"},
{"id": 1105, "nome": "Manutenção Equipamento Buscar"},
{"id": 1106, "nome": "Manutenção Equipamento Atualizar"},
{"id": 1107, "nome": "Manutenção Equipamento Atualizar Vários"},
{"id": 1108, "nome": "Manutenção Equipamento Desativar"},
{"id": 1201, "nome": "Papeis Criar"},
{"id": 1202, "nome": "Papeis Criar Muitos"},
{"id": 1203, "nome": "Papeis Buscar Todos"},
{"id": 1204, "nome": "Papeis Buscar Vários"},
{"id": 1205, "nome": "Papeis Buscar"},
{"id": 1206, "nome": "Papeis Atualizar"},
{"id": 1207, "nome": "Papeis Atualizar Vários"},
{"id": 1208, "nome": "Papeis Desativar"},
]
# Definindo os papéis
endpoint_papel = [
{"nome": "Super Administrador", "permissoes": [1]},
# Outros papéis aqui
]
# Processamento das permissões e papéis
ignored_permissions = await process_permissions(session, endpoint_permissao)
ignored_roles = await process_roles(session, endpoint_papel)
await session.commit()
if not ignored_permissions and not ignored_roles:
print("Permissões e papéis inicializados com sucesso")
else:
if ignored_permissions:
print("Aviso: As seguintes permissões foram ignoradas devido a campos None:")
for permissao in ignored_permissions:
print(permissao)
if ignored_roles:
print("Aviso: Os seguintes papéis foram ignorados devido a campos None:")
for papel in ignored_roles:
print(papel)

View File

@ -0,0 +1,231 @@
from app.database.models import RbacPermissao, RbacPapel, RbacUser
from app.database.session import sessionmanager
from sqlalchemy import select
async def add_or_update_entity(session, model, filters, data):
result = await session.execute(select(model).filter_by(**filters))
entity = result.scalars().first()
if not entity:
entity = model(**data)
session.add(entity)
else:
for key, value in data.items():
setattr(entity, key, value)
return entity
async def add_permissions_to_role(session, papel_data, permissoes_ids):
for permissao_id in permissoes_ids:
result_permissao = await session.execute(select(RbacPermissao).filter_by(id=permissao_id))
permissao_data = result_permissao.scalars().first()
if permissao_data and permissao_data not in papel_data.permissoes:
papel_data.permissoes.append(permissao_data)
for permissao in papel_data.permissoes:
if permissao.id not in permissoes_ids:
papel_data.permissoes.remove(permissao)
async def remove_unused_entities(session, model, valid_ids, id_field="id"):
result_all = await session.execute(select(model))
all_entities = result_all.scalars().all()
for entity in all_entities:
entity_id = str(getattr(entity, id_field))
if entity_id not in valid_ids:
await session.delete(entity)
await session.flush()
async def process_permissions(session, endpoint_permissao1):
ignored_permissions = []
for permissao in endpoint_permissao1:
if permissao["id"] is None or permissao["nome"] is None:
ignored_permissions.append(permissao)
continue
await add_or_update_entity(session, RbacPermissao, {"id": permissao["id"]}, permissao)
permissoes_ids = [str(permissao["id"]) for permissao in endpoint_permissao1]
await remove_unused_entities(session, RbacPermissao, permissoes_ids, id_field="id")
return ignored_permissions
async def process_roles(session, endpoint_papel, user_id):
ignored_roles = []
for papel in endpoint_papel:
if papel.get("nome") is None:
ignored_roles.append(papel)
continue
papel_data = await add_or_update_entity(session, RbacPapel, {"nome": papel["nome"]}, {"nome": papel["nome"]})
await session.flush()
await session.refresh(papel_data)
permissoes_ids = papel["permissoes"] if papel["permissoes"] is not None else []
await add_permissions_to_role(session, papel_data, permissoes_ids)
# Relacionando o papel ao usuário
result_user = await session.execute(select(RbacUser).filter_by(id=user_id))
user = result_user.scalars().first()
if papel_data not in user.papeis:
user.papeis.append(papel_data)
papeis_nomes = [papel["nome"] for papel in endpoint_papel]
await remove_unused_entities(session, RbacPapel, papeis_nomes, id_field="nome")
return ignored_roles
async def initialize_permissions_roles(user_id):
async with sessionmanager.session() as session:
# Definindo as permissões
endpoint_permissao1 = [
{"id": 1, "nome": "Permissão Total"},
{"id": 2, "nome": "Permissão Setor Comercial"},
{"id": 3, "nome": "Permissão RBAC"},
{"id": 4, "nome": "Permissão Setor Estoque"},
{"id": 5, "nome": "Permissão Setor Financeiro"},
{"id": 30, "nome": "Permissão Relação Comercial"},
{"id": 31, "nome": "Permissão Tipo Endereço"},
{"id": 32, "nome": "Permissão Endereço"},
{"id": 33, "nome": "Permissão Pessoa"},
{"id": 34, "nome": "Permissão Usuários"},
{"id": 35, "nome": "Permissão Papel"},
{"id": 36, "nome": "Permissão Setor"},
{"id": 37, "nome": "Permissão Tipo Equipamento"},
{"id": 38, "nome": "Permissão Equipamento"},
{"id": 39, "nome": "Permissão Itens Equipamento"},
{"id": 40, "nome": "Permissão Manutenção Equipamento"},
{"id": 41, "nome": "Permissão Papeis"},
{"id": 101, "nome": "Relação Comercial Criar"},
{"id": 102, "nome": "Relação Comercial Criar Muitos"},
{"id": 103, "nome": "Relação Comercial Buscar Todos"},
{"id": 104, "nome": "Relação Comercial Buscar Vários"},
{"id": 105, "nome": "Relação Comercial Buscar"},
{"id": 106, "nome": "Relação Comercial Atualizar"},
{"id": 107, "nome": "Relação Comercial Atualizar Vários"},
{"id": 108, "nome": "Relação Comercial Apagar"},
{"id": 109, "nome": "Relação Comercial Apagar Vários"},
{"id": 201, "nome": "Tipo Endereço Criar"},
{"id": 202, "nome": "Tipo Endereço Criar Muitos"},
{"id": 203, "nome": "Tipo Endereço Buscar Todos"},
{"id": 204, "nome": "Tipo Endereço Buscar Vários"},
{"id": 205, "nome": "Tipo Endereço Buscar"},
{"id": 206, "nome": "Tipo Endereço Atualizar"},
{"id": 207, "nome": "Tipo Endereço Atualizar Vários"},
{"id": 208, "nome": "Tipo Endereço Apagar"},
{"id": 209, "nome": "Tipo Endereço Apagar Vários"},
{"id": 301, "nome": "Endereço Criar"},
{"id": 302, "nome": "Endereço Criar Muitos"},
{"id": 303, "nome": "Endereço Buscar Todos"},
{"id": 304, "nome": "Endereço Buscar Vários"},
{"id": 305, "nome": "Endereço Buscar"},
{"id": 306, "nome": "Endereço Atualizar"},
{"id": 307, "nome": "Endereço Atualizar Vários"},
{"id": 308, "nome": "Endereço Apagar"},
{"id": 309, "nome": "Endereço Apagar Vários"},
{"id": 401, "nome": "Pessoa Criar"},
{"id": 402, "nome": "Pessoa Criar Muitos"},
{"id": 403, "nome": "Pessoa Buscar Todos"},
{"id": 404, "nome": "Pessoa Buscar Vários"},
{"id": 405, "nome": "Pessoa Buscar"},
{"id": 406, "nome": "Pessoa Atualizar"},
{"id": 408, "nome": "Pessoa Apagar"},
{"id": 409, "nome": "Pessoa Apagar Vários"},
{"id": 501, "nome": "Usuário Criar"},
{"id": 502, "nome": "Usuário Criar Muitos"},
{"id": 503, "nome": "Usuário Buscar Todos"},
{"id": 504, "nome": "Usuário Buscar Vários"},
{"id": 505, "nome": "Usuário Buscar"},
{"id": 506, "nome": "Usuário Atualizar"},
{"id": 507, "nome": "Usuário Atualizar Vários"},
{"id": 508, "nome": "Usuário Apagar"},
{"id": 509, "nome": "Usuário Apagar Vários"},
{"id": 601, "nome": "Papel Criar"},
{"id": 602, "nome": "Papel Criar Muitos"},
{"id": 603, "nome": "Papel Buscar Todos"},
{"id": 604, "nome": "Papel Buscar Vários"},
{"id": 605, "nome": "Papel Buscar"},
{"id": 606, "nome": "Papel Atualizar"},
{"id": 607, "nome": "Papel Atualizar Vários"},
{"id": 608, "nome": "Papel Apagar"},
{"id": 609, "nome": "Papel Apagar Vários"},
{"id": 701, "nome": "Setor Criar"},
{"id": 702, "nome": "Setpr Criar Muitos"},
{"id": 703, "nome": "Setor Buscar Todos"},
{"id": 704, "nome": "Setor Buscar Vários"},
{"id": 705, "nome": "Setor Buscar"},
{"id": 706, "nome": "Setor Atualizar"},
{"id": 707, "nome": "Setor Atualizar Vários"},
{"id": 708, "nome": "Setor Apagar"},
{"id": 709, "nome": "Setor Apagar Vários"},
{"id": 801, "nome": "Tipo Equipamento Criar"},
{"id": 802, "nome": "Tipo Equipamento Criar Muitos"},
{"id": 803, "nome": "Tipo Equipamento Buscar Todos"},
{"id": 804, "nome": "Tipo Equipamento Buscar Vários"},
{"id": 805, "nome": "Tipo Equipamento Buscar"},
{"id": 806, "nome": "Tipo Equipamento Atualizar"},
{"id": 807, "nome": "Tipo Equipamento Atualizar Vários"},
{"id": 808, "nome": "Tipo Equipamento Apagar"},
{"id": 809, "nome": "Tipo Equipamento Apagar Vários"},
{"id": 901, "nome": "Equipamento Criar"},
{"id": 902, "nome": "Equipamento Criar Muitos"},
{"id": 903, "nome": "Equipamento Buscar Todos"},
{"id": 904, "nome": "Equipamento Buscar Vários"},
{"id": 905, "nome": "Equipamento Buscar"},
{"id": 906, "nome": "Equipamento Atualizar"},
{"id": 907, "nome": "Equipamento Atualizar Vários"},
{"id": 908, "nome": "Equipamento Apagar"},
{"id": 909, "nome": "Equipamento Apagar Vários"},
{"id": 1001, "nome": "Itens Equipamento Criar"},
{"id": 1002, "nome": "Itens Equipamento Criar Muitos"},
{"id": 1003, "nome": "Itens Equipamento Buscar Todos"},
{"id": 1004, "nome": "Itens Equipamento Buscar Vários"},
{"id": 1005, "nome": "Itens Equipamento Buscar"},
{"id": 1006, "nome": "Itens Equipamento Atualizar"},
{"id": 1007, "nome": "Itens Equipamento Atualizar Vários"},
{"id": 1008, "nome": "Itens Equipamento Apagar"},
{"id": 1009, "nome": "Itens Equipamento Apagar Vários"},
{"id": 1101, "nome": "Manutenção Equipamento Criar"},
{"id": 1102, "nome": "Manutenção Equipamento Criar Muitos"},
{"id": 1103, "nome": "Manutenção Equipamento Buscar Todos"},
{"id": 1104, "nome": "Manutenção Equipamento Buscar Vários"},
{"id": 1105, "nome": "Manutenção Equipamento Buscar"},
{"id": 1106, "nome": "Manutenção Equipamento Atualizar"},
{"id": 1107, "nome": "Manutenção Equipamento Atualizar Vários"},
{"id": 1108, "nome": "Manutenção Equipamento Apagar"},
{"id": 1109, "nome": "Manutenção Equipamento Apagar Vários"},
{"id": 1201, "nome": "Papeis Criar"},
{"id": 1202, "nome": "Papeis Criar Muitos"},
{"id": 1203, "nome": "Papeis Buscar Todos"},
{"id": 1204, "nome": "Papeis Buscar Vários"},
{"id": 1205, "nome": "Papeis Buscar"},
{"id": 1206, "nome": "Papeis Atualizar"},
{"id": 1207, "nome": "Papeis Atualizar Vários"},
{"id": 1208, "nome": "Papeis Apagar"},
{"id": 1209, "nome": "Papeis Apagar Vários"},
]
# Definindo os papéis
endpoint_papel = [
{"nome": "Super Administrador", "permissoes": [1]},
# Outros papéis aqui
]
# Processamento das permissões e papéis
ignored_permissions = await process_permissions(session, endpoint_permissao1)
ignored_roles = await process_roles(session, endpoint_papel, user_id)
await session.commit()
if not ignored_permissions and not ignored_roles:
print("Permissões e papéis inicializados com sucesso")
else:
if ignored_permissions:
print("Aviso: As seguintes permissões foram ignoradas devido a campos None:")
for permissao in ignored_permissions:
print(permissao)
if ignored_roles:
print("Aviso: Os seguintes papéis foram ignorados devido a campos None:")
for papel in ignored_roles:
print(papel)

View File

@ -0,0 +1,60 @@
from app.database.models import ComercialFisica
from app.database.session import sessionmanager
from sqlalchemy import select
from sqlalchemy.orm import selectinload
async def add_or_update_person(session, model, filters, data):
print("Executando add_or_update_person...")
# Usando selectinload para evitar carregamento preguiçoso inesperado
result = await session.execute(
select(model).options(selectinload("*")).filter_by(**filters)
)
entity = result.scalars().first()
if not entity:
print("Nenhuma pessoa encontrada com os filtros fornecidos. Criando nova pessoa.")
entity = model(**data)
session.add(entity)
else:
print("Pessoa já existente encontrada. Atualizando informações.")
for key, value in data.items():
setattr(entity, key, value)
return entity
async def initialize_person():
print("Iniciando processo de criação/atualização de pessoa...")
async with sessionmanager.session() as session:
# Dados da pessoa
pessoa_data = {
"pessoa_telefone": "00000000000", # Exemplo de telefone
"pessoa_celular": "00000000000", # Exemplo de celular
"pessoa_email": "admin@sonora.com", # Exemplo de email
"pessoa_status": True,
"pessoa_tipo": "1", # Tipo especificado
"fisica_nome": "Admin", # Nome da pessoa
"fisica_cpf": "00000000000", # Exemplo de CPF
"fisica_genero": "O", # Gênero
"fisica_rg": "1.234.567", # Exemplo de RG
}
print("Dados da pessoa para criação/atualização:", pessoa_data)
# Criando ou atualizando a pessoa
pessoa = await add_or_update_person(
session, ComercialFisica, {"pessoa_email": pessoa_data["pessoa_email"]}, pessoa_data
)
print("Pessoa criada ou atualizada, realizando commit...")
# Confirmando alterações no banco
await session.commit()
# Garantindo que os dados estão totalmente carregados
await session.refresh(pessoa)
# Retornando o UUID da pessoa
print(f"Pessoa criada com UUID: {pessoa.uuid}")
return pessoa

View File

@ -0,0 +1,48 @@
"""
FIN_TIPO_PAGAMENTO
Tabela que armazena os tipos de pagamento: DINHEIRO, CARTÃO, CHEQUE, etc.
Tipos padrões cadastrados pelo sistema para toda empresa:
01 = Dinheiro
02 = Cheque
03 = Cartao
04 = Boleto
05 = Transferência Bancária
06 = PIX
"""
"""
FIN_STATUS_PARCELA
Tabela que armazena as possíveis situações de uma parcela. Status padrões:
01 = Aberto
02 = Quitado
03 = Quitado Parcial
04 = Vencido
05 = Renegociado
"""
"""
FIN_DOCUMENTO_ORIGEM
Tabela para cadastro dos tipo de documentos que podem gerar contas a pagar ou receber:
O campo SIGLA pode receber valores tais como: NF, CHQ, NFe, DP, NP, CTe, CT, CF, CFe.
O campo DESCRICAO pode receber valores tais como: NOTA FISCAL | BOLETO | RECIBO | ETC.
01 = NF - Nota Fiscal
02 = NFS - Nota Fiscal de Serviço
03 = FL - Fatura de Locação
"""
"""
FIN_TIPO_RECEBIMENTO
Tabela que armazena os tipos de recebimento: DINHEIRO, CARTÃO, CHEQUE, etc.
Tipos padrões cadastrados pelo sistema para toda empresa:
01 = Dinheiro
02 = Cheque
03 = Cartão
04 = Boleto
05 = PIX
"""

319
assets/style.css Normal file
View File

@ -0,0 +1,319 @@
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
/* do not increase min-width as some may use split screens */
min-width: 800px;
color: #999;
}
h1 {
font-size: 24px;
color: black;
}
h2 {
font-size: 16px;
color: black;
}
p {
color: black;
}
a {
color: #999;
}
table {
border-collapse: collapse;
}
/******************************
* SUMMARY INFORMATION
******************************/
#environment td {
padding: 5px;
border: 1px solid #e6e6e6;
vertical-align: top;
}
#environment tr:nth-child(odd) {
background-color: #f6f6f6;
}
#environment ul {
margin: 0;
padding: 0 20px;
}
/******************************
* TEST RESULT COLORS
******************************/
span.passed,
.passed .col-result {
color: green;
}
span.skipped,
span.xfailed,
span.rerun,
.skipped .col-result,
.xfailed .col-result,
.rerun .col-result {
color: orange;
}
span.error,
span.failed,
span.xpassed,
.error .col-result,
.failed .col-result,
.xpassed .col-result {
color: red;
}
.col-links__extra {
margin-right: 3px;
}
/******************************
* RESULTS TABLE
*
* 1. Table Layout
* 2. Extra
* 3. Sorting items
*
******************************/
/*------------------
* 1. Table Layout
*------------------*/
#results-table {
border: 1px solid #e6e6e6;
color: #999;
font-size: 12px;
width: 100%;
}
#results-table th,
#results-table td {
padding: 5px;
border: 1px solid #e6e6e6;
text-align: left;
}
#results-table th {
font-weight: bold;
}
/*------------------
* 2. Extra
*------------------*/
.logwrapper {
max-height: 230px;
overflow-y: scroll;
background-color: #e6e6e6;
}
.logwrapper.expanded {
max-height: none;
}
.logwrapper.expanded .logexpander:after {
content: "collapse [-]";
}
.logwrapper .logexpander {
z-index: 1;
position: sticky;
top: 10px;
width: max-content;
border: 1px solid;
border-radius: 3px;
padding: 5px 7px;
margin: 10px 0 10px calc(100% - 80px);
cursor: pointer;
background-color: #e6e6e6;
}
.logwrapper .logexpander:after {
content: "expand [+]";
}
.logwrapper .logexpander:hover {
color: #000;
border-color: #000;
}
.logwrapper .log {
min-height: 40px;
position: relative;
top: -50px;
height: calc(100% + 50px);
border: 1px solid #e6e6e6;
color: black;
display: block;
font-family: "Courier New", Courier, monospace;
padding: 5px;
padding-right: 80px;
white-space: pre-wrap;
}
div.media {
border: 1px solid #e6e6e6;
float: right;
height: 240px;
margin: 0 5px;
overflow: hidden;
width: 320px;
}
.media-container {
display: grid;
grid-template-columns: 25px auto 25px;
align-items: center;
flex: 1 1;
overflow: hidden;
height: 200px;
}
.media-container--fullscreen {
grid-template-columns: 0px auto 0px;
}
.media-container__nav--right,
.media-container__nav--left {
text-align: center;
cursor: pointer;
}
.media-container__viewport {
cursor: pointer;
text-align: center;
height: inherit;
}
.media-container__viewport img,
.media-container__viewport video {
object-fit: cover;
width: 100%;
max-height: 100%;
}
.media__name,
.media__counter {
display: flex;
flex-direction: row;
justify-content: space-around;
flex: 0 0 25px;
align-items: center;
}
.collapsible td:not(.col-links) {
cursor: pointer;
}
.collapsible td:not(.col-links):hover::after {
color: #bbb;
font-style: italic;
cursor: pointer;
}
.col-result {
width: 130px;
}
.col-result:hover::after {
content: " (hide details)";
}
.col-result.collapsed:hover::after {
content: " (show details)";
}
#environment-header h2:hover::after {
content: " (hide details)";
color: #bbb;
font-style: italic;
cursor: pointer;
font-size: 12px;
}
#environment-header.collapsed h2:hover::after {
content: " (show details)";
color: #bbb;
font-style: italic;
cursor: pointer;
font-size: 12px;
}
/*------------------
* 3. Sorting items
*------------------*/
.sortable {
cursor: pointer;
}
.sortable.desc:after {
content: " ";
position: relative;
left: 5px;
bottom: -12.5px;
border: 10px solid #4caf50;
border-bottom: 0;
border-left-color: transparent;
border-right-color: transparent;
}
.sortable.asc:after {
content: " ";
position: relative;
left: 5px;
bottom: 12.5px;
border: 10px solid #4caf50;
border-top: 0;
border-left-color: transparent;
border-right-color: transparent;
}
.hidden, .summary__reload__button.hidden {
display: none;
}
.summary__data {
flex: 0 0 550px;
}
.summary__reload {
flex: 1 1;
display: flex;
justify-content: center;
}
.summary__reload__button {
flex: 0 0 300px;
display: flex;
color: white;
font-weight: bold;
background-color: #4caf50;
text-align: center;
justify-content: center;
align-items: center;
border-radius: 3px;
cursor: pointer;
}
.summary__reload__button:hover {
background-color: #46a049;
}
.summary__spacer {
flex: 0 0 550px;
}
.controls {
display: flex;
justify-content: space-between;
}
.filters,
.collapse {
display: flex;
align-items: center;
}
.filters button,
.collapse button {
color: #999;
border: none;
background: none;
cursor: pointer;
text-decoration: underline;
}
.filters button:hover,
.collapse button:hover {
color: #ccc;
}
.filter__label {
margin-right: 10px;
}

View File

@ -0,0 +1,43 @@
import asyncio
import subprocess
from app.database.session import sessionmanager
from app.config import URL_BD
async def atualizar_todos_inquilinos():
# Inicializar o gerenciador de sessão
sessionmanager.init(URL_BD)
try:
# Buscar a lista de esquemas (nomes dos inquilinos)
async with sessionmanager.session() as session:
result = await session.execute(
"""
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name NOT IN ('public', 'information_schema', 'pg_catalog', 'shared', 'pg_toast_temp_1',
'pg_temp_1', 'pg_toast');
"""
)
tenants = [row[0] for row in result.fetchall()]
print(f"Encontrados {len(tenants)} inquilinos para atualizar: {tenants}")
for tenant in tenants:
print(f"Iniciando migração para o inquilino: {tenant}")
try:
subprocess.run(
["alembic", "-x", f"tenant={tenant}", "upgrade", "head"],
check=True, capture_output=True, text=True
)
print(f"Migração para o inquilino '{tenant}' concluída com sucesso.")
except subprocess.CalledProcessError as e:
print(f"Erro durante migração para o inquilino '{tenant}':")
print("Saída padrão (stdout):", e.stdout)
print("Erro padrão (stderr):", e.stderr)
print("Código de saída:", e.returncode)
finally:
await sessionmanager.close()
if __name__ == "__main__":
asyncio.run(atualizar_todos_inquilinos())

55
check_db.py Normal file
View File

@ -0,0 +1,55 @@
# check_db.py
import os
import sys
import time
import asyncio
from urllib.parse import urlparse
import asyncpg
async def check_database_connection():
DATABASE_URL = os.getenv("URL_BD")
if not DATABASE_URL:
print("Erro: Variável de ambiente URL_BD não definida.", file=sys.stderr)
sys.exit(1)
# Analisar a URL para obter os componentes
parsed_url = urlparse(DATABASE_URL)
DB_HOST = parsed_url.hostname
DB_PORT = parsed_url.port or 5432 # Default PostgreSQL port
DB_USER = parsed_url.username
DB_PASSWORD = parsed_url.password
DB_NAME = parsed_url.path.lstrip('/')
print(f"Tentando conectar ao banco de dados em {DB_HOST}:{DB_PORT}/{DB_NAME} como {DB_USER}...")
max_retries = 30 # Tentar por até 30 segundos (30 * 1s sleep)
retry_interval = 1
for i in range(max_retries):
try:
# Tentar conectar usando asyncpg
conn = await asyncpg.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD,
database=DB_NAME,
timeout=5 # Timeout para a tentativa de conexão
)
await conn.close()
print("Conexão com o banco de dados estabelecida com sucesso!")
sys.exit(0) # Sucesso!
except asyncpg.exceptions.PostgresError as e: # Captura erros específicos do asyncpg
print(f"Erro de conexão com o banco de dados (tentativa {i+1}/{max_retries}): {e}", file=sys.stderr)
await asyncio.sleep(retry_interval) # Usar await asyncio.sleep para sleep assíncrono
except Exception as e: # Captura outros erros inesperados
print(f"Erro inesperado durante a conexão (tentativa {i+1}/{max_retries}): {e}", file=sys.stderr)
await asyncio.sleep(retry_interval)
print("Falha ao conectar ao banco de dados após várias tentativas.", file=sys.stderr)
sys.exit(1) # Falha
if __name__ == "__main__":
asyncio.run(check_database_connection()) # Executa a função assíncrona

20
docker.txt Normal file
View File

@ -0,0 +1,20 @@
ir para a raiz do projeto docker login registry.sonoraav.com.br
comando 1 : docker build -t registry.sonoraav.com.br/back-end-evento-app:latest .
comando 2 : $VERSION = '0.0.6'
comando 3 : docker tag `
registry.sonoraav.com.br/back-end-evento-app:latest `
"registry.sonoraav.com.br/back-end-evento-app:$VERSION"
comando 4 : docker push "registry.sonoraav.com.br/back-end-evento-app:$VERSION"
comando 5 : docker push registry.sonoraav.com.br/back-end-evento-app:latest
comando 6 : curl.exe -k -u registry:Sonora@2015 `
https://registry.sonoraav.com.br/v2/_catalog `
-UseBasicParsing
comando 7 : curl.exe -k -u registry:Sonora@2015 `
https://registry.sonoraav.com.br/v2/back-end-evento-app/tags/list `
-UseBasicParsing
chegar no código
docker ps --filter "name=fastapi"
docker exec -it NAME sh

6
iniciar.txt Normal file
View File

@ -0,0 +1,6 @@
Para Criar o Banco de Dados>
- Primeiro Comando
- alembic init -t async alembic
- Com o models configurado
- alembic revision --autogenerate -m "Adding user model"
- alembic upgrade head

14
iniciar_multi_tenant.txt Normal file
View File

@ -0,0 +1,14 @@
Executar arquivo iniciar_pemissoes_e_papeis.py
- Ele vai criar o esquema Shared
- Ele vai criar o esquema modelo de inquilinos
- Por fim vai cadastar as permissões e o papel de Super Usuário
Criação de um Novo Inquilino
- Executar o script de configuração passando os parametros para cadastro do novo cliente:
python novo_inquilino.py --nome 'no cliente' --email "email" --password "Senha" --doc "CPF/CNPJ"
Atualizar Banco de Dados
- Chegar a Migração
alembic -x tenant=default_tenant revision -m "nome migracao" --autogenerate
Atualizar Shared
alembic -x special_schema=shared revision -m "Initial Migration Shared" --autogenerate

View File

@ -0,0 +1,50 @@
import asyncio
import subprocess
from app.scripts.initialize_permissions import initialize_permissions # Importe a função correta
from app.database.session import sessionmanager
from app.config import URL_BD
def alembic_upgrade():
try:
print("Iniciando Migrações Alembic das Tabelas Compartilhadas")
shared = subprocess.run(
["alembic", "-x", "special_schema=shared", "upgrade", "head"],
check=True, capture_output=True, text=True)
print("Migração Alembic das Tabelas Compartilhadas finalizado com sucesso.")
print(shared.stdout)
print("Iniciando Migrações Alembic das Tabelas Modelo dos Inquilinos")
default_tenant = subprocess.run(["alembic", "-x", "tenant=default_tenant",
"upgrade", "head"], check=True, capture_output=True, text=True)
print("Migração Alembic das Tabelas Modelo dos Inquilinos finalizado com sucesso.")
print(default_tenant.stdout)
except subprocess.CalledProcessError as e:
print("Erro durante Migrações Alembic.")
print("Erro na Migração Alembic:")
print("Saída padrão (stdout):", e.stdout)
print("Erro padrão (stderr):", e.stderr)
print("Código de saída:", e.returncode)
raise
async def main():
try:
alembic_upgrade()
except Exception as e:
print(f"Erro na Migração Alembic: {e}")
return
# Inicializar o gerenciador de sessão
sessionmanager.init(URL_BD)
try:
# Executar a função para inicializar permissões e papéis
await initialize_permissions()
finally:
# Fechar o gerenciador de sessão
await sessionmanager.close()
if __name__ == "__main__":
# Rodar o script assíncrono
asyncio.run(main())

45
novo_inquilino.py Normal file
View File

@ -0,0 +1,45 @@
import asyncio
import subprocess
from app.multi_tenant.criar_tenant import tenant_create
from app.database.session import sessionmanager
from app.config import URL_BD
async def main(nome: str, email: str, password: str, cpf_cnpj: str):
# Inicializar o gerenciador de sessão
sessionmanager.init(URL_BD)
cliente = None
try:
print(f"Iniciando a configuração de um novo Cliente '{nome}'...")
cliente = await tenant_create(nome=nome, email=email, password=password, cpf_cnpj=cpf_cnpj)
print(f"Cliente '{nome}' criado com sucesso!")
except Exception as e:
print(f"Erro ao criar o tenant: {e}")
finally:
await sessionmanager.close()
try:
print("Iniciando Migrações Alembic das Tabelas do Cliente")
default_tenant = subprocess.run(["alembic", "-x", f"tenant={cliente}", "upgrade", "head"],
check=True, capture_output=True, text=True)
print("Migração Alembic das Tabelas Modelo dos Inquilinos finalizado com sucesso.")
print(default_tenant.stdout)
except subprocess.CalledProcessError as e:
print("Erro durante Migrações Alembic.")
print("Erro na Migração Alembic:")
print("Saída padrão (stdout):", e.stdout)
print("Erro padrão (stderr):", e.stderr)
print("Código de saída:", e.returncode)
raise
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Criar novo tenant")
parser.add_argument("--nome", required=True, help="Nome do Cliente")
parser.add_argument("--email", required=True, help="Email do usuário inicial")
parser.add_argument("--password", required=True, help="Senha do usuário inicial")
parser.add_argument("--doc", required=True, help="CPF ou CNPJ")
args = parser.parse_args()
asyncio.run(main(nome=args.nome, email=args.email, password=args.password, cpf_cnpj=args.doc,))

2014
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

36
pyproject.toml Normal file
View File

@ -0,0 +1,36 @@
[tool.poetry]
name = "adminuuidpostgresql"
version = "0.1.0"
description = ""
authors = ["RicardoJDaleprane <ricardo.daleprane@gmail.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.112.2"
uvicorn = {extras = ["standard"], version = "^0.30.6"}
sqlalchemy = "^2.0.32"
alembic = "^1.13.2"
fastapi-users = "^13.0.0"
fastapi-users-db-sqlalchemy = "^6.0.1"
uuid6 = "^2024.7.10"
asyncpg = "^0.29.0"
typeguard = "^4.4.1"
fastapi-cli = "^0.0.5"
boto3 = "^1.35.90"
[tool.poetry.group.dev.dependencies]
httpx = {extras = ["http2"], version = "^0.27.2"}
pytest = "^8.3.2"
pytest-asyncio = "^0.24.0"
asyncio = "^3.4.3"
pytest-cov = "^5.0.0"
pytest-html = "^4.1.1"
trio = "^0.26.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

5
pytest.ini Normal file
View File

@ -0,0 +1,5 @@
[pytest]
env = PYTHONPATH=.
addopts = -p no:warnings
asyncio_default_fixture_loop_scope = function
markers = usuarios_permitidos(users): Marcador para definir os usuários permitidos em um teste

31
start.sh Normal file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# --- 1. Verificação de Conectividade com o Banco de Dados ---
# Este passo garante que o PostgreSQL está acessível antes de qualquer operação que dependa dele.
# O script 'check_db.py' é executado e, em caso de falha, o processo é encerrado.
echo "Running database readiness check..."
python /code/check_db.py
# Verifica o código de saída do script Python. Se for diferente de 0 (falha), o bash sai.
if [ $? -ne 0 ]; then
echo "Database readiness check failed. Exiting."
exit 1
fi
echo "Database is ready!"
# --- 2. Executar Script de Inicialização de Dados ---
# Executa o script Python que insere permissões e papéis iniciais no banco de dados.
# Este script também deve usar a variável de ambiente URL_BD para se conectar.
echo "Executing initial permissions and roles script..."
python /code/iniciar_permissoes_e_papeis.py
echo "Permissions and roles script completed!"
# --- 3. Iniciar a Aplicação FastAPI ---
# Após garantir que o banco de dados está pronto, migrado e com dados iniciais,
# a aplicação FastAPI é iniciada usando a CLI e suas configurações de porta e workers.
echo "Starting FastAPI application using 'fastapi run'..."
fastapi run app/main.py --port 80 --workers 4

11
test_main.http Normal file
View File

@ -0,0 +1,11 @@
# Test your FastAPI endpoints
GET http://127.0.0.1:8000/
Accept: application/json
###
GET http://127.0.0.1:8000/hello/User
Accept: application/json
###

8
teste.txt Normal file
View File

@ -0,0 +1,8 @@
DELETE FROM public.pessoa
WHERE "uuid"='018ffdd5-8ff3-724f-834f-c2640acce13d'::uuid::uuid;
DELETE FROM public.pessoa
WHERE "uuid"='018ffdd6-3171-7474-a121-ad2822e3649b'::uuid::uuid;
DELETE FROM public.pessoa
WHERE "uuid"='018ffdd7-d216-7501-8364-17881011681a'::uuid::uuid;
DELETE FROM public.pessoa
WHERE "uuid"='018ffdd8-2c96-7116-a231-9de5cea3074d'::uuid::uuid;

3
tests/__init__.py Normal file
View File

@ -0,0 +1,3 @@

6
tests/_test_client.py Normal file
View File

@ -0,0 +1,6 @@
# _test_client.py
import pytest
@pytest.mark.asyncio
async def test_client_fixture(client):
assert client is not None

190
tests/anterior.py Normal file
View File

@ -0,0 +1,190 @@
# # import pytest
# # import asyncio # Importa asyncio para gerenciar loops de eventos
# # from fastapi.testclient import TestClient
# # from sqlalchemy import select
# # from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
# # from sqlalchemy.orm import sessionmaker
# # from app.main import init_app
# # from app.database.session import sessionmanager
# # from app.database.models import Permissao
# # from app.database.session import Base # Certifique-se de que o Base contém todos os modelos
# # from app.config import URL_BD
# # from app.scripts.initialize_permissions_roles import process_permissions
# #
# # # Criação do engine de testes
# # engine = create_async_engine(URL_BD, echo=True)
# # TestSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
# #
# #
# # async def create_test_database():
# # """
# # Exclui e recria todas as tabelas do banco de dados para garantir um estado limpo.
# # """
# # async with engine.begin() as conn:
# # # Exclui as tabelas se elas já existirem
# # await conn.run_sync(Base.metadata.drop_all)
# # # Cria as tabelas novamente
# # await conn.run_sync(Base.metadata.create_all)
# #
# # # Insere dados iniciais na tabela Permissao
# # async with AsyncSession(engine) as session:
# # permissoes = [
# # Permissao(nome="Permissão Total"),
# #
# # ]
# #
# # session.add_all(permissoes)
# # await session.commit()
# #
# #
# # @pytest.fixture(scope="module")
# # def client():
# # """
# # Fixture para inicializar o cliente de testes do FastAPI com o banco de dados de testes.
# # Cria as tabelas do banco de dados antes de iniciar o cliente.
# # """
# # # Inicializa o sessionmanager com a URL do banco de testes
# # sessionmanager.init(URL_BD)
# #
# # # Cria as tabelas no banco de testes (exclui e recria)
# # asyncio.run(create_test_database())
# #
# # # Inicializa o aplicativo FastAPI para testes
# # app = init_app(init_db=False)
# #
# # with TestClient(app) as c:
# # yield c
# #
# #
# # @pytest.fixture(scope="function")
# # async def session():
# # """
# # Fixture para fornecer uma sessão do SQLAlchemy para os testes.
# # """
# # async with TestSessionLocal() as session:
# # yield session
# #
# #
# import pytest
# import asyncio
# from fastapi.testclient import TestClient
# from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
# from sqlalchemy.orm import sessionmaker
# from app.main import init_app
# from app.database.session import sessionmanager
# from app.database.models import Permissao
# from app.database.session import Base
# from app.config import URL_BD
#
# # Criação do engine de testes
# engine = create_async_engine(URL_BD, echo=True)
# TestSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
#
#
# async def create_test_database():
# """
# Exclui e recria todas as tabelas do banco de dados para garantir um estado limpo.
# """
# async with engine.begin() as conn:
# await conn.run_sync(Base.metadata.drop_all)
# await conn.run_sync(Base.metadata.create_all)
#
# # Insere dados iniciais na tabela Permissao
# async with AsyncSession(engine) as session:
# permissoes = [
# Permissao(nome="Permissão Total"),
# ]
# session.add_all(permissoes)
# await session.commit()
#
#
# @pytest.fixture(scope="module")
# def client():
# """
# Fixture para inicializar o cliente de testes do FastAPI com o banco de dados de testes.
# Cria as tabelas do banco de dados antes de iniciar o cliente.
# """
# # Inicializa o sessionmanager com a URL do banco de testes
# sessionmanager.init(URL_BD)
#
# # Cria as tabelas no banco de testes (exclui e recria)
# asyncio.run(create_test_database())
#
# # Inicializa o aplicativo FastAPI para testes
# app = init_app(init_db=False)
#
# with TestClient(app) as c:
# yield c
#
#
# @pytest.fixture(scope="function")
# async def session():
# """
# Fixture para fornecer uma sessão do SQLAlchemy para os testes.
# """
# async with TestSessionLocal() as session:
# yield session
#
#
# @pytest.fixture(scope="session")
# def event_loop():
# """
# Cria um novo loop de eventos para a sessão de testes.
# """
# loop = asyncio.new_event_loop()
# yield loop
# loop.close()
import pytest
import asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app # Importa o app diretamente do main.py
from app.database.session import sessionmanager
from app.database.models import Permissao
from app.database.session import Base
from app.config import URL_BD
# Criação do engine de testes
engine = create_async_engine(URL_BD, echo=True)
TestSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
async def create_test_database():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
async with AsyncSession(engine) as session:
permissoes = [
Permissao(nome="Permissão Total"),
]
session.add_all(permissoes)
await session.commit()
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="session")
async def client():
# Inicializa o sessionmanager com a URL do banco de testes
sessionmanager.init(URL_BD)
# Cria as tabelas no banco de testes
await create_test_database()
# Usa o app diretamente do main.py
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
@pytest.fixture(scope="function")
async def session():
async with TestSessionLocal() as session:
yield session

251
tests/bkp_conftest.py Normal file
View File

@ -0,0 +1,251 @@
import pytest
import asyncio
from app.main import app
from httpx import AsyncClient
from datetime import datetime, timedelta
from app.database.models import Base, RbacUser, RbacPapel, RbacPermissao
# from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID
from app.config import URL_BD_TESTE, SECRET
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.ext.asyncio import async_sessionmaker
from app.rbac.classes_customizadas import CustomJWTStrategy
from app.rbac.auth import current_active_user
# Criação do engine de testes com o URL do banco de dados
engine = create_async_engine(URL_BD_TESTE)
# Configurando o async_sessionmaker para usar AsyncSession
TestSessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
autoflush=False,
autocommit=False,
)
print("Configuração do sessionmaker concluída")
# Definindo a permissão fictícia que o papel de teste irá possuir
test_permissao = RbacPermissao(
id=1, # "ID" fictício, certifique-se de que o valor seja único e não cause conflitos
nome="Permissao_Fake"
)
# Definindo o papel fictício que o usuário de teste irá possuir
test_papel = RbacPapel(
# uuid=uuid.uuid4(),
nome="Teste_Papel",
permissoes=[test_permissao] # Adicione permissões conforme necessário para simular o comportamento real
)
# Definindo o usuário de teste que será simulado, incluindo o papel
test_user = RbacUser(
# id=uuid.uuid4(), # Gere um UUID único
email="test@email.com",
hashed_password="hashed-password", # Senha fictícia, pois a verificação não será feita
username="testuser",
full_name="Test User",
is_active=True,
is_superuser=False,
papeis=[test_papel] # Incluindo o papel definido anteriormente
)
def fake_current_user():
return test_user # Variável definida com o usuário fictício
# Suponha que você tenha uma lista de permissões que deseja simular
required_permissions_for_test = [1, 2] # "IDs" artificiais de permissões para o teste
# Função para criar e destruir o banco de dados de testes
async def create_test_database():
print("Dentro da função de criação do Banco de Dados")
async with engine.begin() as conn:
print("Apagando todas as tabelas...")
await conn.run_sync(Base.metadata.drop_all)
print("Criando todas as tabelas...")
await conn.run_sync(Base.metadata.create_all)
@pytest.fixture(scope="session")
def anyio_backend():
return "asyncio"
@pytest.fixture(scope="session")
async def client():
print("Iniciando Cliente")
# Chama a função para criar o banco de dados de teste
await create_test_database()
# Inicializa permissões e papéis após a criação do banco de dados
# Definindo a sobrecarga de usuário
app.dependency_overrides[current_active_user] = fake_current_user
# Instanciando a estratégia JWT e gerando o token
strategy = CustomJWTStrategy(secret=SECRET, lifetime_seconds=3600)
token = await strategy.write_token(test_user)
async with AsyncClient(app=app, base_url="http://test") as client:
client.headers.update({"Authorization": f"Bearer {token}"})
yield client
# FUNÇÕES PARA ROTAS QUE REQUEREM AUTENTICAÇÃO
# MODULOS COM UUID PARA TESTES
@pytest.fixture(scope="session")
def datas_referencia():
data_atual = datetime.now().date()
return {
"data_atual": data_atual,
"data_anterior": data_atual - timedelta(days=1),
"data_posterior": data_atual + timedelta(days=1),
}
@pytest.fixture(scope="session")
def uuid_store_relacao_comercial():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e20", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e30", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_tipo_endereco():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e40", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e50", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
}
@pytest.fixture(scope="session")
def uuid_store_pessoa():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e60", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e70", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None,
"uuid_7": None,
"uuid_8": None,
}
@pytest.fixture(scope="session")
def uuid_store_endereco():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e80", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e90", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None,
"uuid_7": None,
"uuid_8": None,
}
@pytest.fixture(scope="session")
def uuid_store_setor():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e21", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e32", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_tipo_equipamento():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e22", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e31", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_equipamento():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e23", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e32", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_itens_equipamento():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e24", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e33", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_manutencao_equipamento():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e25", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e34", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}

335
tests/conftest.py Normal file
View File

@ -0,0 +1,335 @@
import pytest
from fastapi_users.authentication import JWTStrategy
from app.main import app
from httpx import AsyncClient
from datetime import datetime, timedelta
from app.database.models import Base, RbacUser, RbacPapel, RbacPermissao
# from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID
from app.config import URL_BD_TESTE, SECRET
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlalchemy import text
from app.rbac.auth import current_active_user
# Criação do engine de testes com o URL do banco de dados
engine = create_async_engine(URL_BD_TESTE)
selected_user = None
# Configurando o async_sessionmaker para usar AsyncSession
TestSessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
autoflush=False,
autocommit=False,
)
# print("Configuração do sessionmaker concluída")
# print(URL_BD_TESTE)
def pytest_addoption(parser):
parser.addoption(
"--usuario", action="store", default="admin", help="Nome do usuário para executar os testes"
)
# Função para criar um usuário com permissão e papel
def create_user(email, papel_nome, permissoes_ids):
permissoes = [RbacPermissao(id=perm_id, nome=f"Permissao_{perm_id}") for perm_id in permissoes_ids]
papel = RbacPapel(
nome=papel_nome,
permissoes=permissoes
)
user = RbacUser(
email=email,
hashed_password="hashed-password",
fk_inquilino_uuid="pytest",
is_active=True,
is_superuser=False,
papeis=[papel]
)
return user
# Fixture para criar múltiplos usuários
@pytest.fixture(scope="session")
def create_test_users():
users = {
"admin": create_user(
email="admin@example.com",
papel_nome="Admin_Papel",
permissoes_ids=[1]
),
"comercial": create_user(
email="user1@example.com",
papel_nome="User1_Papel",
permissoes_ids=[2]
),
"estoque": create_user(
email="user2@example.com",
papel_nome="User2_Papel",
permissoes_ids=[4]
),
"financeiro": create_user(
email="user3@example.com",
papel_nome="User3_Papel",
permissoes_ids=[5]
),
"pessoa": create_user(
email="user4@example.com",
papel_nome="User4_Papel",
permissoes_ids=[4, 5, 33]
),
}
return users
# Fixture para selecionar um usuário
@pytest.fixture(scope="session")
def selecionar_usuario(create_test_users, request):
usuario_nome = request.config.getoption("--usuario", default="admin")
usuario_selecionado = create_test_users.get(usuario_nome, create_test_users["admin"])
return usuario_selecionado
def fake_current_user(selecionar_usuario):
return selecionar_usuario
async def create_test_database():
tenant_schema = "pytest"
# Dropar e recriar o schema "pytest"
async with engine.begin() as conn:
try:
# print("Dropping schema tenant (pytest)...")
await conn.execute(text(f"DROP SCHEMA IF EXISTS {tenant_schema} CASCADE"))
# print("Creating schema tenant (pytest)...")
await conn.execute(text(f"CREATE SCHEMA {tenant_schema}"))
except Exception as e:
print(f"Erro ao dropar ou recriar o schema '{tenant_schema}': {e}")
raise # Propaga o erro para interromper os testes
# Atualiza o schema das tabelas que não são do schema "shared"
for table in Base.metadata.tables.values():
if table.schema != "shared":
table.schema = tenant_schema
# Cria as tabelas apenas para o schema tenant
tenant_tables = [table for table in Base.metadata.sorted_tables if table.schema == tenant_schema]
async with engine.begin() as conn:
try:
# print("Criando tabelas no schema tenant (pytest)...")
await conn.run_sync(Base.metadata.create_all, tables=tenant_tables)
except Exception as e:
print(f"Erro ao criar as tabelas no schema '{tenant_schema}': {e}")
raise
@pytest.fixture(scope="session")
def anyio_backend():
return "asyncio"
@pytest.fixture(scope="session")
async def client(selecionar_usuario):
# print("Iniciando Cliente")
# Chama a função para criar o banco de dados de teste
await create_test_database()
app.dependency_overrides[current_active_user] = lambda: selecionar_usuario
strategy = JWTStrategy(secret=SECRET, lifetime_seconds=3600)
token = await strategy.write_token(selecionar_usuario)
# print(f"Token gerado: {token}")
async with AsyncClient(app=app, base_url="http://test") as client:
client.headers.update({"Authorization": f"Bearer {token}"})
yield client
# Função para modificar os itens de teste com base nos usuários permitidos
def pytest_collection_modifyitems(config, items):
usuario = config.getoption("--usuario")
def usuario_esta_permitido(usuarios_permitidos_):
# print(f"Verificando se o usuário '{usuario}' está na lista de permitidos: {usuarios_permitidos_}")
return usuario in usuarios_permitidos_
selected_items = []
for item in items:
usuarios_permitidos_marker = item.get_closest_marker("usuarios_permitidos")
if usuarios_permitidos_marker:
usuarios_permitidos = usuarios_permitidos_marker.args[0]
if not usuario_esta_permitido(usuarios_permitidos):
# print(f"Ignorando o teste '{item.name}' porque o usuário '{usuario}' não está permitido.")
continue # Ignorar o teste se o usuário não estiver permitido
selected_items.append(item)
# Atualiza a lista de testes que serão executados
items[:] = selected_items
# FUNÇÕES PARA ROTAS QUE REQUEREM AUTENTICAÇÃO
# MODULOS COM UUID PARA TESTES
@pytest.fixture(scope="session")
def datas_referencia():
data_atual = datetime.now().date()
return {
"data_atual": data_atual,
"data_anterior": data_atual - timedelta(days=1),
"data_posterior": data_atual + timedelta(days=1),
}
@pytest.fixture(scope="session")
def uuid_store_relacao_comercial():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e20", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e30", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_tipo_endereco():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e40", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e50", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
}
@pytest.fixture(scope="session")
def uuid_store_pessoa():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e60", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e70", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None,
"uuid_7": None,
"uuid_8": None,
}
@pytest.fixture(scope="session")
def uuid_store_endereco():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e80", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e90", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None,
"uuid_7": None,
"uuid_8": None,
}
@pytest.fixture(scope="session")
def uuid_store_setor():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e21", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e32", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_tipo_equipamento():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e22", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e31", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_equipamento():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e23", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e32", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_itens_equipamento():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e24", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e33", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}
@pytest.fixture(scope="session")
def uuid_store_manutencao_equipamento():
return {
"uuid_1": "01915caf-2c4d-7270-a071-d928c87f8e25", # UUID inválido para testes
"uuid_2": "01915caf-2c4d-7270-a071-d928c87f8e34", # UUID inválido para testes
"uuid_3": None,
"uuid_4": None,
"uuid_5": None,
"uuid_6": None, # UUID para Relacionamento
"uuid_7": None, # UUID para Relacionamento
"uuid_8": None, # UUID para Relacionamento
"uuid_9": None,
"uuid_10": None,
"uuid_11": None,
}

44
tests/init_db_pytest.py Normal file
View File

@ -0,0 +1,44 @@
import subprocess
import asyncio
from app.database.session import sessionmanager
from app.config import URL_BD
from app.scripts.initialize_permissions_roles import initialize_permissions_roles
from app.scripts.create_initial_user import create_user
def alembic_upgrade():
try:
print("Iniciando Migrações Alembic")
result = subprocess.run(["alembic", "upgrade", "head"], check=True, capture_output=True, text=True)
print("Migração Alembic finalizado com sucesso.")
print(result.stdout)
except subprocess.CalledProcessError as e:
print("Erro durante Migrações Alembic.")
print(e.stderr)
raise
async def init_bd(user_email: str, user_password: str, user_username: str, user_full_name: str,
user_is_superuser: bool = False):
sessionmanager.init(URL_BD)
try:
print("Inserindo dados iniciais no Banco de Dados")
# Criando o usuário inicial
user_id = await create_user(user_email, user_password, user_username, user_full_name, user_is_superuser)
# Inserindo permissões e papéis
await initialize_permissions_roles(user_id)
except Exception as e:
print(f"Falha na inserção de dados: {e}")
if __name__ == "__main__":
# Os dados do usuário podem ser passados como argumentos na linha de comando ou coletados de outra maneira
email = "admin@sonora.com"
password = "admin"
username = "UsuarioAdmin"
full_name = "Admin"
is_superuser = True
asyncio.run(init_bd(email, password, username, full_name, is_superuser))

View File

1414
tests/test_003_pessoa.py Normal file

File diff suppressed because it is too large Load Diff

334
tests/test_004_endereco.py Normal file
View File

@ -0,0 +1,334 @@
import pytest
from httpx import AsyncClient
from fastapi import status
BASE_URL = "/api/endereco"
@pytest.mark.anyio
async def test_route_exists(client: AsyncClient):
response = await client.post(f"{BASE_URL}/get_all")
assert response.status_code != 404 # Certifica-se de que a rota existe
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_create_one(client: AsyncClient, uuid_store_tipo_endereco: dict, uuid_store_pessoa: dict,
uuid_store_endereco: dict):
response = await client.post(f"{BASE_URL}/add_one",
json={"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Endereço Descrição 1",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Complemento 1",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]})
assert response.status_code == 201
uuid_store_endereco["uuid_3"] = response.json()["uuid"]
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_create_many(client: AsyncClient, uuid_store_tipo_endereco: dict, uuid_store_pessoa: dict,
uuid_store_endereco: dict, ):
response = await client.post(f"{BASE_URL}/add_many", json=[
{"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Endereço Descrição 2",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Complemento 2",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]},
{"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Endereço Descrição 3",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Complemento 3",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]},
{"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Endereço Descrição 4",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Complemento 4",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]},
{"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Endereço Descrição 5",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Complemento 5",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]},
{"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Endereço Descrição 6",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Complemento 6",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]}
])
assert response.status_code == 201
data = response.json()
assert len(data) == 5
uuid_store_endereco["uuid_4"] = data[0]["uuid"]
uuid_store_endereco["uuid_5"] = data[1]["uuid"]
uuid_store_endereco["uuid_6"] = data[2]["uuid"]
uuid_store_endereco["uuid_7"] = data[3]["uuid"]
uuid_store_endereco["uuid_8"] = data[4]["uuid"]
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_get_all(client: AsyncClient):
response = await client.post(f"{BASE_URL}/get_all")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_get_many(client: AsyncClient, uuid_store_endereco: dict):
uuids = [uuid_store_endereco["uuid_3"], uuid_store_endereco["uuid_4"]]
response = await client.post(f"{BASE_URL}/get_many", json={"uuids": uuids})
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) == 2
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_get_one(client: AsyncClient, uuid_store_endereco: dict):
response = await client.post(f"{BASE_URL}/get_one", json={"uuid": uuid_store_endereco["uuid_3"]})
assert response.status_code == 200
data = response.json()
assert "uuid" in data
assert data["uuid"] == uuid_store_endereco["uuid_3"]
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_update_one_existing_item(client: AsyncClient, uuid_store_endereco: dict, uuid_store_tipo_endereco: dict,
uuid_store_pessoa: dict):
response = await client.put(f"{BASE_URL}/update_one",
json={"uuid": uuid_store_endereco["uuid_8"],
"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Update Endereço Descrição 6",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Update Complemento 6",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]})
assert response.status_code == 201
data = response.json()
assert data["uuid"] == uuid_store_endereco["uuid_8"]
assert data["endereco_pessoa_descricao"] == "Update Endereço Descrição 6"
assert data["endereco_pessoa_complemento"] == "Update Complemento 6"
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_update_many_existing_item(client: AsyncClient, uuid_store_endereco: dict, uuid_store_tipo_endereco: dict,
uuid_store_pessoa: dict):
response = await client.put(f"{BASE_URL}/update_many", json=[
{"uuid": uuid_store_endereco["uuid_7"], "endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Update Endereço Descrição 5",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Update Complemento 5",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]},
{"uuid": uuid_store_endereco["uuid_6"], "endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Update Endereço Descrição 4",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Update Complemento 4",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]},
])
assert response.status_code == 201
data = response.json()
assert len(data) == 2
# Verificando se os valores atualizados são os corretos
assert data[0]["uuid"] == uuid_store_endereco["uuid_6"]
assert data[0]["endereco_pessoa_descricao"] == "Update Endereço Descrição 4"
assert data[0]["endereco_pessoa_complemento"] == "Update Complemento 4"
assert data[1]["uuid"] == uuid_store_endereco["uuid_7"]
assert data[1]["endereco_pessoa_descricao"] == "Update Endereço Descrição 5"
assert data[1]["endereco_pessoa_complemento"] == "Update Complemento 5"
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_delete_one_item(client: AsyncClient, uuid_store_endereco):
response = await client.request(
method="DELETE",
url=f"{BASE_URL}/delete_one",
json={"uuid": uuid_store_endereco["uuid_3"]}
)
assert response.status_code == 204
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_delete_many_items(client: AsyncClient, uuid_store_endereco):
uuids = [uuid_store_endereco["uuid_4"], uuid_store_endereco["uuid_5"]]
response = await client.request(
method="DELETE",
url=f"{BASE_URL}/delete_many",
json={"uuids": uuids} # Envia o corpo da solicitação como JSON
)
assert response.status_code == 204
# Testes com dados inválidos
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_delete_one_non_existent_item(client: AsyncClient, uuid_store_relacao_comercial):
# Tentando deletar novamente o primeiro item já deletado
response = await client.request(
method="DELETE",
url=f"{BASE_URL}/delete_one",
json={"uuid": uuid_store_relacao_comercial["uuid_1"]}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_get_one_non_existent_item(client: AsyncClient, uuid_store_relacao_comercial):
# Tentando buscar um item deletado
response = await client.request(
method="POST",
url=f"{BASE_URL}/get_one",
json={"uuid": uuid_store_relacao_comercial["uuid_3"]}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_delete_many_non_existent_item(client: AsyncClient, uuid_store_relacao_comercial):
uuids = [uuid_store_relacao_comercial["uuid_1"], uuid_store_relacao_comercial["uuid_2"],
uuid_store_relacao_comercial["uuid_5"]]
response = await client.request(
method="DELETE",
url=f"{BASE_URL}/delete_many",
json={"uuids": uuids} # Envia o corpo da solicitação como JSON
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_update_one_non_existing_item(client: AsyncClient, uuid_store_tipo_endereco: dict,
uuid_store_endereco: dict, uuid_store_pessoa: dict):
# Atualizando o segundo item
response = await client.request(
method="PUT",
url=f"{BASE_URL}/update_one",
json={"uuid": uuid_store_endereco["uuid_1"],
"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Update Endereço Descrição 4",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Update Complemento 4",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_update_many_non_existing_item(client: AsyncClient, uuid_store_endereco: dict, uuid_store_pessoa: dict,
uuid_store_tipo_endereco: dict):
# Atualizando o segundo e terceiro item
response = await client.request(
method="PUT",
url=f"{BASE_URL}/update_many",
json=[
{"uuid": uuid_store_endereco["uuid_1"],
"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Update Endereço Descrição 4",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Update Complemento 4",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]},
{"uuid": uuid_store_endereco["uuid_2"],
"endereco_pessoa_status": True,
"endereco_pessoa_descricao": "Update Endereço Descrição 4",
"endereco_pessoa_numero": "123",
"endereco_pessoa_complemento": "Update Complemento 4",
"endereco_pessoa_cep": "00000000",
"fk_tipo_endereco_uuid": uuid_store_tipo_endereco["uuid_6"],
"fk_pessoa_uuid": uuid_store_pessoa["uuid_6"]}
]
)
assert response.status_code == status.HTTP_404_NOT_FOUND
# Teste com dados fora dos limites de tamanho
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_create_one_min_length(client: AsyncClient, uuid_store_relacao_comercial: dict):
response = await client.post(f"{BASE_URL}/add_one",
json={"endereco_pessoa_descricao": "a"})
assert response.status_code == 422
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_create_one_max_length(client: AsyncClient, uuid_store_relacao_comercial: dict):
response = await client.post(f"{BASE_URL}/add_one",
json={"endereco_pessoa_descricao": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaaaaaaa"})
assert response.status_code == 422
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_create_many_max_and_min_length(client: AsyncClient):
response = await client.post(f"{BASE_URL}/add_many", json=[
{"endereco_pessoa_descricao": "aa"},
{"endereco_pessoa_descricao": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
{"endereco_pessoa_descricao": "aa"},
{"endereco_pessoa_descricao": "aa"}
])
assert response.status_code == 422
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_update_one_existing_item_min_lenght(client: AsyncClient, uuid_store_endereco: dict):
response = await client.put(f"{BASE_URL}/update_one",
json={"uuid": uuid_store_endereco["uuid_8"],
"endereco_pessoa_descricao": "a"})
assert response.status_code == 422
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_update_one_existing_item_max_lenght(client: AsyncClient, uuid_store_endereco: dict):
response = await client.put(f"{BASE_URL}/update_one",
json={"uuid": uuid_store_endereco["uuid_8"],
"endereco_pessoa_descricao": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaa"})
assert response.status_code == 422
@pytest.mark.anyio
@pytest.mark.usuarios_permitidos(["admin", "estoque"])
async def test_update_many_existing_item_max_and_min_length(client: AsyncClient, uuid_store_endereco: dict):
response = await client.put(f"{BASE_URL}/update_many", json=[
{"uuid": uuid_store_endereco["uuid_7"], "endereco_pessoa_descricao": "aa"},
{"uuid": uuid_store_endereco["uuid_6"],
"endereco_pessoa_descricao": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
])
assert response.status_code == 422