Commit Inicial
This commit is contained in:
commit
91a4568db1
|
|
@ -0,0 +1,9 @@
|
|||
iniciar.txt
|
||||
iniciar_multi_tenant.txt
|
||||
docker.txt
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
.env
|
||||
.git
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration with an async dbapi.
|
||||
|
|
@ -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 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 = "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 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)
|
||||
|
||||
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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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,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')
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 há 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,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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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______________________________________________
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,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)
|
||||
|
||||
|
|
@ -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)}")
|
||||
|
||||
|
|
@ -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)}")
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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),
|
||||
|
||||
)
|
||||
|
|
@ -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"],
|
||||
)
|
||||
|
|
@ -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,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
|
||||
|
|
@ -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,
|
||||
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,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)}")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from . import endereco_schemas
|
||||
from . import papel_shemas
|
||||
from . import pessoa_schemas
|
||||
from . import tipo_endereco_schemas
|
||||
from . import utils
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 há 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 já 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,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
|
||||
|
|
@ -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
|
||||
|
|
@ -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}'!")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
"""
|
||||
FIN_TIPO_PAGAMENTO
|
||||
|
||||
Tabela que armazena os tipos de pagamento: DINHEIRO, CARTÃO, CHEQUE, etc.
|
||||
Tipos padrões já 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 já cadastrados pelo sistema para toda empresa:
|
||||
01 = Dinheiro
|
||||
02 = Cheque
|
||||
03 = Cartão
|
||||
04 = Boleto
|
||||
05 = PIX
|
||||
"""
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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())
|
||||
|
|
@ -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,))
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
###
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# _test_client.py
|
||||
import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_fixture(client):
|
||||
assert client is not None
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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))
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
Loading…
Reference in New Issue