Biblioteca para construção de APIs Rest Python, de acordo com o guidelines interno, e com paradigma declarativo.
Project description
RestLib (nsj_rest_lib)
QuickStart
Biblioteca para construção de APIs Rest Python, de acordo com o guidelines interno, e com paradigma declarativo.
Em resumo, com o RestLib, é possível construir APIs Rest completas para CRUD de entidades (GET, POST, PUT e DELETE), bastando escrever arquivos declarativos de DTO, Entity e das rotas em si, sem implementar a lógica de negócio ou acesso a banco de dados.
No entanto, diferentemente de frameworks web complexos, o RestLib é apenas uma biblioteca que pode ser usada em conjunto, ou até mesmo como coadjuvante, de uma implementação completamente manual, não "dominando" o ciclo de vida das requisições WEB (e sendo dependente do Flask enquanto framework HTTP).
Por que mais uma biblioteca no estilo framework web?
Os padrões de desenvolvimento da Nasajon contam com particularidades que são melhor atendidas por bilbiotecas próprias, sejam elas:
- APIs Enxutas
- Retorno exclusivo dos campos de resumo.
- Suporte ao parâmetro fields.
- Paginação obrigatória.
- Recuperação de dados (do banco) somente sob demanda.
- APIs Multi Tenant
- Particinamento de dados por tenant (e/ou grupo empresarial)
- APIs Multi Banco
- A mesma API pode ser conectar a qualquer BD do ambiente web, de acordo com o tenant recebido.
- Suporte ao padrão de isolamento de dados do ERP Desktop (Conjuntos)
- Logs de auditoria e telemetria próprios
- Integração nativa com o motor de webhooks interno
Assim, para não exigir excesso de trabalho repetitivo na implementação das APIs internas, optou-se pelo desenvolvimento de uma biblioteca auxiliar própria.
A grande diferença, na filosofia de desenvolvimento, é que se prioriza um esforço para que a biblioteca possa ser usada como auxiliar de fluxos implementados na mão, assim como não impeça as customizações desejadas pelo programador (além de deixá-lo livre para misturar com outras soluções e bibliotecas).
Passos básicos para desenvolver um CRUD completo usando o RestLib
- Escrever a classe Entity, que reflita a entidade desejada (o formato deve ser plenamento correspondete À estrutura de banco desejada). Exemplo.
- Escrever a classe DTO, que reflita o formato desejado do JSON de entrada e saída (podem haver diferentes formatos em diferentes rotas). Exemplo.
- Escrever o módulo Controler, que será declarativo das rotas que se desejam expôr (o qual segue, em grande parte, o padrão do Flask). Exemplo.
Para facilitar ainda mais o trabalho, foi criado um utilitário de linha de comando, que, por meio das APIs do ChatGPT, é capaz de escrever as classes de DTO e Entity, a partir do DDL da entidade desejada.
Variáveis de ambiente necessárias no seu projeto
Variáveis gerais
Variável | Obrigatória | Descrição |
---|---|---|
APP_NAME | Sim | Nome da aplicação. |
DEFAULT_PAGE_SIZE | Não (padrão: 20) | Quantidade máxima de items retonardos numa página de dados |
USE_SQL_RETURNING_CLAUSE | Não (padrão: true) | Montagem das cláusulas returning |
TESTS_TENANT | Sim | Código do tenant obrigatório para rodar os testes |
Variáveis de banco
Variável | Obrigatória | Descrição |
---|---|---|
DATABASE_HOST | Sim | IP ou nome do host, para conexão com o BD. |
DATABASE_PASS | Sim | Senha para conexão com o BD. |
DATABASE_PORT | Sim | Porta para conexão com o BD. |
DATABASE_NAME | Sim | Nome do BD. |
DATABASE_USER | Sim | Usuário para conexão com o BD. |
DATABASE_DRIVER | Não (padrão: POSTGRES) | Driver para conexão com o BD. |
Variáveis do RabbitMQ
Variável | Obrigatória | Descrição |
---|---|---|
RABBITMQ_HOST | Não | IP ou nome do host. |
RABBITMQ_HTTP_PORT | Não (padrão: 15672) | Porta para conexão. |
RABBITMQ_USER | Não (padrão: guest) | Senha para conexão. |
RABBITMQ_PASS | Não (padrão: guest) | Senha para conexão. |
Montando ambiente de desenvolvimento
Para rodar a biblioteca localmente, após a configuração da sua aplicação principal, siga os passos:
- Clone a biblioteca:
git clone git@github.com:Nasajon/nsj_rest_lib.git
- Desinstale a biblioteca no seu ambiente virtual da aplicação:
pip uninstall nsj-rest-lib
- Na variável de ambiente PYTHONPATH localizado no .env da aplicação principal, coloque o caminho da biblioteca após o caminho da aplicação, entre :
PYTHONPATH=/home/@work/dados-mestre-api:/home/@work/nsj_rest_lib/src
HealthCheck
A classe HealthCheckConfig
oferece uma configuração para verificação da saúde em uma aplicação Flask. Verificando o status do banco de dados e do servidor RabbitMQ. Parâmetro obrigatório na inicialização é o flask_application, outros parâmetros como: injector_factory_class, app_name, rabbitmq_host, rabbitmq_http_port, rabbitmq_user, rabbitmq_pass, serão recuperados das variáveis de ambiente e/ou nulos. Na config
por padrão os parâmetros check_database e check_rabbit_mq virão True e False, podendo ser ajustados na chamada.
Exemplo:
#importando a classe
from nsj_rest_lib.healthcheck_config import HealthCheckConfig
HealthCheckConfig(
flask_application=application
).config(check_database=True, check_rabbit_mq=False)
EntityBase
EntityBase
é uma classe abstrata genérica para representar entidades no banco de dados. Ele fornece um modelo flexível para criar classes de entidade específicas. As subclasses devem herdar desta classe e implementar os métodos para configurar os detalhes da tabela no banco de dados.
Métodos
-
get_table_name(self) -> str
: Método que deve ser implementado pela subclasse para retornar o nome da tabela associada à entidade no banco de dados. -
get_default_order_fields(self) -> List[str]
: Método que deve ser implementado pela subclasse para retornar uma lista de campos para ordenação padrão quando não for especificada uma ordenação personalizada. -
get_pk_field(self) -> str
: Método que deve ser implementado pela subclasse para retornar o nome do campo chave primária na tabela do banco de dados. -
get_fields_map(self) -> dict
: Método que deve ser implementado pela subclasse para retornar um dicionário mapeando nomes de atributos para nomes de campos no banco de dados. -
get_insert_returning_fields(self) -> List[str]
: Método que pode ser implementado pela subclasse para retornar uma lista de campos que devem ser retornados após uma operação de inserção no banco de dados. -
get_update_returning_fields(self) -> List[str]
: Método que pode ser implementado pela subclasse para retornar uma lista de campos que devem ser retornados após uma operação de atualização no banco de dados.
Exemplo:
from nsj_rest_lib.entity.entity_base import EntityBase
class ClienteEntity(EntityBase):
id: str
estabelecimento: str
cliente: str
def __init__(self) -> None:
self.id: str = None
self.estabelecimento: str = None
self.cliente: str = None
def get_table_name(self) -> str:
return 'cliente'
def get_pk_field(self) -> str:
return 'id'
def get_default_order_fields(self) -> List[str]:
return ['estabelecimento', 'cliente', 'id']
def get_fields_map(self) -> dict: #rever
return {
"estabelecimento": "estabelecimento",
"cliente": "cliente"
}
Filter
A classe Filter
é uma representação de um filtro que pode ser aplicado a uma consulta em um banco de dados ou a uma coleção de dados. Um filtro é composto por um FilterOperator e um valor que será usado para realizar a comparação.
Enumeração FilterOperator
EQUALS = "equals"
DIFFERENT = "diferent"
GREATER_THAN = "greater_than"
LESS_THAN = "less_than"
GREATER_OR_EQUAL_THAN = "greater_or_equal_than"
LESS_OR_EQUAL_THAN = "less_or_equal_than"
LIKE = "like"
ILIKE = "ilike"
NOT_NULL = "not_null"
Exemplo:
from nsj_rest_lib.entity.filter import Filter, FilterOperator
Filter(FilterOperator.EQUALS, 'value')
DAO
DAOBase
é uma classe genérica que serve como um Data Access Object (DAO) para simplificar o processo de interação com o banco de dados.
Métodos:
-
__init__
(self, db: DBAdapter2, entity_class: EntityBase) -> None: Construtor da classe DAOBase. Inicializa um objeto DAOBase com uma instância de DBAdapter2 e uma classe de entidade (EntityBase) associada. -
begin(self)
-> None: Inicia uma nova transação no banco de dados. -
commit(self)
-> None: Realiza commit na transação corrente no banco de dados, se houver uma transação em andamento. Não gera erro se não houver uma transação. -
rollback(self)
-> None: Realiza rollback na transação corrente no banco de dados, se houver uma transação em andamento. Não gera erro se não houver uma transação. -
in_transaction(self)
-> bool: Verifica se há uma transação em andamento no DBAdapter. Retorna True se houver uma transação, caso contrário, retorna False. -
get(self, key_field: str, id: uuid.UUID, fields: List[str] = None, filters: Dict[str, List[Filter]] = None, conjunto_type: ConjuntoType = None, conjunto_field: str = None)
-> EntityBase: Retorna uma instância de entidade com base no seu ID. Aceita parâmetros opcionais para especificar campos específicos, filtros adicionais ou junções de conjuntos. -
list(self, after: uuid.UUID, limit: int, fields: List[str], order_fields: List[str], filters: Dict[str, List[Filter]], conjunto_type: ConjuntoType = None, conjunto_field: str = None)
-> List[EntityBase]: Retorna uma lista paginada de entidades. Aceita parâmetros para especificar a partir de qual registro (after) começar a paginar, o número máximo de registros (limit), campos a serem incluídos, campos pelos quais ordenar (order_fields), filtros adicionais e junções de conjuntos. -
insert_relacionamento_conjunto(self, id: str, conjunto_field_value: str, conjunto_type: ConjuntoType = None)
-> None: Insere um relacionamento com um conjunto para uma entidade específica com base no seu ID e no valor do campo de conjunto. Permite especificar o tipo de conjunto, caso necessário. -
delete_relacionamento_conjunto(self, id: str, conjunto_type: ConjuntoType = None)
-> None: Remove um relacionamento com um conjunto para uma entidade específica com base no seu ID. Permite especificar o tipo de conjunto, caso necessário. -
insert(self, entity: EntityBase)
-> EntityBase: Insere um objeto de entidade no banco de dados. Retorna a entidade inserida com os dados atualizados, incluindo a chave primária, se for gerada automaticamente pelo banco de dados. -
update(self, key_field: str, key_value: Any, entity: EntityBase, filters: Dict[str, List[Filter]], partial_update: bool = False)
-> EntityBase: Atualiza um objeto de entidade no banco de dados com base no campo de chave, valor de chave, filtros e entidade fornecidos. Permite a opção partial_update para atualização parcial de campos. Retorna a entidade atualizada com os dados mais recentes do banco de dados. -
list_ids(self, filters: Dict[str, List[Filter]])
-> Optional[List[Any]]: Lista os IDs das entidades que correspondem aos filtros fornecidos. Retorna uma lista de IDs ou None se não houver correspondência. -
delete(self, filters: Dict[str, List[Filter]])
-> None: Exclui registros do banco de dados com base nos filtros fornecidos. Gera uma exceção NotFoundException se nenhum registro for encontrado para exclusão. -
is_valid_uuid(self, value)
-> bool: Verifica se um valor é um UUID válido.
DTO
DTOBase
é uma classe abstrata que representa um Data Transfer Object (DTO) usado para transferir dados entre a camada de apresentação e a camada de serviço de uma aplicação. É especialmente útil em operações onde uma entidade de banco de dados precisa ser convertida em um formato adequado para interações com a interface do usuário.
Atributos:
resume_fields: Set[str]
- Conjunto de campos que devem ser incluídos na visualização resumida do DTO.partition_fields: Set[str]
- Conjunto de campos utilizados para particionamento de dados.fields_map: Dict[str, DTOField]
- Dicionário mapeando nomes de atributos do DTO para configurações de campos correspondentes.list_fields_map: dict
- Dicionário mapeando nomes de atributos do DTO para configurações de campos de lista.field_filters_map: Dict[str, DTOFieldFilter]
- Dicionário mapeando nomes de atributos do DTO para configurações de filtros de campo.pk_field: str
- Nome do campo chave primária na entidade associada ao DTO.fixed_filters: Dict[str, Any]
- Dicionário contendo filtros fixos que devem ser aplicados ao recuperar dados da base de dados.conjunto_type: ConjuntoType
- Tipo de conjunto utilizado para junções de dados relacionados.conjunto_field: str
- Nome do campo utilizado para junções de dados relacionados.escape_validator: bool
- Indica se a validação de dados deve ser ignorada.uniques: Dict[str, Set[str]]
- Dicionário mapeando nomes de campos únicos para um conjunto de valores únicos.candidate_keys: List[str]
- Lista de nomes de campos que juntos formam uma chave candidata única.
Métodos:
__init__(self, entity: Union[EntityBase, dict] = None, escape_validator: bool = False, generate_default_pk_value: bool = True, **kwargs)
-> None: Construtor da classe DTOBase que inicializa um objeto DTOBase com base em uma entidade ou um dicionário de dados, permitindo determinar se a validação deve ser ignorada e se o valor da PK deve ser gerado se não for fornecido.convert_to_entity(self, entity_class: EntityBase, none_as_empty: bool = False)
-> EntityBase - Converte o DTO para uma instância da entidade associada.convert_to_dict(self, fields: Dict[str, List[str]] = None, just_resume: bool = False)
-> Dict - Converte o DTO para um dicionário, permitindo especificar campos para inclusão e se deve ser uma visualização resumida.
DTOField
A classe DTOField
representa uma propriedade de um objeto DTO e define várias configurações para essa propriedade, como tipo esperado, validações, formatações, entre outras. As validações personalizadas são acessadas em DTOFieldValidators. A classe DTOFieldFilter
representa um filtro que pode ser aplicado a uma propriedade DTO para consultas.
Parâmetros:
type [object = None]
: Tipo esperado para a propriedade. Se for do tipo enum.Enum, o valor recebido será convertido para o enumerado.not_null [bool = False]
: Indica se o campo não pode ser None ou vazio (no caso de strings).resume [bool = False]
: Indica se o campo será usado como resumo, sempre retornado em consultas GET que listam os dados.min [int = None]
: Menor valor permitido (ou menor comprimento, para strings).max [int = None]
: Maior valor permitido (ou maior comprimento, para strings).validator [typing.Callable = None]
: Função que valida o valor da propriedade antes de atribuí-lo.strip [bool = False]
: Indica se espaços no início e no fim de strings devem ser removidos.entity_field [str = None]
: Nome da propriedade equivalente na classe de entidade.filters [typing.List[DTOFieldFilter] = None]
: Lista de filtros adicionais suportados para esta propriedade.pk [bool = False]
: Indica se o campo é a chave primária da entidade.use_default_validator [bool = True]
: Indica se o validador padrão deve ser aplicado à propriedade.default_value [typing.Union[typing.Callable, typing.Any] = None]
: Valor padrão de preenchimento da propriedade, caso não seja fornecido.partition_data [bool = False]
: Indica se esta propriedade participa dos campos de particionamento da entidade.convert_to_entity [typing.Callable = None]
: Função para converter o valor do DTO para o valor da entidade.convert_from_entity [typing.Callable = None]
: Função para converter o valor da entidade para o valor do DTO.unique [str = None]
: Nome de chave de unicidade, usado para evitar duplicações no banco de dados.candidate_key [bool = False]
: Indica se este campo é uma chave candidata.
Exemplo:
from nsj_rest_lib.descriptor.dto_field import DTOField, DTOFieldFilter
from nsj_rest_lib.descriptor.dto_field_validators import DTOFieldValidators
cliente: str = DTOField(resume=True, not_null=True, strip=True, min=11, max=60,
validator=DTOFieldValidators().validate_cpf_or_cnpj)
criado_em: datetime.datetime = DTOField(
resume=True,
filters=[
DTOFieldFilter('criado_apos', FilterOperator.GREATER_THAN),
DTOFieldFilter('criado_antes', FilterOperator.LESS_THAN),
],
default_value=datetime.datetime.now
)
Controllers
get_route
Exemplo:
from nsj_rest_lib.controller.get_route import GetRoute
@GetRoute(
url=GET_ROUTE,
http_method='GET',
dto_class=ClienteDTO,
entity_class=ClienteEntity
)
list_route
Exemplo:
from nsj_rest_lib.controller.list_route import ListRoute
@ListRoute(
url=LIST_ROUTE,
http_method='GET',
dto_class=ClienteDTO,
entity_class=ClienteEntity
)
post_route
Exemplo:
from nsj_rest_lib.controller.post_route import PostRoute
@PostRoute(
url=LIST_POST_ROUTE,
http_method='POST',
dto_class=ClienteDTO,
entity_class=ClienteEntity,
dto_response_class=ClientePostReturnDTO
)
put_route
Exemplo:
from nsj_rest_lib.controller.put_route import PutRoute
@PutRoute(
url=GET_PUT_ROUTE,
http_method='PUT',
dto_class=ClienteDTO,
entity_class=ClienteEntity
)
delete_route
Exemplo:
from nsj_rest_lib.controller.delete_route import DeleteRoute
@DeleteRoute(
url=GET_DELETE_ROUTE,
http_method="DELETE",
dto_class=ClienteDTO,
entity_class=ClienteEntity,
injector_factory=InjectorFactoryMultibanco,
service_name="cliente_service",
)
Testes Automatizados
src/
│...
├── tests/
│ ├── api/
│ │ ├── casos_de_teste/
│ │ │ ├── clientes/
│ │ │ │ ├── delete/
│ │ │ │ │ ├── entradas_json/
│ │ │ │ │ │ ├── exemplo1_204.json
│ │ │ │ │ ├── saidas_json/
│ │ │ │ │ │ ├── exemplo1_204.json
│ │ │ │ │ ├── test_clientes_delete.py
│ │ │ │ ├── get/
│ │ │ │ │ ├── entradas_json/
│ │ │ │ │ │ ├── exemplo1_200.json
│ │ │ │ │ ├── saidas_json/
│ │ │ │ │ │ ├── exemplo1_200.json
│ │ │ │ │ ├── test_clientes_get.py
│ │ │ │ ├── post/
│ │ │ │ │ ├── entradas_json/
│ │ │ │ │ │ ├── exemplo1_201.json
│ │ │ │ │ ├── saidas_json/
│ │ │ │ │ │ ├── exemplo1_201.json
│ │ │ │ │ ├── test_clientes_post.py
│ │ │ │ ├── put/
│ │ │ │ │ ├── entradas_json/
│ │ │ │ │ │ ├── exemplo1_204.json
│ │ │ │ │ ├── saidas_json/
│ │ │ │ │ │ ├── exemplo1_204.json
│ │ │ │ │ ├── test_clientes_put.py
│ │ │ ├── dump_sql/
│ │ │ │ ├── global.sql
Montando o ambiente
- Instalar todas as dependências do projeto.
pip install -r requirements.txt
- Subir o container do banco de testes, para cada teste pode ou não rodar o script contido no
global.sql
, basta configurar nopre_setup
conforme necessidade.
docker-compose up -d postgres
- Na configuração do Debug (launch.json), configure o Flask e o Pytest. Certifique-se de que o Flask esteja em execução para o Pytest funcionar corretamente.
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "src/nsj_rest_lib/wsgi.py",
"FLASK_DEBUG": "1"
},
"args": [
"run",
"--no-debugger",
"--no-reload"
],
"jinja": true,
"justMyCode": true
},
{
"name": "Pytest",
"type": "python",
"request": "launch",
"module": "pytest",
"args": [
"-s",
"tests/api/casos_de_teste"
]
}
Criação dos testes
Para a criação dos casos de testes de forma automática pode seguir o exemplo "Criação de caso de teste". Neste contexto, foram desenvolvidos testes para entidade de clientes (DELETE, GET, POST e PUT).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for nsj_rest_lib-2.6.3-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 6ececfef28974aa80f9a4b6a8efe3c01b644b562fb36237eab33134265cf4766 |
|
MD5 | 0a45f9411b7f716c48b728a6a8fc2ef2 |
|
BLAKE2b-256 | ab9c5a8347f050c14a46169f660b056fcd950d07435369020bc43e93d6cdea87 |