Metadata-Version: 2.4
Name: bravaorm
Version: 0.0.39
Summary: Python ORM for MySQL
Home-page: https://github.com/robertons/bravaorm
Author: Roberto Neves
Author-email: robertonsilva@gmail.com
License: MIT
Keywords: orm,datamodel,database,model,entity,sdk,mysql,mariadb
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: Portuguese (Brazilian)
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mysql-connector-python
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: requires-dist
Dynamic: summary

# Brava ORM para MySQL/MariaDB

SDK Python para aumentar produtividade no desenvolvimento de aplicações com integração a banco de dados relacional MySQL/MariaDB.

[![PyPI version](https://badge.fury.io/py/bravaorm.svg)](https://badge.fury.io/py/bravaorm)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

---

## Instalação

Via Pip:

```bash
pip install bravaorm
```

Via Git/Clone:

```bash
git clone https://github.com/robertons/bravaorm
cd bravaorm
pip install -r requirements.txt
python setup.py install
```

**Dependência principal:** `mysql-connector-python`

---

## Conexão com Banco de Dados

```python
import bravaorm

conn = bravaorm.Connection(
    db_user="root",
    db_password="pass",
    db_host="host",
    db_port=3306,
    db_database="dbname",
    db_charset="utf8mb4"
)
```

### Parâmetros da Conexão

| parâmetro           | default | tipo                  | obrigatório |                          |
|---------------------|---------|-----------------------|-------------|--------------------------|
| `db_user`           | None    | string                | sim         | Nome do usuário          |
| `db_password`       | None    | string                | sim         | Senha                    |
| `db_host`           | None    | string                | sim         | Host                     |
| `db_port`           | None    | int                   | sim         | Porta                    |
| `db_database`       | None    | string                | sim         | Nome do banco            |
| `db_ssl`            | False   | boolean               | não         | Habilitar SSL            |
| `db_ssl_ca`         | None    | string                | não         | Certificado CA           |
| `db_ssl_cert`       | None    | string                | não         | Certificado              |
| `db_ssl_key`        | None    | string                | não         | Chave do certificado     |
| `db_charset`        | `utf8`  | string                | não         | Charset do banco         |
| `log_level`         | `error` | string                | não         | Nível de log             |
| `pool`              | None    | MySQLConnectionPool   | não         | Pool de conexões         |
| `uncountable_words` | None    | list                  | não         | Palavras não contáveis   |
| `irregular_words`   | None    | dict                  | não         | Palavras irregulares     |

### Conexão via Context Manager

A conexão suporta o protocolo de gerenciador de contexto (`with`). Em caso de exceção, o rollback é executado automaticamente e a conexão é fechada.

```python
with bravaorm.Connection(db_user="root", db_password="pass", db_host="host", db_port=3306, db_database="dbname") as conn:
    produto = Produto(prod_nome="Exemplo", prod_preco=99.90)
    conn.add(produto)
    conn.save()
# conn.close() é chamado automaticamente ao sair do bloco
```

---

## Pool de Conexões

O BravaORM suporta pool de conexões MySQL via `MySQLConnectionPool`. Configure o pool **uma única vez** no startup da aplicação e passe o pool para cada instância de `Connection`.

```python
import bravaorm

# No startup da aplicação (executado uma vez)
bravaorm.configure_pool(
    pool_name="main",
    pool_size=10,
    user="root",
    password="pass",
    host="host",
    port=3306,
    database="dbname"
)

# Em cada requisição/thread
pool = bravaorm.get_pool("main")
with bravaorm.Connection(pool=pool) as conn:
    produtos = conn.produtos.all

# Para liberar o pool (ex: shutdown da aplicação)
bravaorm.release_pool("main")
```

### Funções do Pool

| função                                           | descrição                                         |
|--------------------------------------------------|---------------------------------------------------|
| `configure_pool(pool_name, pool_size, **config)` | Configura o pool globalmente. Idempotente.        |
| `get_pool(pool_name)`                            | Retorna o pool configurado. Lança `RuntimeError` se não encontrado. |
| `release_pool(pool_name)`                        | Remove a referência ao pool.                      |

> **Nota para RDS Proxy (AWS):** O RDS Proxy pina a conexão de backend enquanto houver uma transação aberta. Sempre termine operações com `save()` (commit) ou `rollback()` seguido de `close()` o mais rápido possível.

---

## Gerando Modelo de Entidade

O `Make()` conecta ao banco de dados, lê o `information_schema` e gera automaticamente as classes Python a partir das tabelas, colunas e chaves estrangeiras.

```python
import os
import bravaorm

bravaorm.Make(
    dir=os.path.dirname(os.path.abspath(__file__)),
    db_user="user",
    db_password="pass",
    db_host="host",
    db_port=3306,
    db_database="dbname"
)
```

### Parâmetros do Make()

| parâmetro           | default                    | tipo   | obrigatório |                                                    |
|---------------------|----------------------------|--------|-------------|----------------------------------------------------|
| `dir`               | —                          | string | sim         | Diretório raiz do projeto                          |
| `db_user`           | —                          | string | sim         | Nome do usuário                                    |
| `db_password`       | —                          | string | sim         | Senha                                              |
| `db_host`           | —                          | string | sim         | Host                                               |
| `db_port`           | —                          | int    | sim         | Porta                                              |
| `db_database`       | —                          | string | sim         | Nome do banco                                      |
| `db_ssl`            | False                      | bool   | não         | Habilitar SSL                                      |
| `date_format`       | `"%d/%m/%Y %H:%M:%S"`      | string | não         | Formato de datas geradas nos modelos               |
| `field_types`       | None                       | dict   | não         | Mapeamento manual `{campo: "Tipo(args)"}` por nome |
| `uncountable_words` | None                       | list   | não         | Palavras não contáveis para o inflector            |
| `irregular_words`   | None                       | dict   | não         | Palavras irregulares para o inflector              |

### Estrutura Gerada

O script gera dois diretórios: `model/lib/` (gerado automaticamente, **não editar**) e `model/` (camada de customização, editável):

```
.
├── model/
│   ├── __init__.py
│   ├── produto.py              # Stub editável — herda de model/lib/produto.py
│   ├── categoria.py
│   └── lib/
│       ├── __init__.py
│       ├── produto.py          # Gerado pelo Make() — regenerado a cada execução
│       └── categoria.py
└── ...
```

> **Convenção de nomes:** As tabelas devem usar o nome no plural (ex: `produtos`) e o ORM deriva o nome da classe no singular (`Produto`) via inflector.

### Exemplo de Classe Gerada

```python
# model/lib/produto.py  (gerado automaticamente pelo Make)
# -*- coding: utf-8 -*-
from bravaorm.entity import *

class Produto(Entity):

    def __init__(cls, **kw):
        cls.__metadata__ = {'pk': ['id']}

        # FIELDS
        cls.id             = Int(pk=True, auto_increment=True, not_null=True, precision=10, scale=0)
        cls.id_categoria   = Int(fk=True, not_null=True, precision=10, scale=0)
        cls.prod_nome      = String(max=155)
        cls.prod_preco     = Decimal(not_null=True, precision=19, scale=2)
        cls.prod_ativo     = Boolean()
        cls.prod_fabricado = DateTime(format='%d/%m/%Y')
        cls.prod_alterado  = DateTime(format='%d/%m/%Y %H:%M:%S')

        # One-to-One
        cls.categorias = Obj(
            context=cls, keyname='categorias',
            name='Categoria', key='id',
            reference='id_categoria', table='categorias'
        )

        # One-to-many
        cls.compras = ObjList(
            context=cls, keyname='compras',
            name='Compra', key='id_produto',
            reference='id', table='compras'
        )

        # Many-to-many
        cls.tags = ObjListOfMany(
            context=cls, keyname='tags',
            name='Tag', reference='id',
            intermediate='produto_tags',
            ref_key='id_produto', rel_key='id_tag',
            table='tags', key='id'
        )

        super().__init__(**kw)
```

```python
# model/produto.py  (stub editável — customizações aqui)
# -*- coding: utf-8 -*-
from bravaorm.entity.datatype import *
from .lib import Produto

class Produto(Produto):

    def __init__(cls, **kw):
        return super(Produto, cls).__init__(**kw)
```

---

## Tipos de Dados

Os campos de uma entidade são declarados com tipos que espelham os tipos do banco de dados e realizam validação automática na atribuição.

| Tipo           | Python equivalente | Descrição                                      |
|----------------|--------------------|------------------------------------------------|
| `String`       | `str`              | Texto. Aceita `max` para limite de caracteres  |
| `Int`          | `int`              | Inteiro. Converte automaticamente se possível  |
| `Decimal`      | `decimal.Decimal`  | Decimal de precisão fixa                       |
| `Float`        | `float`            | Ponto flutuante                                |
| `Boolean`      | `bool`             | Verdadeiro/Falso                               |
| `DateTime`     | `datetime.datetime`| Data e hora. Aceita string no formato definido |
| `Dict`         | `dict` / `list`    | Desserializa JSON (`str` → `dict`/`list`) na leitura, inclusive na hidratação de query |
| `Json`         | `str`              | Armazena e retorna como **string** JSON crua (LONGTEXT no banco); não desserializa      |
| `Obj`          | Entity             | Relacionamento **1:1** (FK nesta tabela)       |
| `ObjList`      | ListType           | Relacionamento **1:N** (FK na tabela filha)    |
| `ObjListOfMany`| ListType           | Relacionamento **N:M** (tabela intermediária)  |

> **Hidratação de `Dict` × `Json` (desde 0.0.38):** campos `Dict()` são desserializados na leitura — inclusive na **hidratação de query** (`.first`/`.all`) —, retornando `dict`/`list`. O fast-path de hidratação de alta performance faz essa conversão apenas para `Dict` (os demais tipos passam direto, sem custo). Campos `Json()` permanecem como **string** JSON crua por design (use-os quando quiser o JSON sem parse).

---

## Referência da API

### Saída / Resultados

| método / propriedade | aplicável       | resultado                                              |
|----------------------|-----------------|--------------------------------------------------------|
| `.first`             | Connection      | Primeiro objeto do SELECT (ou `None`)                  |
| `.all`               | Connection      | Lista de objetos do SELECT                             |
| `.fetch`             | Connection      | Lista de `dict` sem conversão para objetos             |
| `.count`             | Connection      | Inteiro com o total de registros                       |
| `.toJSON()`          | Entity, ListType| Objeto ou lista convertidos para `dict`                |

### Operações de Escrita

| método            | aplicável       | descrição                                              |
|-------------------|-----------------|--------------------------------------------------------|
| `.add(obj)`       | Connection      | Adiciona objeto à fila de inserção/atualização         |
| `.save()`         | Connection      | Persiste toda a fila no banco (INSERT/UPDATE + commit) |
| `.delete(obj)`    | Connection      | Remove objeto pelo PK (requer `.save()`)               |
| `.delete()`       | Connection      | DELETE com condição WHERE (sem `.save()`)              |
| `.set(campo, val)`| Connection      | UPDATE de um único campo com WHERE                     |
| `.update(**kw)`   | Connection      | UPDATE de múltiplos campos com WHERE                   |
| `.rollback()`     | Connection      | Desfaz transação e limpa a fila                        |

### Construtores do Query Builder

| método              | descrição                                              |
|---------------------|--------------------------------------------------------|
| `.where(*cláusula)` | Condição AND principal                                 |
| `.orwhere(*cláusula)`| Bloco OR adicional (requer `.where()` prévio)         |
| `.select(*campos)`  | Campos específicos a selecionar (aceita `tabela.*`)    |
| `.distinct(*campos)`| Adiciona `DISTINCT(campos)` ao SELECT                  |
| `.alias(expr, nome)`| Cria alias para campo ou expressão                     |
| `.orderby(*campos)` | Ordenação (`campo ASC/DESC`)                           |
| `.groupby(*campos)` | Agrupamento                                            |
| `.having(*cláusula)`| Condição HAVING após GROUP BY                          |
| `.orhaving(*cláusula)`| Bloco OR adicional para HAVING                       |
| `.limit(inicio, fim)`| Limite de registros                                  |
| `.join(*tabelas)`   | LEFT JOIN por relacionamento definido na entidade      |
| `.inner(*tabelas)`  | INNER JOIN por relacionamento definido na entidade     |
| `.include(*tabelas)`| Eager loading 1:N e N:M (bulk IN, sem N+1)             |
| `.on(sel,tipo,tab,cond)`| JOIN personalizado sem relacionamento pré-definido |
| `.execute(sql, args, class_name)`| Query SQL direta                          |

---

## Seleção de Objetos

```python
produto = conn.produtos.where("id = 10").first
print(produto.toJSON())
# {'id': 10, 'id_categoria': 3, 'prod_nome': 'Exemplo', 'prod_preco': Decimal('99.90'), ...}
```

### Condição OR

`orwhere` depende de um `where` anterior e cria um bloco `OR` a cada chamada.

```python
produtos = conn.produtos.where("id = 10").orwhere("id = 12").orwhere("id = 14").all
```

SQL gerado: `WHERE (id = 10) OR (id = 12) OR (id = 14)`

### Seleção de Campos Específicos

```python
produto = conn.produtos.where("id = 10").select("id, prod_nome").first
# {'id': 10, 'prod_nome': 'Exemplo'}
```

Wildcard por tabela:

```python
# Seleciona todos os campos de produtos + apenas id e nome de categorias
produto = conn.produtos.join("categorias").select("produtos.*, categorias.id, categorias.cat_nome").first
```

### Alias

```python
produto = conn.produtos.alias("prod_nome", "nome").where("id = 10").first
print(produto["nome"])   # Exemplo
print(produto.prod_nome) # Exemplo
```

> Aliases são campos somente leitura.

### Distinct

```python
categorias = conn.produtos.distinct("id_categoria").all
```

### Ordenamento

```python
produtos = conn.produtos.orderby("prod_nome ASC").all
produtos = conn.produtos.orderby("prod_preco DESC").all
```

### Agrupamento e Having

```python
# Agrupamento simples
resultado = conn.produtos.groupby("id_categoria").all

# Com HAVING
resultado = conn.produtos.groupby("id_categoria").having("COUNT(*) > 5").all
```

### Limite

```python
produtos = conn.produtos.orderby("prod_nome").limit(0, 10).all
```

---

## Relacionamentos

### join — LEFT JOIN (1:1 / N:1)

Equivalente ao `LEFT JOIN`. Recomendado quando o relacionamento retorna **um único resultado** por linha principal.

```python
produtos = conn.produtos.join("categorias").all
# {'id': 10, ..., 'categorias': {'id': 3, 'cat_nome': 'Categoria Teste'}}
```

### inner — INNER JOIN (1:1 / N:1)

Retorna apenas os registros que possuem correspondência na tabela relacionada.

```python
produtos = conn.produtos.inner("categorias").where("categorias.id = 1").all
```

### include — Eager Loading (1:N e N:M)

Busca os objetos relacionados com uma query `IN` única por relacionamento, **sem problema N+1**. Recomendado para coleções.

```python
produto = conn.produtos.include("compras").where("id = 10, compras.compra_paga = 1").first
# {'id': 10, ..., 'compras': [{'id': 1, ...}, {'id': 23, ...}]}
```

Multiple includes:

```python
produtos = conn.produtos.include("compras, tags").all
```

### on — JOIN Personalizado

Permite JOINs com tabelas sem relacionamento definido na entidade. Aceita qualquer tipo (`LEFT`, `RIGHT`, `INNER`).

Parâmetros: `select` (campos a selecionar), `jointype` (`left`/`right`/`inner`), `table` (nome da tabela), `condition` (condição ON).

```python
produto = conn.produtos.on(
    "cupons.cod_cupom, cupons.cup_preco_max",
    "left",
    "cupons",
    "cupons.cup_preco_max >= produtos.prod_preco"
).where("NOT cupons.id IS NULL").all
```

---

## Fetch (Resultados sem Objetos)

Retorna diretamente a lista de `dict` do banco, sem instanciar objetos. Indicado para leituras de alta performance onde o dado será serializado imediatamente.

```python
produtos = conn.produtos.where("prod_ativo = 1").fetch
# [{'id': 10, 'prod_nome': 'Exemplo', ...}, ...]
```

---

## Count

```python
total = conn.produtos.where("prod_ativo = 1").count
# 42
```

---

## Criação de Objetos

### Simples

```python
from model import Produto

produto = Produto()
produto.prod_nome = "Exemplo"
produto.prod_preco = 99.90

conn.add(produto)
conn.save()
```

Via construtor:

```python
produto = Produto(prod_nome="Exemplo", prod_preco=99.90)
conn.add(produto)
conn.save()
```

### Com Relacionamentos (1:N)

```python
from model import Produto, ProdutoFoto

produto = Produto(prod_nome="Exemplo", prod_preco=99.90)

produto.produto_fotos.add(ProdutoFoto(foto_descricao="Vista Frontal", foto_arquivo="frontal.jpg"))
produto.produto_fotos.add(ProdutoFoto(foto_descricao="Vista Lateral", foto_arquivo="lateral.jpg"))

conn.add(produto)
conn.save()
```

> O ORM persiste primeiro o objeto pai, captura o `lastrowid` e propaga automaticamente a FK para os filhos antes de salvá-los.

---

## Atualização de Objetos

> **Auto-enfileiramento ao modificar:** alterar um campo de uma entidade **persistível** (com PK) já a adiciona à fila do `save()` — o `conn.add()` explícito é opcional nesses casos. Entidades **sem PK** (ex.: linhas de VIEW, usadas só para leitura/moldar a resposta) **não** são enfileiradas nem podem ser persistidas via `add()`/`save()`.

### Por Objeto

```python
produto = conn.produtos.where("id = 10").first
produto.prod_preco = 89.90
conn.add(produto)
conn.save()
```

### Em Lote

```python
produtos = conn.produtos.where("prod_preco >= 100").all
for produto in produtos:
    produto.prod_preco = produto.prod_preco * 0.9
    conn.add(produto)
conn.save()
```

### Via Relacionamento

```python
categoria = conn.categorias.include("produtos").where("id = 1, produtos.prod_preco >= 100").first
for produto in categoria.produtos:
    produto.prod_preco = produto.prod_preco * 0.9
    conn.add(produto)
conn.save()
```

---

## Update Query (SQL Direto)

Para atualizações em massa sem instanciar objetos:

```python
# Atualizar um campo
conn.produtos.where("prod_ativo = 0").set("prod_ativo", 1)

# Atualizar múltiplos campos
conn.produtos.where("prod_ativo = 0").update(prod_ativo=1, prod_promo=0)
```

---

## Exclusão de Objetos

```python
# Por objeto
conn.delete(produto)
conn.save()

# Por condição (executa imediatamente, sem save())
conn.produtos.where("prod_preco = 0").delete()
```

---

## Execute Query (SQL Personalizado)

Para queries complexas que não se enquadram no query builder:

```python
# Retorna lista de objetos Produto
produtos = conn.execute(
    "SELECT * FROM produtos WHERE prod_preco > %(min_preco)s",
    args={"min_preco": 100},
    class_name="Produto"
)

# Retorna lista de dict
resultado = conn.execute("SELECT COUNT(*) as total FROM produtos")
```

O parâmetro `args` aceita um dicionário com placeholders `%(chave)s` para evitar SQL injection.

---

## Rollback

```python
try:
    produto = Produto(prod_nome="Novo")
    conn.add(produto)
    conn.save()
except Exception:
    conn.rollback()
    raise
finally:
    conn.close()
```

Com context manager o rollback é automático em caso de exceção.

---

## toJSON()

Converte objeto ou lista de objetos para `dict`, incluindo relacionamentos carregados:

```python
produto = conn.produtos.include("compras").where("id = 10").first
print(produto.toJSON())
# {
#   'id': 10, 'prod_nome': 'Exemplo', 'prod_preco': Decimal('99.90'),
#   'compras': [{'id': 1, 'id_produto': 10, ...}, ...]
# }

lista = conn.produtos.all
print(lista.toJSON())  # lista de dict
```

---

## Gerando Modelo TypeScript (Angular)

O `Angular()` conecta ao banco e gera classes TypeScript espelhando a estrutura do banco de dados, para uso em projetos Angular ou qualquer frontend TypeScript.

```python
import os
import bravaorm

bravaorm.Angular(
    dir=os.path.join(os.path.dirname(os.path.abspath(__file__)), "src/model"),
    db_user="user",
    db_password="pass",
    db_host="host",
    db_port=3306,
    db_database="dbname"
)
```

Gera arquivos `.ts` no diretório especificado e um `index.ts` de barrel export.

---

## Arquitetura

```
bravaorm/
├── __init__.py                 # API pública: Connection, Make, Angular,
│                               #   configure_pool, get_pool, release_pool
│
├── context/
│   ├── connection.py           # Connection — query builder + unit-of-work
│   ├── database.py             # DataBase — wrapper mysql.connector
│   └── pool.py                 # configure_pool / get_pool / release_pool
│
├── entity/
│   ├── entity.py               # Entity — classe base de todos os modelos
│   └── datatype.py             # Tipos: String, Int, Decimal, Float, Boolean,
│                               #   DateTime, Dict, Json, Obj, ObjList,
│                               #   ObjListOfMany, ListType
│
└── utils/
    ├── make.py                 # Make() — geração de código Python a partir do DB
    ├── angular.py              # Angular() — geração de código TypeScript
    ├── inflector/              # Inflector — pluralização/singularização
    │   └── languages/          # Suporte: Português, Inglês, Espanhol
    └── log/                    # Logger colorido com níveis debug/error
```

### Fluxo de uma Query de Leitura

```
conn.produtos                     → Connection.__getattr__ carrega model.produto.Produto
  .where("ativo = 1")             → armazena cláusula formatada
  .include("compras")             → declara eager loading
  .all                            → monta SQL, chama DataBase.fetchall()
                                    → hidrata lista de Produto
                                    → busca Compra via IN (bulk, sem N+1)
                                    → chama entity.add() para anexar filhos
```

### Fluxo de uma Operação de Escrita

```
conn.add(produto)                 → enfileira em __queue__['add']
conn.save()                       → __save__object__():
                                      objetos COM pk → executemany() (bulk)
                                      objetos SEM pk → save() individual (captura lastrowid)
                                    → DataBase.commit()
```

---

## Status do Objeto

Cada entidade possui um `__metadata__['status']` que reflete seu ciclo de vida:

| status     | quando                                   |
|------------|------------------------------------------|
| `created`  | Após instanciação                        |
| `modified` | Após qualquer alteração de campo         |
| `inserted` | Após commit de nova inserção             |
| `updated`  | Após commit de atualização               |
| `deleted`  | Após commit de exclusão                  |
| `loaded`   | Após hidratação via `.all` / `.first`    |

---

## Inflector

O Inflector realiza a conversão automática entre nome de tabela e nome de classe:

- `classify("produtos")` → `"Produto"`
- `tableize("Produto")` → `"produtos"`

Aceita vocabulário customizado:

```python
conn = bravaorm.Connection(
    ...,
    uncountable_words=["status", "lms"],
    irregular_words={"perfil": "perfis", "raiz": "raizes"}
)
```

---

## License

MIT

Copyright (c) 2019-2026 Roberto Neves. All rights reserved. robertonsilva@gmail.com
