尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Spendwise · controle de gastos. Aplicativo para registrar e acompanhar gastos pessoais: na linha de comando, em Python puro, ou no navegador, com uma API em FastAPI. Ver ao vivo: https://controle-gastos-lakes777.vercel.app (demonstração com dados de exemplo, só seus) Gravado com dados de exemplo.檢視完整 README ↗
Spendwise · controle de gastos. Aplicativo para registrar e acompanhar gastos pessoais: na linha de comando, em Python puro, ou no navegador, com uma API em FastAPI.
Ver ao vivo: https://spendwisealp.vercel.app (demonstração com dados de exemplo, só seus)
Gravado com dados de exemplo.
$ python -m gastos resumo --mes 2026-09
alimentação R$ 342,80
mercado R$ 489,90
transporte R$ 127,50
--------------------------
TOTAL R$ 960,20
--simular, linhas que podem ser deixadas de fora e sem nunca importar o mesmo gasto duas vezes.xlsx) ou CSV, geral ou de um mês; o .xlsx sai com valores em R$, datas de verdade e linha de total com fórmula45,90) ou ponto (45.90)Requer Python 3.10+. A linha de comando não tem dependências externas; só a versão web precisa do FastAPI.
git clone https://github.com/Lakes777/spendwise.git
cd spendwise
# Adicionar um gasto (a descrição é opcional; a data padrão é hoje)
python -m gastos adicionar 45,90 mercado "compras da semana"
python -m gastos adicionar 12.50 transporte --data 2026-09-20
# Listar todos os gastos (com o número de cada um)
python -m gastos listar
# Editar o gasto número 2 (só muda o que for informado)
python -m gastos editar 2 --valor 25,00 --categoria transporte
python -m gastos editar 2 --descricao "" # apaga a descrição
# Remover o gasto número 2
python -m gastos remover 2
# Resumo por categoria (geral ou de um mês)
python -m gastos resumo
python -m gastos resumo --mes 2026-09
# Gráfico de barras no terminal
python -m gastos grafico # por categoria, da maior para a menor
python -m gastos grafico --mes 2026-09 # só um mês
python -m gastos grafico --por mes # evolução mês a mês
# Importar o extrato (fatura do cartão ou conta): CSV do Nubank ou OFX de qualquer banco
python -m gastos importar Nubank_2026-09.csv --simular # só mostra o que entraria
python -m gastos importar Nubank_2026-09.csv
python -m gastos importar extrato-inter.ofx # o formato é reconhecido pelo conteúdo
python -m gastos importar extrato-inter.ofx --pular 2,5 # deixa de fora as linhas 2 e 5 da lista
# Seu nome como aparece nos extratos: Pix enviado para ele é transferência entre contas suas
python -m gastos meu-nome adicionar "Ana Souza Lima"
python -m gastos meu-nome # lista os nomes
python -m gastos meu-nome remover "Ana Souza Lima"
# Gastos recorrentes: lançados sozinhos quando o dia chega
python -m gastos recorrente adicionar 1200 aluguel --dia 5
python -m gastos recorrente adicionar 39,90 internet --dia 31 # em mês curto, cai no último dia
python -m gastos recorrente adicionar 55,90 streaming --dia 10 --desde 2026-08 # inclui meses passados
python -m gastos recorrente # lista, com o próximo lançamento
python -m gastos recorrente remover 1 # para de lançar
# Orçamento mensal por categoria
python -m gastos orcamento definir mercado 500 # cria ou troca o limite
python -m gastos orcamento # situação do mês atual
python -m gastos orcamento --mes 2026-08 # de outro mês
python -m gastos orcamento remover mercado
# Exportar (o formato vem da extensão); não sobrescreve arquivo existente sem pedir
python -m gastos exportar gastos.xlsx # planilha do Excel
python -m gastos exportar setembro.xlsx --mes 2026-09
python -m gastos exportar gastos.csv # CSV com ; (Excel em português)
python -m gastos exportar gastos.xlsx --sobrescrever
# Ajuda
python -m gastos --help
Exemplo de listagem:
Nº DATA VALOR CATEGORIA DESCRIÇÃO
2 20/09/2026 R$ 12,50 transporte
1 23/09/2026 R$ 45,90 mercado compras da semana
Exemplo de gráfico:
Gastos por categoria
lanche ██████████████████████████████ R$ 30,00 56%
uber ███████████████████████▋ R$ 23,59 44%
TOTAL R$ 53,59
Exemplo de importação do extrato da conta:
$ python -m gastos importar extrato.csv
Arquivo reconhecido: extrato da conta do Nubank (CSV)
+ 01/09/2026 R$ 45,90 mercado Compra no débito - SUPERMERCADO CONDOR
+ 10/09/2026 R$ 18,50 alimentação Compra no débito - PADARIA BELA VISTA
- 02/09/2026 R$ 2.500,00 Pix recebido - EMPRESA (ignorado: entrada de dinheiro)
- 05/09/2026 R$ 1.200,00 Pagamento de fatura (ignorado: pagamento da fatura; as compras vêm da fatura do cartão)
2 gasto(s) importado(s), 0 já importado(s) antes, 2 ignorado(s)
Quando chega o dia de um gasto recorrente, qualquer comando o lança e avisa:
$ python -m gastos listar
Lançado automaticamente: R$ 1.200,00 em aluguel (05/10/2026)
Nº DATA VALOR CATEGORIA DESCRIÇÃO
1 05/10/2026 R$ 1.200,00 aluguel apartamento
Exemplo de orçamento:
Orçamento de set/2026
lazer R$ 95,00 de R$ 80,00 119% ██████████ ESTOUROU em R$ 15,00
mercado R$ 420,00 de R$ 500,00 84% ████████▍░ ATENÇÃO: sobram R$ 80,00
uber R$ 10,00 de R$ 50,00 20% ██░░░░░░░░ sobram R$ 40,00
Com orçamento definido, adicionar ou editar um gasto já mostra como ficou o mês:
$ python -m gastos adicionar 120 mercado
Gasto adicionado: R$ 120,00 em mercado
Orçamento de mercado em set/2026: R$ 420,00 de R$ 500,00 (84%) - ATENÇÃO: sobram R$ 80,00
Prints com dados de exemplo (modo de demonstração). Na raiz fica o lobby; dele, o Começar leva às 5 abas (Resumo, Gastos, Orçamento, Recorrentes e Importar), cada uma com seu endereço (
/#gastos, por exemplo), com as mesmas animações do portfólio.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m gastos.web
Depois, abra http://127.0.0.1:8000 no navegador. A página usa o mesmo banco da linha de comando (dados/gastos.db), então um gasto adicionado no terminal aparece na web e vice-versa. A documentação interativa da API fica em http://127.0.0.1:8000/docs.
| Rota | O que faz |
|---|---|
GET/POST /gastos | lista (com ?mes=AAAA-MM) e registra gastos |
GET/PATCH/DELETE /gastos/{id} | vê, edita (só os campos enviados) e apaga |
GET /resumo | total do período e por categoria, com porcentagem |
GET /orcamentos | situação do orçamento do mês (ok, atenção, estourou) |
GET /orcamentos/total | todos os orçamentos do mês somados, com as categorias que estouraram (null se não há nenhum) |
PUT/DELETE /orcamentos/{categoria} | define ou apaga o limite mensal |
GET/POST /recorrentes, DELETE /recorrentes/{id} | gastos que se repetem todo mês |
GET /exportar?formato=xlsx | baixa a planilha (ou csv) |
POST /importar/previa | recebe o arquivo (CSV do Nubank ou OFX) e mostra o que entraria, sem salvar |
POST /importar | salva os gastos revisados na prévia, sem repetir os já importados |
GET/POST /importar/meus-nomes, DELETE /importar/meus-nomes/{nome} | nomes do usuário: Pix enviado para eles não é gasto |
GET /importar/exemplo.csv | uma fatura de exemplo, com datas recentes, para testar |
GET /meses, GET /categorias | meses com gastos e categorias já usadas |
POST /conta/cadastro, POST /conta/entrar, POST /conta/sair | contas (só na versão online) |
GET /conta, POST /conta/excluir | quem está logado; apaga a conta (pede a senha) |
GET/POST /conta/chaves, DELETE /conta/chaves/{id} | lista, cria e apaga chaves de acesso (só online, só pelo site) |
GET /info | se é a demonstração, quem está logado e se o cadastro está aberto |
Variáveis de ambiente opcionais: GASTOS_BANCO (arquivo do banco), GASTOS_DEMO=1 (modo demonstração), HOST e PORT.
Na versão online, quem não entra numa conta usa a demonstração. Quem tem conta entra pelo botão Entrar e vê só os próprios dados, que ficam guardados no Postgres. Para criar conta é preciso um código de convite, definido na variável de ambiente CODIGO_CONVITE do servidor; sem ela, o cadastro fica fechado. No computador (python -m gastos.web com SQLite) não há login: o programa é de quem o roda.
Não há recuperação de senha por e-mail (o projeto não envia e-mails).
Para um programa seu (um bot do Telegram, um script) usar a API online sem guardar a sua senha, crie uma chave de acesso em Sua conta → Chaves de acesso. A chave (sw_...) aparece uma vez só, na criação; depois a lista mostra só o nome, quando foi criada e o último uso. Cada conta tem até 5 chaves, e apagar uma corta o acesso do programa na hora.
O programa manda a chave no cabeçalho Authorization e usa as mesmas rotas da página (gastos, resumo, categorias, orçamentos, recorrentes, importação), sempre na conta dona da chave:
curl -X POST https://spendwisealp.vercel.app/gastos \
-H "Authorization: Bearer sw_..." \
-H "Content-Type: application/json" \
-d '{"valor": "35.00", "categoria": "mercado", "descricao": "pão e leite"}'
# 201: {"id": 42, "valor": "35.00", "categoria": "mercado", "descricao": "pão e leite", "data": "2026-10-04"}
curl https://spendwisealp.vercel.app/categorias -H "Authorization: Bearer sw_..."
# 200: ["alimentação", "mercado", "transporte"]
valor é texto com ponto e até 2 casas ("35", "35.9" ou "35.90"); data ("AAAA-MM-DD") é opcional e vale hoje no horário de Brasília; a categoria vira minúscula. Chave errada ou apagada dá 401 ({"detail": "Chave de acesso inválida ou apagada"}). As rotas da conta (/conta/...: excluir a conta, criar ou listar chaves) recusam a chave com 403: quem tiver a chave do bot não consegue apagar a conta nem criar outras chaves. No computador (SQLite, sem login), o cabeçalho é ignorado.
Online, o app roda em modo demonstração: cada visitante recebe uma cópia própria dos dados de exemplo, e pode adicionar, editar e apagar sem que ninguém mais veja. Os dados de exemplo são sempre dos últimos 3 meses (relativos a hoje), com orçamentos e gastos recorrentes já lançados. Cada visitante é uma conta num Postgres (Neon), e as contas são apagadas depois de um dia.
O arquivo app.py da raiz é a entrada da Vercel, que entrega o endereço do banco em DATABASE_URL. Para testar a demonstração no seu computador, com um Postgres local:
GASTOS_DEMO=1 DATABASE_URL=postgresql://usuario@localhost/banco python -m gastos.web
python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
pytest
Os testes do Postgres rodam quando GASTOS_TESTE_POSTGRES tem o endereço de um banco (cada teste usa um schema próprio, apagado no fim); sem ela, são pulados. No GitHub Actions, um Postgres 18 sobe junto com os testes.
A suíte cobre o modelo de dados, o banco SQLite (incluindo a migração do JSON antigo, a edição e a remoção), a exportação para .xlsx e CSV, o gráfico, o orçamento, os gastos recorrentes (simulando datas), a importação do Nubank e do OFX, o fluxo completo da linha de comando e todas as rotas da API (com a data de hoje trocada por uma data fixa) o modo demonstração (visitantes isolados, cookie inválido, pedidos simultâneos, limites e limpeza das contas antigas) e o login (convite, senha em argon2id, bloqueio de tentativas, sessão encerrada, CSRF, isolamento entre usuários e exclusão da conta) e as chaves de acesso (token mostrado uma vez só, só o hash no banco, limite por conta, chave apagada ou de outro usuário, rotas da conta que recusam chave e CSRF continuando a valer para o cookie). Os testes usam pastas temporárias e nunca tocam nos dados reais.
controle-gastos/
├── gastos/
│ ├── __main__.py # linha de comando (argparse)
│ ├── armazenamento.py # banco SQLite (sqlite3, SQL à mão)
│ ├── exportacao.py # exportar para .xlsx e CSV
│ ├── formatacao.py # valores em reais (R$ 1.234,50)
│ ├── grafico.py # gráfico de barras no terminal
│ ├── importacao/ # leitura dos extratos
│ │ ├── __init__.py # ler_extrato: reconhece o formato pelo conteúdo
│ │ ├── comum.py # categorias, valores e o que ignorar (vale para todos)
│ │ ├── nubank.py # CSV do Nubank (fatura e conta)
│ │ └── ofx.py # OFX de qualquer banco (versões 1 e 2)
│ ├── orcamento.py # situação do orçamento (ok, atenção, estourou)
│ ├── recorrentes.py # regras de datas dos gastos recorrentes
│ ├── modelo.py # a classe Gasto
│ └── web/
│ ├── __main__.py # python -m gastos.web (servidor uvicorn)
│ ├── app.py # cria o app FastAPI e serve a página
│ ├── banco_postgres.py # o mesmo Banco, em Postgres, com uma conta por linha
│ ├── contas.py # cadastro com convite, login (argon2id), sessões e chaves de acesso
│ ├── demo.py # modo demonstração: uma conta com exemplos por visitante
│ ├── modelos.py # o que a API recebe e devolve (Pydantic)
│ ├── rotas.py # as rotas da API
│ └── static/ # a página: index.html, estilo.css, app.js e favicon.svg
├── app.py # entrada da Vercel (modo demonstração com Postgres)
├── vercel.json # o que não vai para o servidor (testes, docs)
└── tests/ # testes com pytest
Decimal em vez de float para dinheiro: float acumula erros de arredondamento (0.1 + 0.2 = 0.30000000000000004); Decimal faz contas exatas.sqlite3 da biblioteca padrão: um banco de verdade num único arquivo, sem servidor e sem instalar nada. O SQL é escrito à mão, com marcadores (?) para evitar SQL injection.TEXT, não REAL: o REAL do SQLite é um float e traria de volta os erros de centavos; em texto ("45.90"), o Decimal volta exato. Há um teste que falha se a coluna virar REAL.gastos.json antigo é importado numa única transação e renomeado para gastos.json.migrado (backup). Se algo falhar, nada fica importado pela metade.AUTOINCREMENT, então, depois de remover o gasto 5, nenhum gasto novo recebe o 5. Um número anotado nunca passa a apontar para outro gasto.; (a vírgula já é usada nos centavos) e arquivo em utf-8-sig, cuja marca inicial (BOM) faz o Excel mostrar os acentos certos..xlsx escrito à mão, sem bibliotecas: um .xlsx é um .zip com arquivos XML dentro, então zipfile e xml da biblioteca padrão bastam. O CSV depende das configurações regionais de quem abre (num Windows em inglês, o Excel separa as colunas na vírgula e quebra 30,00 ao meio); no .xlsx, o valor é guardado como número e a data como data, e a planilha abre certa em qualquer idioma. O arquivo gerado foi conferido abrindo no Excel real.=, +, - ou @ seria executado como fórmula pelo Excel; ele é exportado com um ' na frente, e aparece só como texto.▏▎▍▌▋▊▉█ dão precisão de 1/8 de caractere, e um gasto pequeno sempre aparece com pelo menos ▏. Categorias vêm da maior para a menor (fica fácil comparar); meses, em ordem cronológica.Decimal, não a porcentagem. Os casos de fronteira (79,99%, 80%, 100% e um centavo acima) têm testes.origem numa coluna com índice UNIQUE. No extrato da conta, é o identificador que o próprio Nubank dá; na fatura, que não tem identificador, é data + descrição + valor + um contador, para que duas compras iguais no mesmo dia (dois cafés) continuem sendo duas. Importar o mesmo arquivo de novo não repete nada. Atenção: o CSV e o OFX do mesmo banco dão origens diferentes para a mesma compra, então o mesmo período deve ser importado num formato só."41,80", sinal separado - 84,00, Pix no Crédito, descrições longas) viraram testes, com nomes e contas inventados.FITID), que vira a origem; ao lado dele vai o código do banco e um resumo (SHA-256) do número da conta, para duas contas não se confundirem sem guardar o número em si. O arquivo é lido com expressões regulares, que funcionam nas duas versões do formato (1.x em SGML, com tags sem fechamento, e 2.x em XML). Muitos bancos ainda gravam o OFX em Windows-1252; o programa tenta UTF-8 primeiro e, se não der, lê em 1252. As regras do que ignorar (pagamento de fatura, aplicação, Pix no Crédito) são as mesmas do Nubank e aceitam os jeitos diferentes que cada banco escreve ("PAGTO FATURA", "Pagamento de fatura").CHARSET:1252 no cabeçalho, mas vem em UTF-8 (por isso o UTF-8 é tentado primeiro), e escreve o Pix como Pix enviado: "Cp :18236120-Nome", com o código do banco da outra ponta; a descrição vira "Pix enviado - Nome", usando o nome com acentos do campo NAME.--pular 2,5. Um número que não existe na lista cancela tudo, em vez de importar sem aquela linha.<OFX> ou o cabeçalho do CSV), não pela extensão, que o usuário pode ter trocado.ALTER TABLE: bancos de versões anteriores não têm a coluna origem; ao abrir, o programa confere as colunas (PRAGMA table_info) e a acrescenta, sem mexer nos gastos.proximo_mes), que avança na mesma transação em que o gasto é inserido. Rodar o programa várias vezes no mesmo dia não duplica nada, e um gasto lançado que o usuário apagou não volta.--desde inclui meses passados, mas no máximo 12, para um erro de digitação como 2016 não criar 120 gastos.hoje(), que os testes trocam para simular a passagem do tempo (a véspera, o dia certo, meses sem abrir o programa, dia 31 em mês de 30 e ano bissexto).CREATE TABLE IF NOT EXISTS cria as tabelas de orçamentos e de recorrentes em bancos de versões anteriores na primeira vez que são abertos, sem mexer nos gastos.--mes validado e normalizado: 2026-9 vira 2026-09 (senão não acharia nada no banco) e setembro é recusado com uma mensagem clara.Banco, o mesmo cálculo de orçamento e a mesma exportação da linha de comando. Por isso as duas interfaces sempre concordam."45.90", não 45.9. Um número no JSON vira float no JavaScript e traria de volta os erros de centavos. O Pydantic recusa zero, negativo, NaN e mais de 2 casas decimais.secrets.token_urlsafe) e o banco guarda só o SHA-256 dele. "Sair" apaga a sessão no servidor, então um cookie copiado deixa de valer. O cookie é HttpOnly, SameSite=Lax e Secure no HTTPS, e vale 30 dias.SameSite=Lax, todo pedido que altera dados confere o cabeçalho Origin e recusa pedidos vindos de outro site.secrets.token_urlsafe (com o prefixo sw_, fácil de reconhecer se for colada no lugar errado) e o banco guarda só o SHA-256; por isso ela aparece uma vez só. Cada uso anota a data (usada_em), para o usuário ver se uma chave esquecida ainda está em uso. Com o cabeçalho Authorization, vale só a chave: errada, dá 401, sem cair no cookie nem na demonstração. Isso também permite pular a conferência do Origin nesses pedidos, sem abrir brecha de CSRF: o CSRF depende de o navegador mandar o cookie sozinho, e um site de fora não tem como pôr a chave no pedido (e, mesmo que mande uma chave qualquer junto com o cookie, o cookie é ignorado). As rotas da conta recusam a chave, e a criação de chaves trava a linha da conta (FOR UPDATE) para dois pedidos juntos não passarem do limite de 5. A tabela é criada com CREATE TABLE IF NOT EXISTS na primeira vez que o servidor sobe, sem migração manual, e some em cascata com a conta.tipo = 'demo'); o id das contas de usuário (u_...) nem tem o formato aceito pelo cookie da demo, e mesmo assim a demo confere o tipo da conta antes de abri-la. Cada uma dessas proteções tem um teste que falha se ela for removida (conferido quebrando o código de propósito).BancoPostgres tem os mesmos métodos do Banco em SQLite, e as rotas funcionam com qualquer um; um mesmo conjunto de testes roda nos dois para garantir que se comportam igual. O defeito foi reproduzido localmente com uvicorn --workers 4 e confirmado como resolvido.conta, e toda consulta filtra por ela, então um visitante não vê nem altera o gasto de outro, mesmo sabendo o número (há teste para isso). A conta vem de um cookie HttpOnly com um número aleatório (uuid4), conferido por expressão regular. É a mesma estrutura que um login precisaria.INSERT ... ON CONFLICT DO NOTHING); se a página faz vários pedidos juntos, os outros esperam e não repetem os exemplos. O lançamento dos recorrentes trava as linhas (SELECT ... FOR UPDATE), então duas cópias do servidor nunca lançam o mesmo mês. Um teste dispara 8 pedidos ao mesmo tempo com threads, e ele falha se o FOR UPDATE for removido.NUMERIC(12, 2) no Postgres: diferente do SQLite, o Postgres tem um tipo decimal exato para dinheiro; o valor volta como Decimal e o banco também recusa valor negativo (CHECK).ON DELETE CASCADE apaga gastos, orçamentos e recorrentes junto com a conta vencida.America/Sao_Paulo, às 22h de Brasília ele já estaria no dia seguinte.origem; ALTER TABLE ... ADD COLUMN IF NOT EXISTS a acrescenta na primeira vez, sem mexer nos gastos, e há um teste que parte de um banco no formato antigo.innerHTML no front: tudo que vem da API entra na página com textContent, então uma descrição como <script> aparece como texto e não é executada (proteção contra XSS).argparse, sqlite3, csv, zipfile, dataclasses e pathlib resolvem o problema sem dependências externas. FastAPI e uvicorn são usados só pela versão web.dados/ está no .gitignore, então informações financeiras pessoais nunca vão para o repositório.