Skip to main content

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

  1. Conceitos fundamentais
  2. Leitura de arquivos — lg.read()
  3. Filtragem — lg.where()
  4. Seleção e transformação de colunas
  5. Ordenação — lg.sort()
  6. Agrupamento — lg.groupby()
  7. Expressões — lg.col()
  8. CASE WHEN — lg.when()
  9. Pipeline fluente — lg.pipe()
  10. SQL direto — lg.sql()
  11. Privacidade — lg.scan() e lg.mask()
  12. Formato .lgs — arquivo seguro
  13. Multi-frame e lg.open()
  14. JOIN seguro — lg.join()
  15. Diagnóstico — lg.profile() e lg.diff()
  16. Verificação de privacidade — lg.check
  17. Streaming — lg.stream()
  18. Banco de dados — lg.read_db() e link
  19. Utilitários analíticos
  20. Escrita de arquivos — lg.write()
  21. Chaves e salt — segurança
  22. Referência completa da API
  23. 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-353425441ddfb8d1ec
E-mail hash ana@empresa.com7a3f9c1d4e2b8f56
Nome redact Ana SilvaREDACTED
Telefone mask_phone_ddd (11) 98765-4321(11) XXXXX-XXXX
CEP truncate 01310-10001310-XXX
Data de nascimento generalize_date 1985-03-151980-1989
UF / categoria mock_category SPRJ (aleatório)
Renda / numérico mock_numeric 5000.004937.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.DataFrame entra → pl.DataFrame sai, sem conversão para pandas
  • Polars como engine primário em todos os analytics.*; pandas permanece para pd.DataFrame

Novos recursos:

  • lg.top_n(df, n, by, group_by=) — TOP N por grupo via window function
  • lg.when().when().otherwise() — CASE WHEN sem import polars
  • lg.add_column(**cols) — adiciona colunas com kwargs, aceita lg.col(), lg.when(), pl.Expr
  • lg.pipe(source) — pipeline fluente encadeado
  • lg.sql(query, *, df=, key=, salt=) — SQL via DuckDB (zero-copy Arrow)
  • lg.cols(df, pattern, dtype=) — seletor de colunas por padrão/tipo
  • lg.write(df, path) — escreve qualquer formato detectando pela extensão
  • lg.fill_null() com "forward"/"backward" e dict por coluna
  • lg.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 retorna pl.col() diretamente — acesso a todos os 219 métodos Polars
  • lg.lit() e lg.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-XXXX correto, vetorizado (10× mais rápido)
  • Mascaramento data: str.slice Polars nativo (1717ms → 190ms por 1M linhas)
  • lg.sort() overhead: +3.8ms vs df.sort() nativo (Python dispatch)

Segurança — salt:

  • Verificação de entropia Shannon: salts como "aaaa..." agora geram UserWarning
  • 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() em pd.DataFrame retorna pd.DataFrame (antes retornava pl.DataFrame)
  • Mascaramento de string vazia "" e "NaN" → trata como null (não hasheia)
  • lg.groupby(having=) funciona para LazyFrame

v1.0.3

  • lg.col() = pl.col() com fallback logus.expr.Col sem Polars
  • lg.where() aceita kwargs, lista (is_in), operadores tupla, lg.col(), pl.Expr, string, callable
  • lg.groupby(having=, sort=, desc=, limit=) — GROUP BY completo
  • lg.sql() via DuckDB
  • lg.pipe() fluente
  • Encoding auto-detecção (latin-1, cp1252, utf-8-sig)
  • Nulos normalizados: "", "NaN", "none"null antes do hash

v1.0.2

  • lg.col() com operadores de comparação e lógicos
  • lg.add_column() com kwargs
  • lg.when().otherwise() — CASE WHEN
  • lg.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ção
  • lg.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 .lgs AES-256-GCM multi-frame
  • lg.where() com dict, string, callable
  • lg.scan() com detect_sampled() (100× mais rápido)
  • lg.check.kanon(), lg.check.risk(), lg.check.utility()
  • lg.join() seguro
  • lg.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

logus_lgpd-1.0.4.tar.gz (205.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

logus_lgpd-1.0.4-py3-none-any.whl (202.8 kB view details)

Uploaded Python 3

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

Hashes for logus_lgpd-1.0.4.tar.gz
Algorithm Hash digest
SHA256 8a06de4b39d0699b4623eb308ceba3279f826f76067927e7d41529bb80534c7d
MD5 99dccd1fb96d4dd671b0cf6f4aeac0c1
BLAKE2b-256 00f977e1d78b143d9068e79e8ddf60ec9e65702f2e62bb1494e5661bab045587

See more details on using hashes here.

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

Hashes for logus_lgpd-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 7d1bb9e072a03f1866e01c3c3750392244d4795ad1554d31f2bb0f7aa710ab1c
MD5 71cdb02a5c4b0dccead3dc630d797fc2
BLAKE2b-256 1df476b89fe67ba3d691c1daddbd5bf33be92a60028c13985ab90a45a9546ee4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page