Privacy-by-Design para dados tabulares — LGPD compliance em Python.
Project description
logus v1.0.4
Um vocabulário. Engine Polars. Privacidade LGPD.
logus é uma biblioteca Python para análise de dados com privacidade embutida.
Em vez de alternar entre pd.funcao, pl.funcao e funções de anonimização separadas,
você usa lg.* para tudo — internamente Polars quando disponível, pandas como fallback.
import logus as lg
# Lê qualquer formato — auto-detecta encoding e tipo de arquivo
df = lg.read("clientes.csv") # pl.DataFrame
df = lg.read("clientes.xlsx") # pl.DataFrame
df = lg.read("clientes.parquet") # pl.DataFrame
# Manipula com linguagem natural / SQL
df = lg.where(df, uf="SP", tipo_pessoa="PF")
df = lg.sort(df, "renda_mensal", desc=True)
df = lg.add_column(df, imposto=lg.col("renda_mensal") * 0.27)
# Detecta e mascara PII automaticamente (LGPD)
reports = lg.scan(df) # descobre CPF, e-mail, telefone...
df_safe = lg.mask(df, salt=SALT) # mascara com HMAC-SHA256
# Salva cifrado com AES-256-GCM
lg.store(df_safe, "clientes.lgs", key=KEY)
df_back = lg.read("clientes.lgs", key=KEY)
Instalação
pip install logus-lgpd
# Com Polars (recomendado — 2-20× mais rápido)
pip install "logus-lgpd[polars]"
# Com SQL via DuckDB
pip install "logus-lgpd[sql]"
# Tudo
pip install "logus-lgpd[full]"
Obrigatórias: pandas pyarrow cryptography numpy
Opcionais: polars (performance) duckdb (lg.sql) sqlalchemy (banco)
Índice
- Conceitos fundamentais
- Leitura de arquivos —
lg.read() - Filtragem —
lg.where() - Seleção e transformação de colunas
- Ordenação —
lg.sort() - Agrupamento —
lg.groupby() - Expressões —
lg.col() - CASE WHEN —
lg.when() - Pipeline fluente —
lg.pipe() - SQL direto —
lg.sql() - Privacidade —
lg.scan()elg.mask() - Formato
.lgs— arquivo seguro - Multi-frame e
lg.open() - JOIN seguro —
lg.join() - Diagnóstico —
lg.profile()elg.diff() - Verificação de privacidade —
lg.check - Streaming —
lg.stream() - Banco de dados —
lg.read_db()elink - Utilitários analíticos
- Escrita de arquivos —
lg.write() - Chaves e salt — segurança
- Referência completa da API
- Changelog
1. Conceitos fundamentais
Um tipo, uma linguagem
lg.read() retorna pl.DataFrame quando Polars está instalado. Todas as funções
lg.* preservam o tipo de entrada — se entrou pl.DataFrame, sai pl.DataFrame.
df = lg.read("arquivo.csv") # pl.DataFrame
df = lg.where(df, uf="SP") # pl.DataFrame (não converte para pandas)
df = lg.groupby(df, "uf", {...}) # pl.DataFrame
df = lg.mask(df, salt=SALT) # pl.DataFrame
Se você passar pd.DataFrame, recebe pd.DataFrame de volta — zero round-trips.
Engine duplo
| Operação | Engine primário | Fallback |
|---|---|---|
| Leitura CSV/Parquet/JSON/IPC/Arrow | Polars | pandas |
| Leitura Excel/SAS/SPSS/Stata/HDF5 | pandas → converte para pl.DataFrame | pandas |
where, sort, groupby, select |
Polars nativo | pandas |
| Mascaramento hash (HMAC-SHA256) | pandas dedup + map() | — |
| Mascaramento CEP, telefone, data | Polars vetorizado | — |
lg.sql() |
DuckDB (zero-copy Arrow) | — |
Nomenclatura SQL/natural
logus usa nomes que fazem sentido sem saber pandas nem Polars:
| SQL | logus | Aliases |
|---|---|---|
WHERE |
lg.where() |
lg.q() lg.filter_() |
SELECT col1, col2 |
lg.select() |
— |
SELECT * EXCEPT col |
lg.drop() |
— |
ORDER BY col DESC |
lg.sort(desc=True) |
lg.order_by() |
GROUP BY ... HAVING |
lg.groupby(having=) |
lg.group_by() |
SELECT DISTINCT |
lg.unique() |
lg.distinct() lg.drop_duplicates() |
LIMIT N |
lg.head(N) |
lg.limit() |
UNION ALL |
lg.concat() |
lg.union_all() |
COALESCE(col, val) |
lg.fill_null(val) |
lg.fillna() lg.coalesce() |
CAST(col AS tipo) |
lg.cast({'col':'tipo'}) |
— |
CASE WHEN |
lg.when(...).otherwise(...) |
— |
SELECT *, expr AS col |
lg.add_column(col=expr) |
lg.assign() lg.with_column() |
TOP N PER GROUP |
lg.top_n(N, col, group_by=) |
— |
2. Leitura de arquivos — lg.read()
Auto-detecta formato pela extensão. Auto-detecta encoding (UTF-8, Latin-1, CP-1252).
df = lg.read("clientes.csv")
df = lg.read("clientes.xlsx")
df = lg.read("clientes.parquet")
df = lg.read("clientes.json")
df = lg.read("clientes.feather")
df = lg.read("clientes.lgs", key=KEY) # arquivo criptografado logus
# Parâmetros opcionais
df = lg.read("arquivo.csv", sep=";") # separador customizado
df = lg.read("arquivo.csv", encoding="latin-1") # encoding explícito (auto se omitido)
df = lg.read("arquivo.lgs", key=KEY, salt=SALT) # descriptografa + mascara
df = lg.read(df_existente) # passthrough de DataFrame
Formatos suportados:
| Extensão | Engine | Notas |
|---|---|---|
.csv .tsv .txt |
Polars → pandas fallback | Auto-detecta encoding |
.parquet |
Polars | Mais rápido (colunar, comprimido) |
.json .ndjson .jsonl |
Polars | |
.feather .ipc .arrow |
Polars | Zero-copy |
.avro .orc |
Polars | |
.xlsx .xls .ods |
pandas | Requer openpyxl/xlrd |
.xml .html |
pandas | |
.dta |
pandas | Stata |
.sas7bdat .xpt |
pandas | SAS |
.sav .zsav |
pandas | SPSS |
.pkl .hdf .h5 |
pandas | |
.lgs |
logus (AES-256-GCM) | Formato nativo |
lg.read() vs mascaramento
# Sem salt → retorna dado bruto (como pandas/polars)
df = lg.read("clientes.csv")
# Com salt → mascara PII automaticamente ao ler
df = lg.read("clientes.csv", salt=SALT)
# Arquivo .lgs sem salt → descriptografa, retorna bruto
df = lg.read("clientes.lgs", key=KEY)
# Arquivo .lgs com salt → descriptografa + mascara
df = lg.read("clientes.lgs", key=KEY, salt=SALT)
3. Filtragem — lg.where()
# kwargs — mais natural
lg.where(df, uf="SP")
lg.where(df, uf="SP", tipo_pessoa="PF")
# Lista → IS IN
lg.where(df, uf=["SP", "RJ", "MG"])
# Range → BETWEEN
lg.where(df, renda_mensal=(5000, 15000))
# Operadores explícitos
lg.where(df, renda_mensal=(">", 5000))
lg.where(df, renda_mensal=(">=", 5000))
lg.where(df, uf=("!=", "SP"))
# String operations
lg.where(df, nome=("contains", "Silva"))
lg.where(df, nome=("startswith", "Ana"))
lg.where(df, email=("endswith", "@empresa.com"))
lg.where(df, nome=("like", "%Silva%")) # SQL LIKE (% = qualquer coisa)
lg.where(df, doc=("matches", r"\d{3}\.\d{3}")) # regex
# Null checks
lg.where(df, documento=("isnull",)) # IS NULL
lg.where(df, documento=("notnull",)) # IS NOT NULL
lg.where(df, documento=None) # atalho IS NULL
# lg.col() — sem import polars, com toda a expressividade
lg.where(df, lg.col("renda_mensal") > 5000)
lg.where(df, lg.col("renda_mensal").is_between(5000, 15000))
lg.where(df, lg.col("uf").is_in(["SP", "RJ"]))
lg.where(df, lg.col("nome").str.contains("Silva"))
lg.where(df, lg.col("nome").is_null())
lg.where(df, ~lg.col("inadimplente")) # NOT
lg.where(df, (lg.col("uf") == "SP") & (lg.col("renda_mensal") > 5000)) # AND
lg.where(df, (lg.col("uf") == "SP") | (lg.col("uf") == "RJ")) # OR
# String query (pandas .query())
lg.where(df, 'uf == "SP" and renda_mensal > 5000')
# Callable
lg.where(df, lambda d: d["uf"] == "SP")
# pl.Expr (quando já importou polars)
lg.where(df, pl.col("uf") == "SP")
# LazyFrame → retorna LazyFrame (sem materializar)
lf = pl.scan_parquet("grande.parquet")
lf_filtrado = lg.where(lf, uf="SP") # ainda lazy
df = lf_filtrado.collect() # materializa
Aliases: lg.q() (curto), lg.filter_() (não conflita com builtin filter)
4. Seleção e transformação de colunas
lg.select() — escolhe colunas
lg.select(df, "uf")
lg.select(df, ["uf", "renda_mensal", "tipo_pessoa"])
lg.select(df, lg.cols(df, "renda")) # colunas com "renda" no nome
lg.select(df, lg.cols(df, dtype="String")) # todas as strings
lg.drop() — remove colunas
lg.drop(df, "coluna_inutil")
lg.drop(df, ["col1", "col2"])
lg.drop(df, lg.cols(df, dtype="String", exclude=["nome"]))
lg.rename() — renomeia colunas
lg.rename(df, {"cpf": "documento", "renda": "renda_mensal"})
lg.add_column() — adiciona ou substitui colunas
Aceita lg.col(), lg.when(), pl.Expr, scalar, callable, array.
Executa todas as expressões em um único passo via Polars.
lg.add_column(df,
# Aritmética
imposto = lg.col("renda_mensal") * 0.27,
renda_liquida = lg.col("renda_mensal") * 0.73,
# CASE WHEN
faixa_renda = lg.when(lg.col("renda_mensal") > 10000, "alta")
.when(lg.col("renda_mensal") > 5000, "media")
.otherwise("baixa"),
# Window functions (sem import polars)
media_uf = lg.col("renda_mensal").mean().over("uf"),
rank_renda = lg.col("renda_mensal").rank("dense", descending=True),
# Acumulado
renda_acum = lg.col("renda_mensal").cum_sum(),
# LAG / LEAD
renda_anterior = lg.col("renda_mensal").shift(1),
# String ops
nome_lower = lg.col("nome").str.to_lowercase(),
nome_len = lg.col("nome").str.len_chars(),
# Date ops
ano_nasc = lg.col("data_nascimento").str.to_date("%Y-%m-%d").dt.year(),
# Constante
origem = "brasil",
# Callable (pandas fallback)
custom = lambda d: d["renda_mensal"] / d["idade"].clip(1, None),
)
Aliases: lg.assign() (pandas), lg.with_column() (singular)
lg.cast() — converte tipos
lg.cast(df, {"renda_mensal": "float32", "idade": "int32"})
lg.cast(df, {"flag": "bool", "data": "date"})
lg.cast(df, {"categoria": "categorical"})
Tipos: int int8 int16 int32 int64 uint8..uint64 float float32 float64 str string bool date datetime categorical
lg.fill_null() — preenche nulos
lg.fill_null(df, 0) # todos os nulos → 0
lg.fill_null(df, {"renda_mensal": 0, "uf": "N/A"}) # por coluna
lg.fill_null(df, "forward") # propaga valor anterior
lg.fill_null(df, "backward") # propaga valor seguinte
Aliases: lg.fillna() (pandas), lg.coalesce() (SQL)
lg.clip() — recorta valores
lg.clip(df, {"renda_mensal": (1320, 500_000), "idade": (0, 120)})
lg.clip(df, {"renda": (0, None)}) # só mínimo
lg.apply() — aplica função a colunas
lg.apply(df, {"uf": str.upper, "email": str.lower, "nome": str.title})
lg.apply(df, {"renda_mensal": lambda v: round(v, 2)})
lg.cols() — seletor de colunas por padrão
lg.cols(df, "renda") # colunas com "renda" no nome
lg.cols(df, ["renda", "idade"]) # múltiplos padrões
lg.cols(df, dtype="String") # todas as strings
lg.cols(df, dtype="Float64") # todas as floats
lg.cols(df, dtype="String", exclude=["nome"]) # strings exceto nome
# Combina com select, drop:
lg.select(df, lg.cols(df, "renda"))
lg.drop(df, lg.cols(df, dtype="Boolean"))
5. Ordenação — lg.sort()
lg.sort(df, "renda_mensal") # crescente
lg.sort(df, "renda_mensal", desc=True) # decrescente
lg.sort(df, ["uf", "renda_mensal"]) # multi-coluna crescente
lg.sort(df, ["uf", "renda_mensal"], desc=True) # ambas decrescentes
lg.sort(df, ["uf", "renda_mensal"], ascending=[True, False]) # misto
lg.sort(df, "renda_mensal", nulls_last=False) # nulos no início
Alias SQL: lg.order_by()
6. Agrupamento — lg.groupby()
# Básico
lg.groupby(df, "uf", {"renda_mensal": "mean"})
# Múltiplas funções
lg.groupby(df, "uf", {"renda_mensal": ["mean", "sum", "min", "max"]})
# Coluna origem diferente do nome resultado
lg.groupby(df, "uf", {
"media_renda": ("renda_mensal", "mean"),
"total": ("*", "count"),
"n_pj": ("tipo_pessoa", "count"),
})
# Multi-coluna by
lg.groupby(df, ["uf", "tipo_pessoa"], {"renda_mensal": "mean"})
# HAVING + ORDER BY + LIMIT em um passo
lg.groupby(df, "uf",
{"media": ("renda_mensal", "mean"), "n": ("*", "count")},
having = {"n": (">", 100)},
sort = "media",
desc = True,
limit = 10,
)
Funções de agregação: mean sum min max count std var first last n_unique median
Alias: lg.group_by()
lg.unique() — remove duplicatas
lg.unique(df) # linhas completamente únicas
lg.unique(df, "cpf") # uma linha por CPF
lg.unique(df, ["uf", "tipo"]) # uma linha por combinação
lg.unique(df, "cpf", keep="last") # mantém a última ocorrência
lg.unique(df, "cpf", keep="none") # remove TODAS as duplicatas
Aliases: lg.distinct() (SQL), lg.drop_duplicates() (pandas)
lg.top_n() — top N por grupo
lg.top_n(df, 3, "renda_mensal") # top 3 global
lg.top_n(df, 3, "renda_mensal", group_by="uf") # top 3 por UF
lg.top_n(df, 3, "renda_mensal", group_by="uf", desc=False) # bottom 3
Equivalente SQL: SELECT *, RANK() OVER (PARTITION BY uf ORDER BY renda DESC) AS r WHERE r <= 3
lg.concat() — combina DataFrames
lg.concat([df_jan, df_fev, df_mar]) # empilha linhas (UNION ALL)
lg.concat([df_a, df_b], axis=1) # combina colunas
lg.concat([df_polars, df_pandas]) # mistura tipos — retorna pl.DataFrame
Alias SQL: lg.union_all()
7. Expressões — lg.col()
lg.col() retorna pl.col() quando Polars está instalado — acesso a todos os
219 métodos nativos sem import polars.
# Comparação
lg.col("uf") == "SP"
lg.col("uf") != "RJ"
lg.col("renda_mensal") > 5000
lg.col("renda_mensal").is_between(5000, 15000)
# Membership
lg.col("uf").is_in(["SP", "RJ", "MG"])
lg.col("doc").is_null()
lg.col("doc").is_not_null()
# String (via Polars .str namespace)
lg.col("nome").str.contains("Silva")
lg.col("nome").str.starts_with("Ana")
lg.col("email").str.ends_with("@empresa.com")
lg.col("nome").str.to_lowercase()
lg.col("nome").str.to_uppercase()
lg.col("nome").str.len_chars()
lg.col("cpf").str.replace_all(r"\D", "") # remove não-dígitos
lg.col("cpf").str.slice(0, 3) # substring
# Data/hora (via Polars .dt namespace)
lg.col("data_nascimento").str.to_date("%Y-%m-%d").dt.year()
lg.col("data_nascimento").str.to_date("%Y-%m-%d").dt.month()
lg.col("data_nascimento").str.to_date("%Y-%m-%d").dt.day()
lg.col("ts").dt.truncate("1mo") # trunca para mês
# Aritmética
lg.col("renda_mensal") * 0.27
lg.col("renda_mensal") + lg.col("bonus")
lg.col("preco") - lg.col("desconto")
lg.col("valor") / lg.col("quantidade")
# Window functions
lg.col("renda_mensal").mean().over("uf") # média por UF
lg.col("renda_mensal").rank("dense", descending=True)
lg.col("renda_mensal").rank("dense").over("uf") # rank dentro do grupo
lg.col("renda_mensal").cum_sum() # acumulado
lg.col("renda_mensal").shift(1) # LAG(1)
lg.col("renda_mensal").shift(-1) # LEAD(1)
lg.col("renda_mensal").rolling_mean(window_size=7) # média móvel 7 dias
# Combinações lógicas
~lg.col("inadimplente") # NOT
(lg.col("uf") == "SP") & (lg.col("renda_mensal") > 5000) # AND
(lg.col("uf") == "SP") | (lg.col("uf") == "RJ") # OR
# Outros
lg.lit(42) # valor literal
lg.concat_str(["nome", "uf"], separator=" - ") # concatena strings
8. CASE WHEN — lg.when()
# CASE WHEN sem import polars
faixa = (
lg.when(lg.col("renda_mensal") > 10000, "alta")
.when(lg.col("renda_mensal") > 5000, "media")
.otherwise("baixa")
)
# Uso em add_column
df2 = lg.add_column(df,
faixa_renda = lg.when(lg.col("renda_mensal") > 10000, "alta")
.when(lg.col("renda_mensal") > 5000, "media")
.otherwise("baixa"),
categoria = lg.when(lg.col("inadimplente"), "devedor")
.otherwise("regular"),
adulto = lg.when(lg.col("idade") >= 18, True).otherwise(False),
)
# Com expressão comparando colunas
df3 = lg.add_column(df,
acima_media = lg.when(
lg.col("renda_mensal") > lg.col("renda_mensal").mean().over("uf"),
"acima"
).otherwise("abaixo")
)
9. Pipeline fluente — lg.pipe()
Evita variáveis temporárias. Encadeia operações de forma legível.
result = (
lg.pipe("clientes.parquet") # lê arquivo
.where(tipo_pessoa="PF", uf="SP") # filtra
.add_column(
imposto = lg.col("renda_mensal") * 0.27,
faixa = lg.when(lg.col("renda_mensal") > 10000, "alta")
.when(lg.col("renda_mensal") > 5000, "media")
.otherwise("baixa"),
media_uf = lg.col("renda_mensal").mean().over("uf"),
)
.mask(salt=SALT) # mascara PII
.groupby(
"faixa",
{"renda_media": ("renda_mensal", "mean"), "n": ("*", "count")},
having={"n": (">", 1000)},
sort="renda_media",
desc=True,
)
.collect() # retorna pl.DataFrame
)
# Com arquivo .lgs (descriptografa automaticamente)
result = (
lg.pipe("clientes.lgs", key=KEY)
.where(tipo_pessoa="PF")
.mask(salt=SALT)
.store("clientes_sp_masked.lgs", key=KEY) # salva sem .collect()
)
# Com SQL
result = (
lg.pipe()
.sql("SELECT * FROM read_parquet('dados.parquet') WHERE uf='SP'")
.mask(salt=SALT)
.collect()
)
Métodos disponíveis no pipeline:
read, sql, where, select, drop, rename, sort, groupby,
add_column, cast, fill_null, drop_duplicates, head, tail,
mask, scan, profile, store, collect, to_pandas, to_polars
10. SQL direto — lg.sql()
Executa SQL em DataFrames via DuckDB. Zero-copy via Arrow para pl.DataFrame.
# SQL em DataFrame em memória
result = lg.sql(
"SELECT uf, AVG(renda_mensal) AS media, COUNT(*) AS n "
"FROM df GROUP BY uf HAVING n > 100 ORDER BY media DESC",
df=df
)
# JOIN entre múltiplos DataFrames
result = lg.sql(
"SELECT c.uf, c.renda_mensal, p.valor "
"FROM clientes c JOIN pedidos p ON c.documento = p.documento",
clientes=df_clientes,
pedidos=df_pedidos,
)
# Lê Parquet/CSV diretamente com SQL (sem carregar em Python)
result = lg.sql(
"SELECT * FROM read_parquet('clientes.parquet') WHERE uf='SP'"
)
result = lg.sql(
"SELECT * FROM read_csv('dados.csv') WHERE renda > 5000"
)
# Arquivo .lgs descriptografado automaticamente
result = lg.sql(
"SELECT uf, COUNT(*) AS n FROM base GROUP BY uf",
base="clientes.lgs",
key=KEY,
)
# Com mascaramento PII no resultado
result = lg.sql("SELECT * FROM df", df=df, salt=SALT)
Requer:
pip install duckdb
11. Privacidade — lg.scan() e lg.mask()
lg.scan() — detecta PII
# Aceita arquivo, pd.DataFrame ou pl.DataFrame
reports = lg.scan(df)
reports = lg.scan("clientes.parquet")
reports = lg.scan("clientes.lgs", key=KEY)
# Relatório por coluna
for col, r in reports.items():
print(f"{col}: tipo={r.pii_type.value} risco={r.risk_level.value} estratégia={r.mask_strategy.value}")
# Tipos detectados automaticamente:
# cpf, cnpj, email, telefone, cep, data_nascimento, nome, rg, ip,
# cartao_credito, quasi_identifier, numerico, categorico
Estratégias de mascaramento por tipo:
| Tipo | Estratégia | Exemplo antes → depois |
|---|---|---|
| CPF / CNPJ | hash |
111.444.777-35 → 3425441ddfb8d1ec |
hash |
ana@empresa.com → 7a3f9c1d4e2b8f56 |
|
| Nome | redact |
Ana Silva → REDACTED |
| Telefone | mask_phone_ddd |
(11) 98765-4321 → (11) XXXXX-XXXX |
| CEP | truncate |
01310-100 → 01310-XXX |
| Data de nascimento | generalize_date |
1985-03-15 → 1980-1989 |
| UF / categoria | mock_category |
SP → RJ (aleatório) |
| Renda / numérico | mock_numeric |
5000.00 → 4937.12 (perturbado) |
lg.mask() — mascara PII
# Mascaramento completo (detecta e mascara automaticamente)
df_safe = lg.mask(df, salt=SALT)
# Colunas específicas
df_safe = lg.mask(df, salt=SALT, columns=["cpf", "email"])
# Excluir colunas
df_safe = lg.mask(df, salt=SALT, exclude=["uf", "tipo_pessoa"])
# Relatório do que foi mascarado
df_safe = lg.mask(df, salt=SALT, verbose=True)
# Aceita pd.DataFrame e pl.DataFrame — retorna o mesmo tipo
df_pl_safe = lg.mask(df_pl, salt=SALT) # recebe/retorna pl.DataFrame
df_pd_safe = lg.mask(df_pd, salt=SALT) # recebe/retorna pd.DataFrame
Determinismo: o mesmo CPF com o mesmo salt sempre gera o mesmo token. Essencial para JOINs entre tabelas mascaradas.
token_a = lg.mask(df_a, salt=SALT)["cpf"]
token_b = lg.mask(df_b, salt=SALT)["cpf"]
# token_a["111.444.777-35"] == token_b["111.444.777-35"] # sempre True
Normalização: diferentes formatações do mesmo CPF geram o mesmo token.
# "111.444.777-35", "11144477735", "111-444-777.35" → mesmo token
Nulos: None, "", "NaN", "none", "null" → mantidos como nulos (não hasheados).
lg.diff() — compara antes × depois
diff = lg.diff(df_original, df_mascarado)
print(diff["summary"])
print(diff["columns_changed"])
print(diff["columns_unchanged"])
12. Formato .lgs — arquivo seguro
AES-256-GCM com autenticação de integridade. Suporte a múltiplos DataFrames.
KEY = lg.generate_salt() # chave AES (256 bits)
SALT = lg.generate_salt() # salt HMAC (diferente da KEY!)
# Grava — variações
lg.store(df, "f.lgs", key=KEY) # cifra sem mascarar
lg.store(df, "f.lgs", key=KEY, salt=SALT) # mascara + cifra em 1 operação
lg.store(df, "f.lgs", anonymize=True) # só mascara, sem criptografia
lg.store(df, "f.lgs") # sem cripto (avisa se tiver PII)
# Com metadados
lg.store(df, "f.lgs", key=KEY, salt=SALT,
metadata={"origem": "crm", "versao": "3", "squad": "dados"},
overwrite=True)
# Lê
df = lg.read("f.lgs", key=KEY) # bruto
df = lg.read("f.lgs", key=KEY, salt=SALT) # + mascara
df = lg.read("f.lgs", key=KEY, raw=True) # sem mascara adicional
# Inspeciona sem descriptografar
info = lg.inspect("f.lgs", key=KEY)
print(info["content_type"]) # raw_dataframe | masked_dataframe | anonymous_dataframe
print(info["shape"])
print(info["metadata"])
# Rotação de chave (sem descriptografar em disco)
lg.rekey("f.lgs", old_key=KEY_ANTIGO, new_key=KEY_NOVO)
Aliases: lg.save = lg.store, lg.load = lg.read
13. Multi-frame e lg.open()
# Salva múltiplos DataFrames em um arquivo
lg.store(
{"clientes": df_clientes, "pedidos": df_pedidos, "pagamentos": df_pagamentos},
"base_producao.lgs",
key=KEY, salt=SALT,
metadata={"ambiente": "producao", "versao_schema": "4"},
)
# Lê todos os frames
frames = lg.read("base_producao.lgs", key=KEY)
df_clientes = frames["clientes"]
df_pedidos = frames["pedidos"]
# Lê um frame específico (sem carregar os demais)
df_c = lg.read("base_producao.lgs", key=KEY, frame="clientes")
# Context manager
with lg.open("base_producao.lgs", key=KEY) as f:
print(f.frame_names()) # ["clientes", "pedidos", "pagamentos"]
print(f.shape())
print(f.info())
df = f.read() # todos os frames
df = f.frame("clientes") # frame específico
f.add_frame("logs", df_logs) # adiciona frame
# Verificação de integridade
from logus.secure_file import SecureFile
info = SecureFile.verify("f.lgs", key=KEY)
if not info:
raise RuntimeError("Arquivo corrompido ou chave errada!")
print(info.content_type, info.shape, info.encryption)
14. JOIN seguro — lg.join()
lg.join() aplica o mesmo mascaramento em ambas as tabelas antes do JOIN,
garantindo que o CPF 111.444.777-35 vire o mesmo token nas duas.
# JOIN seguro (mascara antes de juntar)
resultado = lg.join(
df_clientes, df_pedidos,
on="cpf",
salt=SALT,
how="inner", # inner | left | right | full
)
# Com tabelas já mascaradas
resultado = lg.join(df_clientes_safe, df_pedidos_safe, on="cpf")
# lg.join() valida que os tokens são compatíveis (mesmo salt)
15. Diagnóstico — lg.profile() e lg.diff()
# Perfil completo (JSON-serializable)
report = lg.profile(df)
report = lg.profile("clientes.parquet")
report = lg.profile("clientes.lgs", key=KEY)
print(report["shape"]) # (1000000, 11)
print(report["pii_columns"]) # ["cpf", "email", "nome", ...]
print(report["n_pii_columns"]) # 6
print(report["pii_risk_summary"]) # {"high": 3, "medium": 2, "low": 1}
print(report["null_pct"]) # 2.4
print(report["nunique"]) # {"uf": 20, "cpf": 999619, ...}
# JSON para log / SIEM
import json
json_str = json.dumps(report, ensure_ascii=False)
# Diff antes × depois
diff = lg.diff(df_original, df_mascarado)
print(diff["summary"])
print(diff["columns_changed"])
# Info rápido (como df.info())
lg.info(df)
16. Verificação de privacidade — lg.check
df_safe = lg.mask(df, salt=SALT)
# k-anonimato (ANPD recomenda k >= 5)
r = lg.check.kanon(
df_safe,
quasi_identifiers=["uf", "idade", "tipo_pessoa"],
target_k=5,
)
print(f"k = {r.k_anonymity.k_value}")
print(f"Conforme ANPD (k≥5): {r.compliant_anpd}")
# Risco de re-identificação
r2 = lg.check.risk(
df_safe,
quasi_identifiers=["uf", "idade"],
masked_columns=["cpf", "email", "nome"],
)
print(f"Risco: {r2.risk_score:.3f} ({r2.risk_level})")
# Utilidade preservada
r3 = lg.check.utility(df_original, df_safe)
print(f"Utilidade: {r3.overall_score:.1%}")
17. Streaming — lg.stream()
Para arquivos que não cabem em memória.
# Processa em chunks sem OOM
for chunk in lg.stream("grande.csv", salt=SALT, chunksize=50_000):
# chunk é pd.DataFrame mascarado
salvar_no_banco(chunk)
# Com callback de progresso
def progresso(n_chunk, linhas_feitas, total_estimado):
print(f"Chunk {n_chunk}: {linhas_feitas:,} linhas")
for chunk in lg.stream("grande.csv", salt=SALT,
chunksize=50_000, on_progress=progresso):
processar(chunk)
18. Banco de dados — lg.read_db() e link
# Lê de banco
df = lg.read_db("postgresql://user:pass@host/db",
"SELECT * FROM clientes WHERE uf='SP'",
salt=SALT)
# Interface completa via link
from logus import link
adapter = link.db("postgresql://user:pass@host/db", salt=SALT)
# Query
df = adapter.query("SELECT * FROM clientes LIMIT 1000")
# Escreve
adapter.write(df_safe, "clientes_masked", if_exists="replace")
# Mascaramento direto no banco (sem round-trip Python)
result = adapter.in_db_mask("clientes", dry_run=True) # revisa SQLs primeiro
adapter.create_masked_view("clientes") # cria VIEW mascarada
19. Utilitários analíticos
# Shape e schema
lg.shape(df) # (1000000, 11)
lg.schema(df) # {'nome': 'String', 'renda_mensal': 'Float64', ...}
lg.dtypes(df) # alias de schema()
lg.info(df) # imprime resumo com tipos e nulos
# Contagem
lg.count(df) # total de linhas
lg.count(df, "cpf") # não-nulos na coluna cpf
# Nulos
lg.count_nulls(df) # pd.Series com contagem por coluna
lg.null_counts(df) # alias
# Únicos
lg.nunique(df) # pd.Series com n_unique por coluna
lg.value_counts(df, "uf")
lg.value_counts(df, "uf", normalize=True, n=5)
# Estatísticas
lg.describe(df) # pd.DataFrame com estatísticas descritivas
lg.corr(df) # correlação entre colunas numéricas
# Pivot / reshape
lg.pivot(df, index="uf", columns="tipo_pessoa",
values="renda_mensal", aggfunc="mean")
lg.melt(df, id_cols=["uf"], value_cols=["renda_mensal", "idade"])
lg.unpivot(df, ...) # alias de melt()
# Amostra
lg.sample(df, n=1000)
lg.sample(df, frac=0.1, seed=42)
# Head / tail
lg.head(df, 10)
lg.tail(df, 5)
lg.limit(df, 10) # alias de head()
# Conversão de tipo
lg.to_pandas(df) # pl.DataFrame → pd.DataFrame (passthrough se já pandas)
lg.to_polars(df) # pd.DataFrame → pl.DataFrame (passthrough se já polars)
20. Escrita de arquivos — lg.write()
# Auto-detecta formato pela extensão
lg.write(df, "resultado.csv")
lg.write(df, "resultado.parquet")
lg.write(df, "resultado.xlsx")
lg.write(df, "resultado.json")
lg.write(df, "resultado.feather")
# Com opções
lg.write(df, "resultado.csv", separator=";") # CSV europeu
# Para arquivos .lgs (criptografados), use lg.store()
lg.store(df, "resultado.lgs", key=KEY)
lg.store(df, "resultado.lgs", key=KEY, salt=SALT) # mascara + cifra
21. Chaves e salt — segurança
Conceito
| Salt (HMAC) | Key (AES) | |
|---|---|---|
| Para quê | Mascaramento determinístico | Criptografia do arquivo |
| Quem vê | Quem precisa fazer JOINs | Quem precisa ler os dados |
| Se vazar | Tokens podem ser revertidos por força bruta | Dados ficam expostos |
| Rotação | Exige re-mascarar todos os dados | lg.rekey() (sem decifrar) |
Geração segura
# CORRETO — 256 bits de entropia real
SALT = lg.generate_salt() # 48 chars, ~240 bits
KEY = lg.generate_salt() # use salt diferente para cada
# Em produção — variáveis de ambiente ou vault
import os
SALT = os.environ["LOGUS_SALT"]
KEY = os.environ["LOGUS_KEY"]
Requisitos do salt
| Requisito | Detalhes | Erro |
|---|---|---|
| Mínimo 16 bytes (128 bits) | Menos que isso → ValueError |
ValueError |
| Mínimo 6 caracteres distintos | Ex: "aaaaaaaaaaaaaaaa" → aviso |
UserWarning |
| Entropia Shannon ≥ 2.0 bits | Muito repetitivo → aviso | UserWarning |
| Sem palavras de dicionário | "password123..." → aviso |
UserWarning |
Sem anos (ex: 2024) |
Reduz espaço de busca | UserWarning |
# ERRADO — lança ValueError (muito curto)
lg.mask(df, salt="curto")
# ERRADO — UserWarning (fraco mas funciona)
lg.mask(df, salt="aaaaaaaaaaaaaaaa") # só 1 caractere único
lg.mask(df, salt="senha123senha123") # palavra de dicionário
# CORRETO
lg.mask(df, salt=lg.generate_salt())
lg.generate_salt()
SALT = lg.generate_salt() # 48 chars hex (256 bits)
HEX = lg.generate_salt_hex() # alternativa hex puro
22. Referência completa da API
🔐 Privacidade (núcleo exclusivo do logus)
| Função | Descrição |
|---|---|
lg.scan(source, *, key, sample_size, threshold) |
Detecta PII em DataFrame, arquivo ou .lgs |
lg.mask(df, *, salt, columns, exclude, verbose) |
Mascara PII com HMAC-SHA256 |
lg.profile(source, *, key) |
Diagnóstico JSON-serializable |
lg.diff(original, masked) |
Compara antes × depois |
lg.join(left, right, on, *, salt, how) |
JOIN seguro com tokens compatíveis |
lg.check.kanon(df, quasi_identifiers, target_k) |
k-anonimato (ANPD k≥5) |
lg.check.risk(df, quasi_identifiers, masked_columns) |
Risco de re-identificação |
lg.check.utility(original, masked) |
Utilidade preservada (0–1) |
📁 Leitura e Escrita
| Função | Descrição |
|---|---|
lg.read(source, *, key, salt, raw, frame) |
Lê qualquer formato + .lgs |
lg.store(source, path, *, key, salt, metadata) |
Salva como .lgs (AES-256-GCM) |
lg.write(df, path, **kwargs) |
Escreve CSV/Parquet/Excel/JSON (sem cripto) |
lg.open(path, *, key, salt) |
LGSFile context manager |
lg.inspect(path, *, key) |
Metadados sem descriptografar |
lg.rekey(path, *, old_key, new_key) |
Rotação de chave |
lg.stream(source, *, salt, chunksize, on_progress) |
Chunks sem OOM |
lg.read_db(url, sql, *, salt) |
Lê de banco relacional |
lg.save / lg.load |
Aliases de store/read |
🔍 WHERE / Filtragem
| Função | Descrição |
|---|---|
lg.where(df, expr, **kwargs) |
Filtra linhas (todas as sintaxes) |
lg.q(df, ...) |
Alias curto de where() |
lg.filter_(df, ...) |
Alias (não conflita com builtin) |
lg.query(df, ...) |
Alias backward compat |
📋 SELECT / Colunas
| Função | Descrição |
|---|---|
lg.select(df, cols) |
Seleciona colunas |
lg.drop(df, cols) |
Remove colunas |
lg.rename(df, mapping) |
Renomeia colunas |
lg.cols(df, pattern, *, dtype, exclude) |
Lista colunas por padrão/tipo |
lg.add_column(df, **cols) |
Adiciona/substitui colunas |
lg.with_column(df, ...) |
Alias de add_column() |
lg.assign(df, ...) |
Alias pandas de add_column() |
lg.cast(df, schema) |
Converte tipos |
lg.fill_null(df, value) |
Preenche nulos |
lg.fillna(df, ...) |
Alias pandas |
lg.coalesce(df, ...) |
Alias SQL |
lg.clip(df, bounds) |
Recorta valores numéricos |
lg.apply(df, funcs) |
Aplica função a colunas |
📊 ORDER BY / GROUP BY
| Função | Descrição |
|---|---|
lg.sort(df, by, *, desc, ascending, nulls_last) |
Ordena |
lg.order_by(df, ...) |
Alias SQL |
lg.groupby(df, by, agg, *, having, sort, desc, limit) |
Agrupa + agrega |
lg.group_by(df, ...) |
Alias polars |
lg.unique(df, subset, *, keep) |
Remove duplicatas |
lg.distinct(df, ...) |
Alias SQL |
lg.drop_duplicates(df, ...) |
Alias pandas |
lg.top_n(df, n, by, *, group_by, desc) |
Top N por grupo |
lg.head(df, n) |
Primeiras N linhas |
lg.tail(df, n) |
Últimas N linhas |
lg.limit(df, n) |
Alias SQL de head() |
lg.sample(df, n, frac, *, seed) |
Amostra aleatória |
lg.concat(frames, *, axis) |
Concatena DataFrames |
lg.union_all(...) |
Alias SQL de concat() |
lg.pivot(df, *, index, columns, values, aggfunc) |
Wide pivot |
lg.melt(df, *, id_cols, value_cols, name, value) |
Long unpivot |
lg.unpivot(df, ...) |
Alias polars de melt() |
📈 INFO / Estatísticas
| Função | Descrição |
|---|---|
lg.describe(df) |
Estatísticas descritivas |
lg.info(df) |
Resumo: shape, tipos, nulos |
lg.schema(df) |
Schema: {col: tipo} |
lg.dtypes(df) |
Alias de schema() |
lg.shape(df) |
(linhas, colunas) |
lg.count(df, col) |
Linhas ou não-nulos |
lg.count_nulls(df) |
Nulos por coluna |
lg.null_counts(df) |
Alias |
lg.nunique(df) |
Únicos por coluna |
lg.value_counts(df, col, *, normalize, n) |
Frequência de valores |
lg.corr(df) |
Correlação |
🔧 Expressões
| Função | Descrição |
|---|---|
lg.col(name) |
Expressão de coluna (= pl.col quando Polars disponível) |
lg.lit(value) |
Valor literal (= pl.lit) |
lg.concat_str(cols, separator) |
Concatena colunas string |
lg.when(condition, value) |
Inicia CASE WHEN |
🔄 Conversão de tipo
| Função | Descrição |
|---|---|
lg.to_pandas(df) |
pl.DataFrame → pd.DataFrame |
lg.to_polars(df) |
pd.DataFrame → pl.DataFrame |
⚡ Pipeline e SQL
| Função | Descrição |
|---|---|
lg.pipe(source, *, key, salt) |
Pipeline fluente |
lg.sql(query, *, salt, key, **frames) |
SQL via DuckDB |
🔑 Segurança
| Função | Descrição |
|---|---|
lg.generate_salt() |
Gera salt seguro (256 bits) |
lg.generate_salt_hex() |
Variante hex |
lg.configure(*, audit, audit_path) |
Configura auditoria |
🗄️ Banco de dados
| Objeto | Descrição |
|---|---|
lg.link.db(url, salt) |
Adapter SQL com privacidade |
adapter.query(sql, params) |
Executa query |
adapter.write(df, table) |
Escreve no banco |
adapter.in_db_mask(table, dry_run) |
Mascara no banco |
adapter.create_masked_view(table) |
Cria VIEW mascarada |
23. Changelog
v1.1.0 (2024)
Arquitetura — engine duplo sem round-trip:
lg.where(),lg.sort(),lg.groupby()etc. agora preservam o tipo de entrada:pl.DataFrameentra →pl.DataFramesai, sem conversão para pandas- Polars como engine primário em todos os
analytics.*; pandas permanece parapd.DataFrame
Novos recursos:
lg.top_n(df, n, by, group_by=)— TOP N por grupo via window functionlg.when().when().otherwise()— CASE WHEN sem import polarslg.add_column(**cols)— adiciona colunas com kwargs, aceita lg.col(), lg.when(), pl.Exprlg.pipe(source)— pipeline fluente encadeadolg.sql(query, *, df=, key=, salt=)— SQL via DuckDB (zero-copy Arrow)lg.cols(df, pattern, dtype=)— seletor de colunas por padrão/tipolg.write(df, path)— escreve qualquer formato detectando pela extensãolg.fill_null()com"forward"/"backward"e dict por colunalg.clip(),lg.apply()
Nomenclatura SQL completa:
lg.q()(alias curto de where),lg.order_by(),lg.group_by(),lg.distinct(),lg.union_all(),lg.coalesce(),lg.limit(),lg.unpivot()
Expressões — lg.col() = pl.col():
lg.col()agora retornapl.col()diretamente — acesso a todos os 219 métodos Polarslg.lit()elg.concat_str()expostos como funções de módulo
Performance:
- Detecção de PII: 5.9s → 33ms para 1M linhas (
detect_sampled) - Mascaramento telefone:
(XX) XXXXX-XXXXcorreto, vetorizado (10× mais rápido) - Mascaramento data:
str.slicePolars nativo (1717ms → 190ms por 1M linhas) lg.sort()overhead: +3.8ms vsdf.sort()nativo (Python dispatch)
Segurança — salt:
- Verificação de entropia Shannon: salts como
"aaaa..."agora geramUserWarning - Verificação de caracteres únicos: mínimo 6 distintos
- Mensagem de erro melhorada com instrução de correção
Correções:
lg.read()sem salt não mascara mais automaticamente (era bug de design)lg.mask()empd.DataFrameretornapd.DataFrame(antes retornavapl.DataFrame)- Mascaramento de string vazia
""e"NaN"→ trata comonull(não hasheia) lg.groupby(having=)funciona para LazyFrame
v1.0.3
lg.col() = pl.col()com fallbacklogus.expr.Colsem Polarslg.where()aceita kwargs, lista (is_in), operadores tupla,lg.col(),pl.Expr, string, callablelg.groupby(having=, sort=, desc=, limit=)— GROUP BY completolg.sql()via DuckDBlg.pipe()fluente- Encoding auto-detecção (latin-1, cp1252, utf-8-sig)
- Nulos normalizados:
"","NaN","none"→nullantes do hash
v1.0.2
lg.col()com operadores de comparação e lógicoslg.add_column()com kwargslg.when().otherwise()— CASE WHENlg.drop_duplicates(),lg.concat(),lg.pivot(),lg.melt()lg.to_pandas(),lg.to_polars()
v1.0.1
- Encoding auto-detecção para CSV
lg.read()sem salt → retorna dado bruto (breaking change intencional)lg.store(key=, salt=)→ mascara + cifra em uma operaçãolg.pl.read()=lg.read()(mesmo resultado)
v1.0.0
lg.read()com auto-detecção de formato- Mascaramento Polars-first com preservação de tipo
- Formato
.lgsAES-256-GCM multi-frame lg.where()com dict, string, callablelg.scan()comdetect_sampled()(100× mais rápido)lg.check.kanon(),lg.check.risk(),lg.check.utility()lg.join()segurolg.stream()sem OOM
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 logus_lgpd-1.0.4.tar.gz.
File metadata
- Download URL: logus_lgpd-1.0.4.tar.gz
- Upload date:
- Size: 205.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a06de4b39d0699b4623eb308ceba3279f826f76067927e7d41529bb80534c7d
|
|
| MD5 |
99dccd1fb96d4dd671b0cf6f4aeac0c1
|
|
| BLAKE2b-256 |
00f977e1d78b143d9068e79e8ddf60ec9e65702f2e62bb1494e5661bab045587
|
File details
Details for the file logus_lgpd-1.0.4-py3-none-any.whl.
File metadata
- Download URL: logus_lgpd-1.0.4-py3-none-any.whl
- Upload date:
- Size: 202.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d1bb9e072a03f1866e01c3c3750392244d4795ad1554d31f2bb0f7aa710ab1c
|
|
| MD5 |
71cdb02a5c4b0dccead3dc630d797fc2
|
|
| BLAKE2b-256 |
1df476b89fe67ba3d691c1daddbd5bf33be92a60028c13985ab90a45a9546ee4
|