kotsu-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotsu_cli-0.1.0.dist-info/METADATA +252 -0
- kotsu_cli-0.1.0.dist-info/RECORD +33 -0
- kotsu_cli-0.1.0.dist-info/WHEEL +4 -0
- kotsu_cli-0.1.0.dist-info/entry_points.txt +3 -0
- kotsu_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- money_cli/__init__.py +3 -0
- money_cli/__main__.py +197 -0
- money_cli/armazenamento/__init__.py +4 -0
- money_cli/armazenamento/backup.py +70 -0
- money_cli/armazenamento/banco.py +525 -0
- money_cli/armazenamento/caminhos.py +72 -0
- money_cli/armazenamento/migracoes/0001_inicial.sql +35 -0
- money_cli/armazenamento/migracoes/0002_recorrencia.sql +21 -0
- money_cli/armazenamento/migracoes/0003_mesclagem.sql +54 -0
- money_cli/armazenamento/migracoes/__init__.py +32 -0
- money_cli/armazenamento/preferencias.py +31 -0
- money_cli/armazenamento/trava.py +123 -0
- money_cli/armazenamento/troca.py +285 -0
- money_cli/dominio/__init__.py +5 -0
- money_cli/dominio/dinheiro.py +10 -0
- money_cli/dominio/filtro.py +63 -0
- money_cli/dominio/linha.py +179 -0
- money_cli/dominio/mes.py +57 -0
- money_cli/dominio/mescla.py +440 -0
- money_cli/dominio/parcelas.py +52 -0
- money_cli/dominio/recorrencia.py +20 -0
- money_cli/dominio/tipos.py +95 -0
- money_cli/dominio/totais.py +17 -0
- money_cli/tela/__init__.py +1 -0
- money_cli/tela/app.py +756 -0
- money_cli/tela/cores.py +31 -0
- money_cli/tela/perguntas.py +144 -0
- money_cli/tela/temas.py +74 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kotsu-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cozy personal finance in your terminal: income, expenses and installments of the month on one screen.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sera-DAI/kotsu
|
|
6
|
+
Project-URL: Issues, https://github.com/Sera-DAI/kotsu/issues
|
|
7
|
+
Project-URL: Português, https://github.com/Sera-DAI/kotsu/blob/main/README.pt-BR.md
|
|
8
|
+
Author: Sera-DAI
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: budget,finance,terminal,textual,tui
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: textual<9,>=8.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/banner.en.svg" alt="Kotsu: cozy personal finance in your terminal" width="100%">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<p align="center"><b>English</b> · <a href="https://github.com/Sera-DAI/kotsu/blob/main/README.pt-BR.md">Português (Brasil)</a></p>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="https://img.shields.io/badge/python-3.11%2B-ff7fb8?style=flat-square&labelColor=1d1424&logo=python&logoColor=ffe9f4" alt="Python 3.11+">
|
|
33
|
+
<img src="https://img.shields.io/badge/terminal-Windows%20%C2%B7%20Linux-ff7fb8?style=flat-square&labelColor=1d1424&logo=windowsterminal&logoColor=ffe9f4" alt="Windows Terminal and Linux">
|
|
34
|
+
<img src="https://img.shields.io/badge/data-only%20on%20your%20computer-ffa28c?style=flat-square&labelColor=1d1424&logo=sqlite&logoColor=ffe9f4" alt="Local data">
|
|
35
|
+
<img src="https://img.shields.io/badge/license-MIT-ffa28c?style=flat-square&labelColor=1d1424" alt="MIT license">
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/stack/python.svg" width="44" alt="Python" title="Python">
|
|
40
|
+
<img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/stack/githubactions.svg" width="44" alt="GitHub Actions" title="GitHub Actions">
|
|
41
|
+
<img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/stack/claude.svg" width="44" alt="Claude" title="Claude">
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<p align="center"><sub>Built with Python, Textual, SQLite and Claude</sub></p>
|
|
45
|
+
|
|
46
|
+
<p align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/divisor.svg" width="420" alt=""></p>
|
|
47
|
+
|
|
48
|
+
# Kotsu
|
|
49
|
+
|
|
50
|
+
**Kotsu** (コツコツ, *kotsukotsu*) is a Japanese word for "little by little, steadily". It is a terminal app for everyday money: income, expenses and the month's installments on a single screen, in a pink and salmon **Cotton Candy** look. No giant spreadsheet, no dashboard full of charts. You jot an entry on one line, see the whole month and get on with your day.
|
|
51
|
+
|
|
52
|
+
It runs on Windows Terminal and Linux, works offline and keeps everything in a SQLite file on your computer. No account, no cloud, no telemetry, no ads.
|
|
53
|
+
|
|
54
|
+
> The app interface is in Brazilian Portuguese for now (labels such as *receitas* = income, *despesas* = expenses, *saldo* = balance). The commands and keys below work the same in any language.
|
|
55
|
+
|
|
56
|
+
## Contents
|
|
57
|
+
|
|
58
|
+
- [I. What it does](#i-what-it-does)
|
|
59
|
+
- [II. Install](#ii-install)
|
|
60
|
+
- [III. Use](#iii-use)
|
|
61
|
+
- [IV. Your data and backups](#iv-your-data-and-backups)
|
|
62
|
+
- [V. Under the hood](#v-under-the-hood)
|
|
63
|
+
- [VI. Development](#vi-development)
|
|
64
|
+
- [VII. License](#vii-license)
|
|
65
|
+
|
|
66
|
+
## I. What it does
|
|
67
|
+
|
|
68
|
+
<table>
|
|
69
|
+
<tr>
|
|
70
|
+
<td width="72" align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/lancar.svg" width="44" alt=""></td>
|
|
71
|
+
<td><b>Quick line</b><br><code>89,90 mercado #alimentação @nubank</code> and Enter. No forms: the amount comes first, then account, category and date in any order.</td>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr>
|
|
74
|
+
<td align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/parcelas.svg" width="44" alt=""></td>
|
|
75
|
+
<td><b>One screen</b><br>Income, expenses and balance for the month, one line per account (each with its own color) and the list of entries. The ← / → arrows switch months.</td>
|
|
76
|
+
</tr>
|
|
77
|
+
<tr>
|
|
78
|
+
<td align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/todomes.svg" width="44" alt=""></td>
|
|
79
|
+
<td><b>Installments and "every month"</b><br><code>120 geladeira @nubank 10x</code> becomes 10 installments, one per month. Categories marked "every month" (rent, internet, salary) show up on their own, with a <code>↻</code>.</td>
|
|
80
|
+
</tr>
|
|
81
|
+
<tr>
|
|
82
|
+
<td align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/temas.svg" width="44" alt=""></td>
|
|
83
|
+
<td><b>Two Cotton Candy themes</b><br>Night (default) and light, with hearts, a little face and "zzz… zerada" when an account is empty. The <code>t</code> key switches and your choice is saved.</td>
|
|
84
|
+
</tr>
|
|
85
|
+
<tr>
|
|
86
|
+
<td align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/teclado.svg" width="44" alt=""></td>
|
|
87
|
+
<td><b>Keyboard first</b><br>Everything has a key, and the footer reminds you which. Filter by <code>@account</code> or <code>#category</code> with <code>/</code>.</td>
|
|
88
|
+
</tr>
|
|
89
|
+
<tr>
|
|
90
|
+
<td align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/privado.svg" width="44" alt=""></td>
|
|
91
|
+
<td><b>Truly private</b><br>Your data stays on your computer. Kotsu never sends anything over the network.</td>
|
|
92
|
+
</tr>
|
|
93
|
+
</table>
|
|
94
|
+
|
|
95
|
+
## II. Install
|
|
96
|
+
|
|
97
|
+
<table>
|
|
98
|
+
<tr>
|
|
99
|
+
<td width="72" align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/instalar.svg" width="44" alt=""></td>
|
|
100
|
+
<td>Requires Python 3.11 or newer and <a href="https://pipx.pypa.io/">pipx</a>. The package is not on PyPI yet: install straight from GitHub.</td>
|
|
101
|
+
</tr>
|
|
102
|
+
</table>
|
|
103
|
+
|
|
104
|
+
Windows Terminal (PowerShell), in one line:
|
|
105
|
+
|
|
106
|
+
```powershell
|
|
107
|
+
irm https://raw.githubusercontent.com/Sera-DAI/kotsu/main/instalar.ps1 | iex
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Linux/macOS (needs pipx; the script tells you if it is missing):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
curl -fsSL https://raw.githubusercontent.com/Sera-DAI/kotsu/main/instalar.sh | bash
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Or step by step, without a script:
|
|
117
|
+
|
|
118
|
+
Windows Terminal (PowerShell):
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
py -m pip install --user pipx
|
|
122
|
+
py -m pipx ensurepath
|
|
123
|
+
py -m pipx install kotsu-cli
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Linux (Debian/Ubuntu):
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
sudo apt install pipx
|
|
130
|
+
pipx ensurepath
|
|
131
|
+
pipx install kotsu-cli
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Reopen the terminal and run `kotsu`. To update: `pipx upgrade kotsu-cli` (or run the installer again). To remove: `pipx uninstall kotsu-cli` (your data stays in the data folder).
|
|
135
|
+
|
|
136
|
+
## III. Use
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
☁ Algodão-Doce ☁ novembro de 2026
|
|
140
|
+
|
|
141
|
+
♡ receitas 3.500,00 ✧ despesas 432,90 ☆ saldo 3.067,10 (◕‿◕)♡
|
|
142
|
+
|
|
143
|
+
♡ itaú ♥♥♥♥♥♥♥♥♥♥♥♥♥♥· -210,50 +3.500,00
|
|
144
|
+
♡ nubank ♥♥♥♥♥♥♥♥♥♥♥♥♥♥ -222,40
|
|
145
|
+
|
|
146
|
+
Data Descrição Categoria Conta Valor
|
|
147
|
+
08/11 padaria alimentação nubank 12,50
|
|
148
|
+
07/11 mercado alimentação nubank 89,90
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Key | Action |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `l` | opens the quick line (Enter saves, Esc closes) |
|
|
154
|
+
| ← / → | previous / next month |
|
|
155
|
+
| `/` | filters by `@account`, `#category`, both, or just a name (empty Enter or Esc clears) |
|
|
156
|
+
| `e` | edits the selected entry in the quick line, date included |
|
|
157
|
+
| `c` | copies the selected entry into the quick line, dated today |
|
|
158
|
+
| `d` | deletes the selected entry (an installment asks "only this one or all?") |
|
|
159
|
+
| `r` | renames the selected entry |
|
|
160
|
+
| `m` | turns "every month" on/off for the selected entry's category |
|
|
161
|
+
| `b` | manual database backup (a new copy in `backups/`) |
|
|
162
|
+
| `t` | switches the theme (Cotton Candy night or light) |
|
|
163
|
+
| `v` | shows the check code (same on another device = same data) |
|
|
164
|
+
| `q` | quits |
|
|
165
|
+
|
|
166
|
+
The line starts with the amount; the rest comes in any order:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
89,90 mercado #alimentação @nubank expense (no sign)
|
|
170
|
+
+3500 salário @itaú income
|
|
171
|
+
120 geladeira #casa @nubank 10x 10 installments of 120.00, one per month
|
|
172
|
+
45 farmácia ontem date: ontem (yesterday), 15/10 or 15/10/2025 (default: today)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Every entry needs an `@account`; without one the line is not saved. An account or category that does not exist yet asks whether to create it. Editing or renaming an installment asks "only this one or all?". Amounts use a decimal comma and dates are day/month, as in Brazil.
|
|
176
|
+
|
|
177
|
+
### ↻ Categories that repeat every month
|
|
178
|
+
|
|
179
|
+
When you create a category, Kotsu asks whether it repeats every month (or press `m` on an existing one). Every new entry in it reappears each month with the same amount, account and day (day 31 becomes the last day of the month), with no end, until you stop it. Months appear as you navigate with ← / →, up to 24 months ahead of today. Deleting, editing or renaming one asks "only this month or the following ones?".
|
|
180
|
+
|
|
181
|
+
## IV. Your data and backups
|
|
182
|
+
|
|
183
|
+
<table>
|
|
184
|
+
<tr>
|
|
185
|
+
<td width="72" align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/backup.svg" width="44" alt=""></td>
|
|
186
|
+
<td>
|
|
187
|
+
<ul>
|
|
188
|
+
<li><b>Where it lives:</b> <code>%APPDATA%\money-cli</code> on Windows and <code>~/.local/share/money-cli</code> on Linux (or the folder in the <code>MONEY_CLI_DADOS</code> variable). <code>kotsu --dados FOLDER</code> opens another folder.</li>
|
|
189
|
+
<li><b>Manual backup:</b> the <code>b</code> key creates <code>backups/money.YYYY-MM-DD-HHMMSS.sqlite</code> and keeps the 5 most recent copies. Kotsu never backs up on its own.</li>
|
|
190
|
+
<li><b>Using two computers (Google Drive):</b> on both, run <code>kotsu --pasta-dados "G:\My Drive\Kotsu"</code> (any existing data on the computer is copied there; if both already have data, choose which one wins). Keep the folder "Available offline". Use one computer at a time: Kotsu warns if it is open on the other one and refuses to open if Drive created conflicting copies (<code>--forcar</code> skips the warning). Backups from the <code>b</code> key stay in the local folder, outside Drive. <code>kotsu --pasta-dados-padrao</code> undoes it.</li>
|
|
191
|
+
<li><b>Merging with another device:</b> <code>kotsu --exportar data.json</code> writes everything to a file; on the other device, <code>kotsu --importar data.json</code> merges it with what is there (the most recent change to each entry wins, and deleted entries stay deleted). Do it both ways and check: <code>kotsu --conferir</code> shows a short code that is the same on both when the data is the same; <code>kotsu --conferir data.json</code> lists the differences. Before importing, Kotsu saves <code>backups/money.antes-da-troca.sqlite</code>.</li>
|
|
192
|
+
<li><b>No password or encryption:</b> protect your computer and the folder as you would any personal file.</li>
|
|
193
|
+
<li><b>Restore:</b> close the app, move the current <code>money.sqlite</code> elsewhere, copy the chosen backup to <code>money.sqlite</code>, open the app and check the totals.</li>
|
|
194
|
+
</ul>
|
|
195
|
+
</td>
|
|
196
|
+
</tr>
|
|
197
|
+
</table>
|
|
198
|
+
|
|
199
|
+
## V. Under the hood
|
|
200
|
+
|
|
201
|
+
<table>
|
|
202
|
+
<tr>
|
|
203
|
+
<td width="72" align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/estrutura.svg" width="44" alt=""></td>
|
|
204
|
+
<td>Three layers, and a test fails if one of them imports what it must not. Financial rules (totals, installments, the quick-line parser) live in the domain; the screen only displays.</td>
|
|
205
|
+
</tr>
|
|
206
|
+
</table>
|
|
207
|
+
|
|
208
|
+
```mermaid
|
|
209
|
+
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#41274f", "primaryTextColor": "#ffe9f4", "primaryBorderColor": "#ff7fb8", "lineColor": "#ffa28c", "secondaryColor": "#291b33", "tertiaryColor": "#1d1424", "fontFamily": "Segoe UI, sans-serif"}}}%%
|
|
210
|
+
flowchart LR
|
|
211
|
+
T["tela/<br>Textual: display only"] --> A["armazenamento/<br>SQLite, migrations, backup"]
|
|
212
|
+
T --> D["dominio/<br>money in cents,<br>installments, quick line"]
|
|
213
|
+
A --> D
|
|
214
|
+
A --> F[("money.sqlite<br>on your computer")]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- Money is an integer number of cents, never a `float`; the sign comes from the type (income or expense).
|
|
218
|
+
- Every write is a transaction: all or nothing. Changed the schema? A numbered migration, with a copy of the database made first.
|
|
219
|
+
- Totals are never stored: SQL computes them per month.
|
|
220
|
+
|
|
221
|
+
Full contributor rules (in Portuguese): [AGENTS.md](AGENTS.md).
|
|
222
|
+
|
|
223
|
+
## VI. Development
|
|
224
|
+
|
|
225
|
+
<table>
|
|
226
|
+
<tr>
|
|
227
|
+
<td width="72" align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/dev.svg" width="44" alt=""></td>
|
|
228
|
+
<td>Requires Python 3.11+. The tests never touch the network.</td>
|
|
229
|
+
</tr>
|
|
230
|
+
</table>
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
git clone https://github.com/Sera-DAI/kotsu.git
|
|
234
|
+
cd kotsu
|
|
235
|
+
python -m venv .venv
|
|
236
|
+
# Windows: .venv\Scripts\activate Linux: source .venv/bin/activate
|
|
237
|
+
pip install -e ".[dev]"
|
|
238
|
+
kotsu --dados ./tmp # uses a test folder
|
|
239
|
+
python ferramentas/verificar.py --rapido # lint + fast tests
|
|
240
|
+
python ferramentas/verificar.py # everything, same as CI
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## VII. License
|
|
244
|
+
|
|
245
|
+
<table>
|
|
246
|
+
<tr>
|
|
247
|
+
<td width="72" align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/icones/licenca.svg" width="44" alt=""></td>
|
|
248
|
+
<td><a href="https://github.com/Sera-DAI/kotsu/blob/main/LICENSE">MIT</a>. Made with care, little by little.</td>
|
|
249
|
+
</tr>
|
|
250
|
+
</table>
|
|
251
|
+
|
|
252
|
+
<p align="center"><img src="https://raw.githubusercontent.com/Sera-DAI/kotsu/main/docs/imagens/divisor.svg" width="420" alt=""></p>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
money_cli/__init__.py,sha256=fst8zy8ia_p-wERJK3zPVksz5g_PXiKOUHVHxv_RY88,72
|
|
2
|
+
money_cli/__main__.py,sha256=kvoZiYTk_BKgasOophgCNb9AWJWY-8Byu2mEKJCf4Jw,7945
|
|
3
|
+
money_cli/armazenamento/__init__.py,sha256=m2cds88krvPtWa326k9ixijdYc5ymNL5XY5vFrjBOjc,135
|
|
4
|
+
money_cli/armazenamento/backup.py,sha256=dU4VpLSuoeupJx8r9wGxq5nxrXOiQ4Gafcd9XFJCbgg,2673
|
|
5
|
+
money_cli/armazenamento/banco.py,sha256=909n4leU8e_UTkcY0h2rvH-YPnyQ0OiV4HBNjA7JFeQ,20840
|
|
6
|
+
money_cli/armazenamento/caminhos.py,sha256=deCfRPCwnWU9J5tsR4ZQe7tA71k61fzDt7MFu2REfpw,2478
|
|
7
|
+
money_cli/armazenamento/preferencias.py,sha256=YPDoBcNZo4FTAihQJssXLDnwa1uFOvJZvkTl7fbAYAg,1025
|
|
8
|
+
money_cli/armazenamento/trava.py,sha256=zHfmHCfilJy2S4DlxKf87lp5dFlT9MQJ11_ijbJWwKw,4433
|
|
9
|
+
money_cli/armazenamento/troca.py,sha256=tMEYlEpXI3dAfMUyNRDlaimtxvwxZrxCU8J6l9XpZfc,10630
|
|
10
|
+
money_cli/armazenamento/migracoes/0001_inicial.sql,sha256=pO0n8J9gYJZDmrUvkz9ADf9MeuCan8a0RaNX6QJBLDE,1424
|
|
11
|
+
money_cli/armazenamento/migracoes/0002_recorrencia.sql,sha256=yEs_QhQCXRuLA7DXTMS1fZpQo_Aw5kM5rSdqrknm9xU,1122
|
|
12
|
+
money_cli/armazenamento/migracoes/0003_mesclagem.sql,sha256=wr5jaxDlIbtI5fi_m3OnHiN9-F6rI4KHl-vkH07n8ok,2439
|
|
13
|
+
money_cli/armazenamento/migracoes/__init__.py,sha256=fijRhzsluMP5iL9CulV_290UDTFWj6k0yPUVMgkuRI4,1028
|
|
14
|
+
money_cli/dominio/__init__.py,sha256=cmvisdxU6BwtvcundWYs1KQCwSRbzxKLcHNE-cFvrjY,198
|
|
15
|
+
money_cli/dominio/dinheiro.py,sha256=kcuwt09F9evqZdHHOrMc7qGhlkbTG_okEdMuH2T0Mt8,355
|
|
16
|
+
money_cli/dominio/filtro.py,sha256=ZuwodluBDmybLiAAjQuB4e0Eay9QYp2liDcjdbAKRpU,1998
|
|
17
|
+
money_cli/dominio/linha.py,sha256=8DINcGnDUfVZLbqvSzgD1pYcgvxfjMBut_BRxn19GBw,5954
|
|
18
|
+
money_cli/dominio/mes.py,sha256=LXyJ6pKpwIGhJ0tIC0CQCchmrk0Fe8RRcZ8EOjbrMbw,1214
|
|
19
|
+
money_cli/dominio/mescla.py,sha256=dDculF64YjLMKGcXCSRI7UlAhKYjOYNOvCuRoKnoLOU,17295
|
|
20
|
+
money_cli/dominio/parcelas.py,sha256=S3NOgBtXeaL77dmIBDjbfq8cakIzNOBoVSrg_OnOwgc,1596
|
|
21
|
+
money_cli/dominio/recorrencia.py,sha256=THYjBCbLM7y5hbW1bGYXbXInoQvcmeEZVWyoBbLZovo,712
|
|
22
|
+
money_cli/dominio/tipos.py,sha256=W4L_Kzwp60j0U9pIBPfHAHECb9XlOsGuqJcpM8o_xEs,2502
|
|
23
|
+
money_cli/dominio/totais.py,sha256=HHaAbF_LF_Th5FFCOFQz8EpOKyU2sqn5OC921gWxouU,451
|
|
24
|
+
money_cli/tela/__init__.py,sha256=i2hx_7yc99VbenvOKRuKIfA883--ROqSq_d45CMSock,83
|
|
25
|
+
money_cli/tela/app.py,sha256=Jitbt7PHiXf-LaGb4d2snNJP2qE8AGKk3VIsZOwbVCE,32336
|
|
26
|
+
money_cli/tela/cores.py,sha256=M5EPoVLZvyJL0FRq8tx6a_NEh8kevj-b1Jlx_--YtEY,918
|
|
27
|
+
money_cli/tela/perguntas.py,sha256=aSHyOYa92P-Awt5EkcLaNPWKjQEz6DYWsXuazEHiAMY,4581
|
|
28
|
+
money_cli/tela/temas.py,sha256=inxLMPQMXGwl9leM4eCoeQuK8auLmnqRQ1LN1lKZ72E,2203
|
|
29
|
+
kotsu_cli-0.1.0.dist-info/METADATA,sha256=Cv8_NqScSil4PW0mUwtf7msfgOwCcKHTEyYZEayXeis,13640
|
|
30
|
+
kotsu_cli-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
31
|
+
kotsu_cli-0.1.0.dist-info/entry_points.txt,sha256=E0svGN498wq8qvDw8820U6nJItPV5aVpl0qzFopHidw,82
|
|
32
|
+
kotsu_cli-0.1.0.dist-info/licenses/LICENSE,sha256=9_YiWxWshiRbKfTki_c_6nuceaqeME91Vd3S6mXhJMQ,1065
|
|
33
|
+
kotsu_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sera-DAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
money_cli/__init__.py
ADDED
money_cli/__main__.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Ponto de entrada: `kotsu` (ou `money`, ou `python -m money_cli`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import datetime as dt
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from money_cli import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: list[str] | None = None) -> int:
|
|
14
|
+
parser = argparse.ArgumentParser(prog="kotsu", description="Finanças pessoais no terminal.")
|
|
15
|
+
parser.add_argument("--dados", type=Path, help="pasta de dados (padrão: pasta do usuário)")
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--pasta-dados",
|
|
18
|
+
type=Path,
|
|
19
|
+
metavar="PASTA",
|
|
20
|
+
help="guarda PASTA (ex.: no Google Drive) como pasta de dados deste computador e sai",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--pasta-dados-padrao",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="volta à pasta de dados padrão deste computador e sai",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--forcar",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="abre mesmo com o Kotsu aberto em outro computador ou com cópias em conflito",
|
|
31
|
+
)
|
|
32
|
+
troca = parser.add_mutually_exclusive_group()
|
|
33
|
+
troca.add_argument(
|
|
34
|
+
"--exportar",
|
|
35
|
+
type=Path,
|
|
36
|
+
metavar="ARQUIVO",
|
|
37
|
+
help="escreve os dados num arquivo de troca para outro aparelho e sai",
|
|
38
|
+
)
|
|
39
|
+
troca.add_argument(
|
|
40
|
+
"--importar",
|
|
41
|
+
type=Path,
|
|
42
|
+
metavar="ARQUIVO",
|
|
43
|
+
help="junta aos dados daqui o arquivo de troca de outro aparelho e sai",
|
|
44
|
+
)
|
|
45
|
+
troca.add_argument(
|
|
46
|
+
"--conferir",
|
|
47
|
+
type=Path,
|
|
48
|
+
nargs="?",
|
|
49
|
+
const=True,
|
|
50
|
+
metavar="ARQUIVO",
|
|
51
|
+
help="mostra o código de conferência; com ARQUIVO, as diferenças para ele, e sai",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument("--version", action="version", version=f"money-cli {__version__}")
|
|
54
|
+
args = parser.parse_args(argv)
|
|
55
|
+
|
|
56
|
+
if args.pasta_dados is not None or args.pasta_dados_padrao:
|
|
57
|
+
return _configurar_pasta(args)
|
|
58
|
+
|
|
59
|
+
from money_cli.armazenamento import trava
|
|
60
|
+
from money_cli.armazenamento.banco import Banco
|
|
61
|
+
from money_cli.armazenamento.caminhos import arquivo_do_banco, pasta_de_backups
|
|
62
|
+
from money_cli.tela.app import MoneyApp
|
|
63
|
+
|
|
64
|
+
arquivo = arquivo_do_banco(args.dados)
|
|
65
|
+
pasta = arquivo.parent
|
|
66
|
+
copias = trava.conflitos(pasta, arquivo.name)
|
|
67
|
+
if copias and not args.forcar:
|
|
68
|
+
lista = "\n".join(f" {c}" for c in copias)
|
|
69
|
+
print(
|
|
70
|
+
"O Drive guardou cópias do banco em conflito (dois computadores salvaram ao mesmo\n"
|
|
71
|
+
f"tempo):\n{lista}\n"
|
|
72
|
+
"Veja qual tem os lançamentos certos, deixe só ele como money.sqlite e mova as\n"
|
|
73
|
+
"outras para fora da pasta. Para abrir mesmo assim: kotsu --forcar",
|
|
74
|
+
file=sys.stderr,
|
|
75
|
+
)
|
|
76
|
+
return 1
|
|
77
|
+
try:
|
|
78
|
+
minha = trava.tomar(pasta, f"{dt.datetime.now():%d/%m/%Y %H:%M}", args.forcar)
|
|
79
|
+
except trava.BancoEmUso as erro:
|
|
80
|
+
print(
|
|
81
|
+
f"{erro}\nFeche o Kotsu lá e espere o Drive terminar de sincronizar. Se ele foi\n"
|
|
82
|
+
"fechado à força, use: kotsu --forcar",
|
|
83
|
+
file=sys.stderr,
|
|
84
|
+
)
|
|
85
|
+
return 1
|
|
86
|
+
try:
|
|
87
|
+
banco = Banco.abrir(arquivo, pasta_de_backups(pasta))
|
|
88
|
+
try:
|
|
89
|
+
if args.exportar or args.importar or args.conferir:
|
|
90
|
+
return _trocar(banco, args)
|
|
91
|
+
MoneyApp(banco, pasta_tema=pasta).run()
|
|
92
|
+
finally:
|
|
93
|
+
banco.fechar()
|
|
94
|
+
finally:
|
|
95
|
+
minha.liberar()
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _trocar(banco, args: argparse.Namespace) -> int:
|
|
100
|
+
"""`--exportar`, `--importar` e `--conferir` (D39): troca de dados entre aparelhos."""
|
|
101
|
+
from money_cli.armazenamento import troca
|
|
102
|
+
from money_cli.dominio import mescla
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
if args.exportar:
|
|
106
|
+
codigo = troca.exportar(banco, args.exportar.expanduser())
|
|
107
|
+
print(f"Dados exportados para {args.exportar}\nCódigo de conferência: {codigo}")
|
|
108
|
+
elif args.importar:
|
|
109
|
+
resumo = troca.importar(banco, args.importar.expanduser())
|
|
110
|
+
if resumo.nada:
|
|
111
|
+
print("Nada mudou aqui: este aparelho já tinha tudo o que o arquivo traz.")
|
|
112
|
+
else:
|
|
113
|
+
print(
|
|
114
|
+
f"Lançamentos: {resumo.entraram} entraram, {resumo.mudaram} mudaram,"
|
|
115
|
+
f" {resumo.sairam} saíram."
|
|
116
|
+
)
|
|
117
|
+
print(f"Código de conferência: {troca.codigo(banco)}")
|
|
118
|
+
diferencas = troca.comparar(banco, args.importar.expanduser())
|
|
119
|
+
if not diferencas.iguais:
|
|
120
|
+
print(
|
|
121
|
+
"O outro aparelho ainda não tem tudo o que há aqui: exporte daqui e"
|
|
122
|
+
" importe lá para os códigos baterem."
|
|
123
|
+
)
|
|
124
|
+
elif args.conferir is True:
|
|
125
|
+
print(f"Código de conferência: {troca.codigo(banco)}")
|
|
126
|
+
else:
|
|
127
|
+
aqui = troca.ler_estado(banco)
|
|
128
|
+
la = troca.ler_arquivo(args.conferir.expanduser())
|
|
129
|
+
diferencas = mescla.comparar(aqui, la)
|
|
130
|
+
print(f"Código daqui: {diferencas.codigo_aqui}")
|
|
131
|
+
print(f"Código do arquivo: {diferencas.codigo_la}")
|
|
132
|
+
if diferencas.iguais:
|
|
133
|
+
print("Iguais: os dois têm os mesmos dados.")
|
|
134
|
+
return 0
|
|
135
|
+
for titulo, itens, estado in (
|
|
136
|
+
("Só aqui", diferencas.so_aqui, aqui),
|
|
137
|
+
("Só no arquivo", diferencas.so_la, la),
|
|
138
|
+
):
|
|
139
|
+
if itens:
|
|
140
|
+
print(f"\n{titulo} ({len(itens)}):")
|
|
141
|
+
for item in itens:
|
|
142
|
+
print(f" {mescla.descrever(item, estado)}")
|
|
143
|
+
if diferencas.diferentes:
|
|
144
|
+
print(f"\nDiferentes ({len(diferencas.diferentes)}):")
|
|
145
|
+
for meu, dele in diferencas.diferentes:
|
|
146
|
+
print(f" aqui: {mescla.descrever(meu, aqui)}")
|
|
147
|
+
print(f" arquivo: {mescla.descrever(dele, la)}")
|
|
148
|
+
if not (diferencas.so_aqui or diferencas.so_la or diferencas.diferentes):
|
|
149
|
+
print("Os lançamentos são os mesmos; a diferença está em contas, categorias,")
|
|
150
|
+
print("séries de todo mês ou exclusões ainda não trocadas.")
|
|
151
|
+
return 2
|
|
152
|
+
except mescla.ErroDeTroca as erro:
|
|
153
|
+
print(f"Não deu certo: {erro}", file=sys.stderr)
|
|
154
|
+
return 1
|
|
155
|
+
except OSError as erro:
|
|
156
|
+
print(f"Não foi possível gravar o arquivo: {erro}", file=sys.stderr)
|
|
157
|
+
return 1
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _configurar_pasta(args: argparse.Namespace) -> int:
|
|
162
|
+
"""`--pasta-dados`: aponta este computador para uma pasta (ex.: do Drive) e sai."""
|
|
163
|
+
from money_cli.armazenamento import caminhos
|
|
164
|
+
from money_cli.armazenamento.banco import copiar_arquivo_banco
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
if args.pasta_dados_padrao:
|
|
168
|
+
caminhos.gravar_pasta_configurada(None)
|
|
169
|
+
print(f"Pasta de dados: {caminhos.pasta_local()} (padrão).")
|
|
170
|
+
return 0
|
|
171
|
+
destino = args.pasta_dados.expanduser().resolve()
|
|
172
|
+
novo = caminhos.arquivo_do_banco(destino)
|
|
173
|
+
atual = caminhos.arquivo_do_banco(caminhos.pasta_de_dados())
|
|
174
|
+
if atual.resolve() != novo and atual.exists():
|
|
175
|
+
if novo.exists():
|
|
176
|
+
print(
|
|
177
|
+
f"Já existe um banco em {novo} e outro em {atual}. O Kotsu não junta os\n"
|
|
178
|
+
"dois: escolha qual vale, mova o outro para fora e rode de novo.",
|
|
179
|
+
file=sys.stderr,
|
|
180
|
+
)
|
|
181
|
+
return 1
|
|
182
|
+
destino.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
copiar_arquivo_banco(atual, novo)
|
|
184
|
+
print(f"Banco copiado para {novo} (o original continua em {atual}).")
|
|
185
|
+
caminhos.gravar_pasta_configurada(destino)
|
|
186
|
+
except OSError as erro:
|
|
187
|
+
print(f"Não foi possível configurar a pasta: {erro}", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
print(
|
|
190
|
+
f"Pasta de dados deste computador: {destino}\nBackups (tecla b) ficam em "
|
|
191
|
+
f"{caminhos.pasta_local() / 'backups'}, fora da sincronização."
|
|
192
|
+
)
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Backup manual do SQLite: cada pedido publica uma cópia completa nova e mantém as 5 últimas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sqlite3
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
MANTER = 5
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def backup_manual(
|
|
16
|
+
conexao: sqlite3.Connection,
|
|
17
|
+
caminho: Path,
|
|
18
|
+
agora: dt.datetime,
|
|
19
|
+
pasta: Path | None = None,
|
|
20
|
+
) -> Path:
|
|
21
|
+
from money_cli.armazenamento.banco import copiar_banco
|
|
22
|
+
|
|
23
|
+
pasta = pasta or caminho.parent / "backups"
|
|
24
|
+
pasta.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
base = f"{caminho.stem}.{agora:%Y-%m-%d-%H%M%S}"
|
|
26
|
+
descritor, nome = tempfile.mkstemp(prefix=".backup-", suffix=".tmp", dir=pasta)
|
|
27
|
+
os.close(descritor) # Windows: fechar antes de SQLite abrir o mesmo arquivo.
|
|
28
|
+
temporario = Path(nome)
|
|
29
|
+
try:
|
|
30
|
+
copiar_banco(conexao, temporario)
|
|
31
|
+
with temporario.open("r+b") as arquivo:
|
|
32
|
+
os.fsync(arquivo.fileno())
|
|
33
|
+
# Mesmo segundo: sufixo maior que o de qualquer cópia existente, para que a ordem
|
|
34
|
+
# por nome continue sendo a ordem de criação depois da poda.
|
|
35
|
+
usados = [_numero(p.name, base) for p in pasta.glob(f"{base}*.sqlite")]
|
|
36
|
+
for tentativa in range(max([0, *usados]) + 1, max([0, *usados]) + 1000):
|
|
37
|
+
destino = pasta / (f"{base}.sqlite" if tentativa == 1 else f"{base}-{tentativa}.sqlite")
|
|
38
|
+
try:
|
|
39
|
+
# Publicação atômica sem sobrescrever cópia existente (NTFS e Linux).
|
|
40
|
+
os.link(temporario, destino)
|
|
41
|
+
break
|
|
42
|
+
except FileExistsError:
|
|
43
|
+
continue
|
|
44
|
+
else:
|
|
45
|
+
raise OSError("não foi possível escolher um nome livre para o backup")
|
|
46
|
+
finally:
|
|
47
|
+
temporario.unlink(missing_ok=True)
|
|
48
|
+
_podar(pasta, caminho.stem)
|
|
49
|
+
return destino
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _numero(nome: str, base: str) -> int:
|
|
53
|
+
resto = nome[len(base) : -len(".sqlite")]
|
|
54
|
+
return int(resto[1:]) if re.fullmatch(r"-\d+", resto) else 1 if resto == "" else 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _podar(pasta: Path, stem: str) -> None:
|
|
58
|
+
"""Só depois de publicar: mantém as `MANTER` mais recentes.
|
|
59
|
+
|
|
60
|
+
Só olha cópias com o nome deste módulo (`money.AAAA-MM-DD-HHMMSS[-N].sqlite`); cópias de
|
|
61
|
+
pré-migração, backups de outro formato e outros arquivos nunca são tocados.
|
|
62
|
+
"""
|
|
63
|
+
padrao = re.compile(re.escape(stem) + r"\.(\d{4}-\d{2}-\d{2}-\d{6})(?:-(\d+))?\.sqlite")
|
|
64
|
+
copias = []
|
|
65
|
+
for arquivo in pasta.iterdir():
|
|
66
|
+
achado = padrao.fullmatch(arquivo.name)
|
|
67
|
+
if achado and arquivo.is_file():
|
|
68
|
+
copias.append(((achado[1], int(achado[2] or 1)), arquivo))
|
|
69
|
+
for _, antigo in sorted(copias, reverse=True)[MANTER:]:
|
|
70
|
+
antigo.unlink(missing_ok=True)
|