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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ kotsu = money_cli.__main__:main
3
+ money = money_cli.__main__:main
@@ -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
@@ -0,0 +1,3 @@
1
+ """Money CLI: finanças pessoais no terminal."""
2
+
3
+ __version__ = "0.1.0"
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,4 @@
1
+ """Camada de armazenamento: SQLite, migrações, gravação atômica.
2
+
3
+ Depende só de `dominio`. Nunca importa `tela` nem Textual.
4
+ """
@@ -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)