ORM async minimalista (ActiveRecord) para MariaDB/MySQL e PostgreSQL — inspirado no DataLayer PHP de Robson V. Leite.
Project description
aiodatalayer
ORM mínimo no estilo ActiveRecord, totalmente assíncrono, inspirado no DataLayer (PHP) do Robson V. Leite / CoffeeCode. Suporte nativo a MariaDB/MySQL (asyncmy) e PostgreSQL (asyncpg), zero configuração no código — tudo via variáveis de ambiente.
Pacote PyPI:
aiodatalayer· Import:from datalayer import DataLayer
O DataLayer é uma camada de persistência que abstrai o CRUD do seu banco. Você modela uma tabela em poucas linhas e ganha leitura, escrita, atualização e remoção encadeáveis e seguras.
Highlights
- API encadeável e enxuta (
find().order().limit().fetch()) - INSERT/UPDATE automático no
save() - Validação de campos obrigatórios
- Timestamps automáticos (
created_at/updated_at) - Conversão de
datetime/date/timepara ISO 8601 na leitura - Timezone configurável (default
America/Sao_Paulo) - Suporte multi-driver (MariaDB + Postgres) via mesma interface
- Exceções tipadas (
ValidationError,ConnectionError,QueryError) - 100% async/await
- Cobertura de testes com driver fake (sem precisar de banco real)
Instalação
pip install aiodatalayer
Ou com uv:
uv add aiodatalayer
Para desenvolvimento local:
uv pip install -e ".[dev]"
Dependências
| Pacote | Finalidade |
|---|---|
asyncmy |
Driver async MariaDB/MySQL |
asyncpg |
Driver async PostgreSQL |
Configuração
Crie um .env na raiz do projeto:
DATA_LAYER_DRIVER=mariadb # "mariadb" ou "postgres"
DATA_LAYER_HOST=localhost
DATA_LAYER_PORT=3306 # 3306 mariadb | 5432 postgres
DATA_LAYER_DATABASE=meu_banco
DATA_LAYER_USER=app
DATA_LAYER_PASSWORD=secret
DATA_LAYER_POOL_MIN=1 # opcional, default 1
DATA_LAYER_POOL_MAX=10 # opcional, default 10
DATA_LAYER_TIMEZONE=America/Sao_Paulo # opcional, default America/Sao_Paulo
A lib lê as variáveis de ambiente diretamente e gerencia o pool internamente — não há connect() ou disconnect() explícito. Você carrega o .env da forma que preferir (python-dotenv, os.environ, Docker, shell export, etc.):
from dotenv import load_dotenv
load_dotenv() # antes de importar datalayer
from datalayer import DataLayer
Subindo um MariaDB local com Docker
docker run -d \
--name datalayer-mariadb \
-p 3306:3306 \
-e MARIADB_ROOT_PASSWORD=root_password \
-e MARIADB_DATABASE=local_db \
-e MARIADB_USER=local_user \
-e MARIADB_PASSWORD=local_password \
-v datalayer-mariadb-data:/var/lib/mysql \
--restart unless-stopped \
mariadb:11
Definindo um Model
Você pode declarar via atributos de classe ou via super().__init__() — ambos funcionam.
from datalayer import DataLayer
class User(DataLayer):
table = "users"
required = ["first_name", "last_name", "email"]
primary_key = "id" # default: "id"
timestamps = True # default: True
ou
class User(DataLayer):
def __init__(self):
# table, required, primary_key, timestamps
super().__init__("users", ["first_name", "last_name"], "id", True)
Timestamps automáticos
Quando timestamps = True, a lib espera as colunas created_at e updated_at na tabela:
| Coluna | Tipo SQL | Comportamento |
|---|---|---|
created_at |
TIMESTAMP |
Preenchido no INSERT, nunca alterado |
updated_at |
TIMESTAMP |
Preenchido no INSERT, atualizado a cada UPDATE |
O valor é gerado em datetime.now(<timezone>) segundo a TZ configurada.
API
find(condition?, **params)
Monta a cláusula WHERE. Retorna self para encadeamento.
# todos os registros
await User().find().fetch(True)
# com filtro e named params
await User().find("id = :id", id=5).fetch()
await User().find("role = :role AND active = :active", role="admin", active=True).fetch(True)
Os placeholders :nome são traduzidos internamente para $1, $2… (Postgres) ou %s (MariaDB).
find_by_id(id)
Atalho para find("id = :id", id=id) já com fetch automático. Retorna o registro hidratado (ou None).
user = await User().find_by_id(2)
print(user.first_name)
fetch(all=False)
Executa o SELECT após find().
# um registro
user = await User().find("id = :id", id=5).fetch()
# todos os registros
users = await User().find().fetch(all=True)
# encadeamento completo
users = (
await User()
.find("role = :role", role="admin")
.order("name ASC")
.limit(10)
.fetch(all=True)
)
fetch()→ instância do model (ouNone)fetch(True)→list[dict](lista vazia se nada bater)
limit(n) / offset(n) / order(clause)
await User().find().order("name ASC").limit(10).offset(20).fetch(True)
await User().find().order("created_at DESC").fetch(True)
in_(column, values)
Cláusula IN. Nome com underscore para não conflitar com a keyword in do Python.
await User().find().in_("id", [1, 2, 3]).fetch(True)
# combinado com find
await User().find("role = :role", role="admin").in_("id", [1, 2, 3]).fetch(True)
columns(spec)
Restringe colunas do SELECT. Recebe string crua (sem escape, sem array). Default é *. Não afeta count().
await User().find().columns("id, name, email").fetch(True)
await User().find_by_id(1) # ainda *
await User().find("id = :id", id=1).columns("id, name").fetch()
# count ignora — sempre COUNT(*)
await User().find().columns("id").count()
count()
Total de registros da query atual.
total = await User().find().count()
admins = await User().find("role = :role", role="admin").count()
save()
INSERT ou UPDATE — decidido automaticamente pela presença do primary_key nos dados.
# CREATE
user = User()
user.first_name = "Robson"
user.last_name = "Leite"
user.email = "robson@exemplo.com"
saved = await user.save()
print(saved.id)
# UPDATE
user = await User().find_by_id(1)
user.first_name = "Robson V."
await user.save()
Comportamento:
- Valida
requiredantes da execução — lançaValidationError - INSERT popula
created_ateupdated_at - UPDATE atualiza apenas
updated_at - Re-busca o registro após a operação e retorna o model hidratado
destroy()
Remove o(s) registro(s) da query atual.
# por id
user = await User().find_by_id(5)
await user.destroy()
# em lote
await User().find("active = :active", active=False).destroy()
- Retorna
Truese ao menos um registro foi removido,Falsecaso contrário.
data()
Retorna um dict (cópia) com os dados da instância — útil para serialização.
user = await User().find_by_id(1)
print(user.data())
# {"id": 1, "first_name": "Robson", "created_at": "2026-04-29T09:00:00", ...}
Datas (datetime/date/time) são automaticamente convertidas para ISO 8601 na timezone configurada.
Métodos customizados no model
class User(DataLayer):
table = "users"
required = ["first_name", "last_name"]
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
user = await User().find_by_id(1)
print(user.full_name())
Encadeamento completo
Modificadores podem ser combinados em qualquer ordem antes de fetch():
users = (
await User()
.find("role = :role", role="admin")
.in_("id", [1, 2, 3, 4, 5])
.order("name ASC")
.limit(10)
.offset(0)
.fetch(all=True)
)
Timezone
Por padrão a lib opera em America/Sao_Paulo. Mude via .env:
DATA_LAYER_TIMEZONE=UTC
ou programaticamente:
from datalayer import Connection
Connection.set_timezone("Europe/Lisbon")
Aceita qualquer TZ válida no IANA tz database.
datetime aware retornado pelo driver é convertido para a TZ configurada antes do isoformat(). datetime naive é tratado como já estando na TZ configurada.
Tratamento de erros
from datalayer import (
DataLayerError,
ValidationError,
ConnectionError,
QueryError,
)
try:
await user.save()
except ValidationError as e:
print(e) # "Campo 'email' e obrigatorio"
except QueryError as e:
print(e) # erro do driver / SQL invalido
except DataLayerError as e:
print(e) # qualquer erro da lib
| Exception | Quando ocorre |
|---|---|
ValidationError |
Campo obrigatório ausente no save() |
ConnectionError |
Falha ao conectar / pool indisponível |
QueryError |
Erro de SQL retornado pelo driver |
Todas herdam de DataLayerError.
Testes
uv pip install -e ".[dev]"
uv run pytest
Os testes usam um driver em memória (tests/fake_driver.py) — não precisam de banco real.
50 passed in 0.07s
Arquitetura
datalayer/
├── __init__.py # carrega .env, expõe DataLayer + exceções
├── connection.py # singleton de pool, TZ, env vars
├── model.py # ActiveRecord base
├── exceptions.py
├── grammar/
│ ├── base.py # SQL builder + tradução de :name
│ ├── postgres.py # placeholder $1..$N
│ └── mariadb.py # placeholder %s
└── drivers/
├── base.py # AsyncDriver ABC
├── postgres.py # wrapper asyncpg
└── mariadb.py # wrapper asyncmy
Fluxo de uma query
User().find("role = :role", role="admin").order("name ASC").limit(10).fetch(True)
│
├─ find() → armazena condition + params
├─ order() → armazena ORDER BY
├─ limit() → armazena LIMIT
├─ fetch(True) → grammar.compile_select(...)
├─ grammar → traduz :role → $1 / %s, monta SQL
├─ driver → executa via pool, retorna rows
└─ fetch → normaliza datetime/date/time → ISO string
Fora do escopo (v1)
- JOINs e eager loading
- Migrations / schema builder
- Soft delete
- Hooks / eventos (
before_save,after_save) - Suporte a SQLite
Créditos
- Implementação Python: Kauê Leal
- Inspirado no DataLayer PHP de Robson V. Leite e UpInside Treinamentos
Licença
MIT.
Project details
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
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aiodatalayer-2.3.1.tar.gz.
File metadata
- Download URL: aiodatalayer-2.3.1.tar.gz
- Upload date:
- Size: 16.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
798fb0bfb71a66599808280c864f47cef6912c6b961b1a57c94dcadd2ae24dad
|
|
| MD5 |
6f1ec1b53df107bff053f51322304472
|
|
| BLAKE2b-256 |
644f9835ae3c32fdd61d91357aedd21c8bb87da39981f15ae4a84b7bb94d8954
|
Provenance
The following attestation bundles were made for aiodatalayer-2.3.1.tar.gz:
Publisher:
ci.yml on kauelima21/DataLayer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aiodatalayer-2.3.1.tar.gz -
Subject digest:
798fb0bfb71a66599808280c864f47cef6912c6b961b1a57c94dcadd2ae24dad - Sigstore transparency entry: 1435374009
- Sigstore integration time:
-
Permalink:
kauelima21/DataLayer@a34aa325a8513705feb828ffa8bf14a23c735977 -
Branch / Tag:
refs/tags/v2.3.1 - Owner: https://github.com/kauelima21
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@a34aa325a8513705feb828ffa8bf14a23c735977 -
Trigger Event:
push
-
Statement type:
File details
Details for the file aiodatalayer-2.3.1-py3-none-any.whl.
File metadata
- Download URL: aiodatalayer-2.3.1-py3-none-any.whl
- Upload date:
- Size: 16.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2d6573e9e5f383aaf0e800318fcb5cdb50bb868a02ce332aee0fa6967374d2f
|
|
| MD5 |
8df66f42334655baa859c95ad57f871a
|
|
| BLAKE2b-256 |
a4e8f1fffd4a0433c14c1c836b293b201559f84751efb766e9fdf0a0b173d4d3
|
Provenance
The following attestation bundles were made for aiodatalayer-2.3.1-py3-none-any.whl:
Publisher:
ci.yml on kauelima21/DataLayer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aiodatalayer-2.3.1-py3-none-any.whl -
Subject digest:
b2d6573e9e5f383aaf0e800318fcb5cdb50bb868a02ce332aee0fa6967374d2f - Sigstore transparency entry: 1435374060
- Sigstore integration time:
-
Permalink:
kauelima21/DataLayer@a34aa325a8513705feb828ffa8bf14a23c735977 -
Branch / Tag:
refs/tags/v2.3.1 - Owner: https://github.com/kauelima21
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@a34aa325a8513705feb828ffa8bf14a23c735977 -
Trigger Event:
push
-
Statement type: