stundu 0.3.0__tar.gz

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.
Files changed (79) hide show
  1. stundu-0.3.0/.gitignore +36 -0
  2. stundu-0.3.0/.gitlab-ci.yml +33 -0
  3. stundu-0.3.0/CONTRIBUTING.md +133 -0
  4. stundu-0.3.0/LICENSE +21 -0
  5. stundu-0.3.0/PKG-INFO +484 -0
  6. stundu-0.3.0/README.md +459 -0
  7. stundu-0.3.0/__init__.py +0 -0
  8. stundu-0.3.0/billable_screenshot.png +0 -0
  9. stundu-0.3.0/build-snap.sh +27 -0
  10. stundu-0.3.0/cli/__init__.py +36 -0
  11. stundu-0.3.0/cli/config.py +182 -0
  12. stundu-0.3.0/cli/formats.py +179 -0
  13. stundu-0.3.0/cli/i18n_setup.py +94 -0
  14. stundu-0.3.0/cli/locales/de.json +174 -0
  15. stundu-0.3.0/cli/locales/en.json +174 -0
  16. stundu-0.3.0/cli/locales/es.json +174 -0
  17. stundu-0.3.0/cli/locales/et.json +174 -0
  18. stundu-0.3.0/cli/locales/fr.json +174 -0
  19. stundu-0.3.0/cli/locales/it.json +174 -0
  20. stundu-0.3.0/cli/locales/lt.json +174 -0
  21. stundu-0.3.0/cli/locales/lv.json +174 -0
  22. stundu-0.3.0/cli/locales/pl.json +174 -0
  23. stundu-0.3.0/cli/locales/pt.json +174 -0
  24. stundu-0.3.0/cli/locales/uk.json +174 -0
  25. stundu-0.3.0/core/__init__.py +32 -0
  26. stundu-0.3.0/core/billing.py +481 -0
  27. stundu-0.3.0/core/engine.py +85 -0
  28. stundu-0.3.0/core/entry_references.py +182 -0
  29. stundu-0.3.0/core/invoice.py +152 -0
  30. stundu-0.3.0/core/models.py +94 -0
  31. stundu-0.3.0/core/rates.py +304 -0
  32. stundu-0.3.0/core/time_segments.py +161 -0
  33. stundu-0.3.0/docs/concepts.md +188 -0
  34. stundu-0.3.0/docs/examples.md +145 -0
  35. stundu-0.3.0/docs/release.md +97 -0
  36. stundu-0.3.0/exporters/__init__.py +93 -0
  37. stundu-0.3.0/exporters/ledger.py +223 -0
  38. stundu-0.3.0/exporters/timewarrior.py +159 -0
  39. stundu-0.3.0/importers/__init__.py +116 -0
  40. stundu-0.3.0/importers/ledger.py +207 -0
  41. stundu-0.3.0/importers/timewarrior.py +192 -0
  42. stundu-0.3.0/logo.png +0 -0
  43. stundu-0.3.0/logo.svg +417 -0
  44. stundu-0.3.0/logo1.png +0 -0
  45. stundu-0.3.0/pyproject.toml +47 -0
  46. stundu-0.3.0/requirements-dev.txt +4 -0
  47. stundu-0.3.0/requirements-snap.txt +7 -0
  48. stundu-0.3.0/requirements.txt +8 -0
  49. stundu-0.3.0/setup.cfg +21 -0
  50. stundu-0.3.0/snap/snapcraft.yaml +30 -0
  51. stundu-0.3.0/storage/__init__.py +4 -0
  52. stundu-0.3.0/storage/base.py +26 -0
  53. stundu-0.3.0/storage/csv_storage.py +381 -0
  54. stundu-0.3.0/stundu.egg-info/PKG-INFO +484 -0
  55. stundu-0.3.0/stundu.egg-info/SOURCES.txt +78 -0
  56. stundu-0.3.0/stundu.egg-info/dependency_links.txt +1 -0
  57. stundu-0.3.0/stundu.egg-info/entry_points.txt +2 -0
  58. stundu-0.3.0/stundu.egg-info/requires.txt +15 -0
  59. stundu-0.3.0/stundu.egg-info/top_level.txt +6 -0
  60. stundu-0.3.0/stundu.py +1781 -0
  61. stundu-0.3.0/templates/invoice.html +314 -0
  62. stundu-0.3.0/tests/__init__.py +1 -0
  63. stundu-0.3.0/tests/conftest.py +8 -0
  64. stundu-0.3.0/tests/test_billing.py +591 -0
  65. stundu-0.3.0/tests/test_csv_storage.py +534 -0
  66. stundu-0.3.0/tests/test_engine.py +216 -0
  67. stundu-0.3.0/tests/test_entry_references.py +200 -0
  68. stundu-0.3.0/tests/test_exporter_ledger.py +396 -0
  69. stundu-0.3.0/tests/test_exporter_timewarrior.py +305 -0
  70. stundu-0.3.0/tests/test_exporters.py +57 -0
  71. stundu-0.3.0/tests/test_formats.py +173 -0
  72. stundu-0.3.0/tests/test_importer_ledger.py +281 -0
  73. stundu-0.3.0/tests/test_importer_timewarrior.py +204 -0
  74. stundu-0.3.0/tests/test_importers.py +68 -0
  75. stundu-0.3.0/tests/test_invoice.py +355 -0
  76. stundu-0.3.0/tests/test_models.py +128 -0
  77. stundu-0.3.0/tests/test_rates.py +255 -0
  78. stundu-0.3.0/tests/test_report.py +219 -0
  79. stundu-0.3.0/tests/test_time_segments.py +246 -0
@@ -0,0 +1,36 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+
8
+ # Virtual environment
9
+ venv/
10
+ .venv/
11
+
12
+ # Testing
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+
17
+ # User data (don't commit personal time tracking data)
18
+ billable_data.csv
19
+ *.bak
20
+
21
+ # IDE
22
+ .idea/
23
+ .vscode/
24
+ *.swp
25
+ *.swo
26
+
27
+ # Distribution
28
+ dist/
29
+ build/
30
+ *.egg-info/
31
+
32
+ # Snapcraft build artifacts
33
+ parts/
34
+ stage/
35
+ prime/
36
+ *.snap
@@ -0,0 +1,33 @@
1
+ # You can override the included template(s) by including variable overrides
2
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
3
+ # Secret Detection customization: https://docs.gitlab.com/user/application_security/secret_detection/pipeline/configure
4
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
5
+ # Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
6
+ # Note that environment variables can be set in several places
7
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
8
+ stages:
9
+ - test
10
+ - secret-detection
11
+
12
+ python-test:
13
+ stage: test
14
+ image: python:3.11
15
+ before_script:
16
+ - pip install -r requirements-dev.txt
17
+ script:
18
+ - flake8 stundu.py core/ storage/ importers/ exporters/ tests/ --ignore=E203,E501,W503,E231
19
+ - pytest -q --cov --cov-report=xml:coverage.xml
20
+ - curl -Os https://cli.codecov.io/latest/linux/codecov
21
+ - chmod +x codecov
22
+ - ./codecov --verbose upload-process --fail-on-error -t $CODECOV_TOKEN -r mrtmednis/stundu -F unittests -f ./coverage.xml
23
+
24
+
25
+ sast:
26
+ stage: test
27
+ include:
28
+ - template: Security/SAST.gitlab-ci.yml
29
+ - template: Security/Secret-Detection.gitlab-ci.yml
30
+ variables:
31
+ SECRET_DETECTION_ENABLED: 'true'
32
+ secret_detection:
33
+ stage: secret-detection
@@ -0,0 +1,133 @@
1
+ # Contributing
2
+
3
+ Thank you for your interest in contributing to Stundu!
4
+
5
+ ## Development Setup
6
+
7
+ 1. Clone the repository and navigate to the project directory:
8
+
9
+ ```bash
10
+ git clone <repository-url>
11
+ cd stundu
12
+ ```
13
+
14
+ 2. Create a virtual environment:
15
+
16
+ ```bash
17
+ python3 -m venv venv
18
+ ```
19
+
20
+ 3. Activate the virtual environment:
21
+
22
+ ```bash
23
+ # Linux/macOS
24
+ source venv/bin/activate
25
+
26
+ # Windows
27
+ venv\Scripts\activate
28
+ ```
29
+
30
+ 4. Install dependencies:
31
+
32
+ ```bash
33
+ pip install -r requirements.txt
34
+ ```
35
+
36
+ 5. Install development dependencies:
37
+
38
+ ```bash
39
+ pip install pytest pytest-cov flake8 black
40
+ ```
41
+
42
+ ## Running Tests
43
+
44
+ Run the linter:
45
+
46
+ ```bash
47
+ flake8 stundu.py core/ storage/ cli/ importers/ tests/
48
+ ```
49
+
50
+ Run the tests:
51
+
52
+ ```bash
53
+ pytest
54
+ ```
55
+
56
+ Run tests with coverage:
57
+
58
+ ```bash
59
+ pytest --cov
60
+ ```
61
+
62
+ ## Code Style
63
+
64
+ This project uses [Black](https://black.readthedocs.io/) for code formatting with default settings (88 character line length).
65
+
66
+ Format code before committing:
67
+
68
+ ```bash
69
+ black stundu.py core/ storage/ cli/ importers/ tests/
70
+ ```
71
+
72
+ ## Pull Requests
73
+
74
+ 1. Fork the repository
75
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
76
+ 3. Make your changes
77
+ 4. Run tests and linter
78
+ 5. Commit your changes
79
+ 6. Push to your fork
80
+ 7. Open a pull request
81
+
82
+ ## Adding Translations
83
+
84
+ Translation files are located in `cli/locales/`. To add a new language:
85
+
86
+ 1. Copy `cli/locales/en.json` to `cli/locales/<code>.json`
87
+ 2. Translate all strings
88
+ 3. Add the locale mapping in `cli/formats.py` (`LOCALE_MAP`)
89
+ 4. Update the Available Translations table in `README.md`
90
+
91
+ ## Building the Snap Package
92
+
93
+ Snap builds are done inside a [Multipass](https://multipass.run/) VM to keep the host clean.
94
+
95
+ ### One-time: create the VM
96
+
97
+ ```bash
98
+ multipass launch --name snap-build --disk 20G 24.04
99
+ ```
100
+
101
+ The default 5 GB disk fills up during the build, so 20 GB is safer.
102
+
103
+ ### Transfer source and build
104
+
105
+ Commit all changes first, then bundle the repo and transfer it to the VM:
106
+
107
+ ```bash
108
+ git bundle create ~/stundu.bundle --all
109
+ multipass transfer ~/stundu.bundle snap-build:/home/ubuntu/stundu.bundle
110
+ multipass exec snap-build -- sudo rm -rf /home/ubuntu/build
111
+ multipass exec snap-build -- git clone /home/ubuntu/stundu.bundle /home/ubuntu/build
112
+ multipass exec snap-build -- bash -c "cd /home/ubuntu/build && sudo snapcraft pack --destructive-mode"
113
+ ```
114
+
115
+ Using a git bundle avoids sshfs permission issues and ensures git tags are available for versioning. `--destructive-mode` builds directly in the VM, bypassing the nested LXD container that snapcraft uses by default.
116
+
117
+ ### Retrieve and install
118
+
119
+ ```bash
120
+ multipass transfer 'snap-build:/home/ubuntu/build/stundu_<version>_amd64.snap' .
121
+ sudo snap install --dangerous stundu_<version>_amd64.snap
122
+ ```
123
+
124
+ `--dangerous` is required for locally built, unsigned snaps. Note that fish shell does not expand wildcards in remote paths — use the exact filename.
125
+
126
+ ## Adding Importers
127
+
128
+ To add support for importing from a new time tracker:
129
+
130
+ 1. Create a new file in `importers/` (e.g., `importers/mytool.py`)
131
+ 2. Implement the `Importer` protocol from `importers/base.py`
132
+ 3. Register the importer in `importers/registry.py`
133
+ 4. Add tests in `tests/test_import.py`
stundu-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Martins Mednis
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.
stundu-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,484 @@
1
+ Metadata-Version: 2.4
2
+ Name: stundu
3
+ Version: 0.3.0
4
+ Summary: Time tracking library with billing calculations
5
+ License: MIT
6
+ Project-URL: Homepage, https://gitlab.com/mrtmednis/stundu
7
+ Project-URL: Repository, https://gitlab.com/mrtmednis/stundu
8
+ Project-URL: Issues, https://gitlab.com/mrtmednis/stundu/-/issues
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: mathjson-solver==1.18.0
13
+ Requires-Dist: tals>=0.1.0
14
+ Requires-Dist: jinja2==3.1.6
15
+ Provides-Extra: cli
16
+ Requires-Dist: typer==0.19.2; extra == "cli"
17
+ Requires-Dist: tabulate==0.9.0; extra == "cli"
18
+ Requires-Dist: python-i18n==0.3.9; extra == "cli"
19
+ Requires-Dist: babel==2.17.0; extra == "cli"
20
+ Provides-Extra: pdf
21
+ Requires-Dist: weasyprint==65.1; extra == "pdf"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest==8.3.5; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # Stundu
27
+
28
+ [![codecov](https://codecov.io/gl/mrtmednis/stundu/graph/badge.svg)](https://codecov.io/gl/mrtmednis/stundu)
29
+
30
+ > **Note:** This project was previously published as **billable** on the Snap Store and GitLab. The name was too generic and heavily overused across the web, making the project hard to find. The old `billable` snap is deprecated — please install `stundu` instead.
31
+
32
+ A command-line time tracking application with built-in billing calculations. Track work entries with timestamps and tags, calculate durations, compute billing based on configurable hourly rates with support for afterhours and holiday/weekend modifiers, generate professional invoices, and import data from other time trackers.
33
+
34
+ ## Table of Contents
35
+
36
+ - [Installation](#installation)
37
+ - [Quick Start](#quick-start)
38
+ - [Configuration](#configuration)
39
+ - [Expression-Based Rates](#expression-based-rates)
40
+ - [Localization](#localization)
41
+ - [Commands](#commands)
42
+ - [Supported Importers](#supported-importers)
43
+ - [Data Storage](#data-storage)
44
+ - [Documentation](#documentation)
45
+ - [Contributing](#contributing)
46
+ - [License](#license)
47
+
48
+ ## Installation
49
+
50
+ There are three ways to get Stundu depending on your use case:
51
+
52
+ | Method | Best for |
53
+ |--------|----------|
54
+ | [Snap](#snap) | End users on Linux who want the CLI |
55
+ | [pip](#pip) | Developers who want to use the core library |
56
+ | [From source](#from-source) | Contributing or hacking on the project |
57
+
58
+ ### Snap
59
+
60
+ The easiest way to install the CLI on Linux:
61
+
62
+ ```bash
63
+ sudo snap install stundu
64
+ ```
65
+
66
+ Or visit [snapcraft.io/stundu](https://snapcraft.io/stundu).
67
+
68
+ ### pip
69
+
70
+ Install the core library (no CLI dependencies):
71
+
72
+ ```bash
73
+ pip install stundu
74
+ ```
75
+
76
+ Install with the CLI included:
77
+
78
+ ```bash
79
+ pip install stundu[cli]
80
+ ```
81
+
82
+ Install with PDF invoice support:
83
+
84
+ ```bash
85
+ pip install stundu[cli,pdf]
86
+ ```
87
+
88
+ The core library exposes the billing engine, data models, storage backend, and importers/exporters. The `stundu` command requires the `[cli]` extra.
89
+
90
+ ### From Source
91
+
92
+ #### Prerequisites
93
+
94
+ - Python 3.11 or higher
95
+
96
+ #### Setup
97
+
98
+ 1. Clone the repository and navigate to the project directory:
99
+
100
+ ```bash
101
+ cd stundu
102
+ ```
103
+
104
+ 2. Create a virtual environment:
105
+
106
+ ```bash
107
+ python3 -m venv venv
108
+ ```
109
+
110
+ 3. Activate the virtual environment:
111
+
112
+ ```bash
113
+ # Linux/macOS
114
+ source venv/bin/activate
115
+
116
+ # Windows
117
+ venv\Scripts\activate
118
+ ```
119
+
120
+ 4. Install dependencies:
121
+
122
+ ```bash
123
+ pip install -r requirements.txt
124
+ ```
125
+
126
+ ## Quick Start
127
+
128
+ ```bash
129
+ python stundu.py start "Acme Corp" "Development"
130
+ ```
131
+ ```
132
+ Started tracking: Acme Corp, Development
133
+ Started at: 09:00
134
+ ```
135
+
136
+ ```bash
137
+ python stundu.py status
138
+ ```
139
+ ```
140
+ Currently tracking: Acme Corp, Development
141
+ Started at: 09:00
142
+ Elapsed: 2h 15m
143
+ ```
144
+
145
+ ```bash
146
+ python stundu.py stop
147
+ ```
148
+ ```
149
+ Stopped tracking: Acme Corp, Development
150
+ Duration: 3h 0m
151
+ ```
152
+
153
+ ```bash
154
+ python stundu.py log
155
+ ```
156
+ ```
157
+ @ Start End Duration Tags Status
158
+ -- ------- ------- ---------- ---------------------- --------
159
+ @1 09:00 12:00 3h 0m Acme Corp, Development
160
+ ```
161
+
162
+ ```bash
163
+ python stundu.py configure rate "Acme Corp" 50 EUR
164
+ python stundu.py report
165
+ ```
166
+ ```
167
+ Report for 'Acme Corp' (Jan 1, 2026 to Jan 31, 2026)
168
+ ────────────────────────────────────────────────────────────
169
+ ╭──────────────┬───────┬───────┬──────────┬─────────────┬────────────╮
170
+ │ Date │ Start │ End │ Duration │ Tags │ Cost │
171
+ ├──────────────┼───────┼───────┼──────────┼─────────────┼────────────┤
172
+ │ Jan 15, 2026 │ 09:00 │ 12:00 │ 3h 0m │ Development │ 150.00 EUR │
173
+ │ │ 14:00 │ 18:00 │ 4h 0m │ Development │ 200.00 EUR │
174
+ │ │ 18:00 │ 20:00 │ 2h 0m │ Development │ 150.00 EUR │
175
+ ├──────────────┼───────┼───────┼──────────┼─────────────┼────────────┤
176
+ │ Jan 18, 2026 │ 10:00 │ 14:00 │ 4h 0m │ Development │ 400.00 EUR │
177
+ ╰──────────────┴───────┴───────┴──────────┴─────────────┴────────────╯
178
+
179
+ ────────────────────────────────────────────────────────────
180
+ Total time: 13h 0m
181
+
182
+ Total:
183
+ 900.00 EUR
184
+ ```
185
+
186
+ See [docs/examples.md](docs/examples.md) for more detailed examples.
187
+
188
+ ## Configuration
189
+
190
+ ### First Run
191
+
192
+ On first run, Stundu displays a welcome message and prompts you to run the setup wizard:
193
+
194
+ ```
195
+ Welcome to Stundu!
196
+ This appears to be your first run.
197
+ Run 'stundu.py setup' to configure.
198
+ ```
199
+
200
+ Run `stundu.py setup` to create a configuration file interactively.
201
+
202
+ ### Config File
203
+
204
+ The default config file location is `~/.config/stundu/config.json`:
205
+
206
+ ```json
207
+ {
208
+ "locale": "lv",
209
+ "style": "rounded_grid",
210
+ "data_file": "/path/to/my/timesheet.csv",
211
+ "formats": {
212
+ "date": "short",
213
+ "time": "short",
214
+ "datetime": "medium"
215
+ }
216
+ }
217
+ ```
218
+
219
+ Run `configure` without arguments to see which config file is in use and what values are active.
220
+
221
+ #### Table Style
222
+
223
+ The `style` key controls how tables are rendered in `report`, `log`, and `rates`. Set it with:
224
+
225
+ ```bash
226
+ python stundu.py configure style rounded_grid
227
+ ```
228
+
229
+ Available styles (run `configure style --help` for the full list):
230
+
231
+ | Tier | Styles | Divider |
232
+ |------|--------|---------|
233
+ | ASCII | `plain`, `simple`, `grid`, `github`, `psql`, `pipe`, `presto`, `pretty` | `-` |
234
+ | Light | `simple_grid`, `rounded_grid`, `outline`, `simple_outline`, `rounded_outline` | `─` |
235
+ | Heavy | `heavy_grid`, `double_grid`, `fancy_grid`, `heavy_outline`, `double_outline`, `fancy_outline` | `━` |
236
+
237
+ The default is `simple`. The Quick Start section shows an example report rendered with `rounded_grid`.
238
+
239
+ #### Date/Time Format Options
240
+
241
+ The `formats` section controls how dates and times are displayed. You can use:
242
+
243
+ **Preset styles** (locale-aware):
244
+ - `short` - Compact format (e.g., "1/15/26" in English, "15.01.26" in German)
245
+ - `medium` - Default format (e.g., "Jan 15, 2026")
246
+ - `long` - Full month name (e.g., "January 15, 2026")
247
+ - `full` - Complete format (e.g., "Thursday, January 15, 2026")
248
+
249
+ **Custom patterns** (using Unicode CLDR format):
250
+ ```json
251
+ {
252
+ "formats": {
253
+ "date": "dd.MM.yyyy",
254
+ "time": "HH:mm",
255
+ "datetime": "yyyy-MM-dd HH:mm"
256
+ }
257
+ }
258
+ ```
259
+
260
+ ### Environment Variables
261
+
262
+ | Variable | Description |
263
+ |----------|-------------|
264
+ | `STUNDU_CONFIG` | Custom config file path |
265
+ | `STUNDU_DATA` | Custom data file path |
266
+
267
+ ### CLI Options
268
+
269
+ | Option | Description |
270
+ |--------|-------------|
271
+ | `--lang`, `-L` | Override language (e.g., `--lang=de`) |
272
+ | `--data`, `-d` | Override data file path for this command |
273
+
274
+ ### Precedence
275
+
276
+ **Data file location** (highest to lowest priority):
277
+ 1. `--data` CLI option
278
+ 2. `STUNDU_DATA` environment variable
279
+ 3. `data_file` in config file
280
+ 4. Default: `stundu_data.csv` in current directory
281
+
282
+ **Config file location**:
283
+ 1. `STUNDU_CONFIG` environment variable
284
+ 2. Default: `~/.config/stundu/config.json`
285
+
286
+ ## Expression-Based Rates
287
+
288
+ Beyond fixed hourly rates, Billable supports MathJSON expressions as the `hourly_rate`. This lets you implement billing models where the effective rate depends on how much time has already been billed — retainers, volume discounts, loyalty pricing, and more. Entries that cross a rate-change threshold mid-session are automatically split at the exact minute the threshold is reached.
289
+
290
+ Expression-based rates are set by editing the data file directly (they are an advanced feature):
291
+
292
+ ```
293
+ SET_HOURLY_RATE "Client" <expression> <currency>
294
+ ```
295
+
296
+ ### First Hour Free, Then 50 EUR/h (Monthly Retainer)
297
+
298
+ ```
299
+ SET_HOURLY_RATE "Acme Corp" ["If",[["Less",["Sum","[Acme Corp][0M:+1M][hours]"],1],0],50] EUR
300
+ ```
301
+
302
+ The expression sums all logged hours for "Acme Corp" in the current calendar month. While the total is under 1 hour the rate is 0; after that it's 50 EUR/h. The counter resets automatically at the start of each month.
303
+
304
+ ### Volume Discount — Full Rate for First 20 h, Lower Rate After
305
+
306
+ ```
307
+ SET_HOURLY_RATE "Acme Corp" ["If",[["Less",["Sum","[Acme Corp][0M:+1M][hours]"],20],80],60] EUR
308
+ ```
309
+
310
+ First 20 hours each month: 80 EUR/h. Everything beyond: 60 EUR/h.
311
+
312
+ ### Loyalty Rate — Cheaper After 100 Lifetime Hours
313
+
314
+ ```
315
+ SET_HOURLY_RATE "Acme Corp" ["If",[["Less",["Sum","[Acme Corp][:-1][hours]"],100],75],50] EUR
316
+ ```
317
+
318
+ `[:-1]` sums all entries *before* the current one. The rate starts at 75 EUR/h and drops to 50 EUR/h once the client has accumulated 100 hours of lifetime work.
319
+
320
+ See [docs/concepts.md](docs/concepts.md) for the full reference, including the entry reference syntax, available fields, and how expression-based rates compose with afterhours and holiday modifiers.
321
+
322
+ ## Localization
323
+
324
+ The CLI supports multiple languages with full localization of messages and date/time formats. The language is detected automatically from your system locale, or you can override it with `--lang`:
325
+
326
+ ```bash
327
+ python stundu.py --lang=de status
328
+ python stundu.py --lang=es log
329
+ ```
330
+
331
+ Date and time formatting automatically adapts to your locale using [Babel](https://babel.pocoo.org/):
332
+
333
+ | Locale | Date Example | Time |
334
+ |--------|----------------------|---------|
335
+ | en | Jan 15, 2026 | 2:30 PM |
336
+ | de | 15.01.2026 | 14:30 |
337
+ | fr | 15 janv. 2026 | 14:30 |
338
+ | lv | 2026. gada 15. janv. | 14:30 |
339
+
340
+ ### Available Translations
341
+
342
+ | Code | Language |
343
+ |------|------------|
344
+ | en | English |
345
+ | de | German |
346
+ | es | Spanish |
347
+ | et | Estonian |
348
+ | fr | French |
349
+ | it | Italian |
350
+ | lt | Lithuanian |
351
+ | lv | Latvian |
352
+ | pl | Polish |
353
+ | pt | Portuguese |
354
+ | uk | Ukrainian |
355
+
356
+ ## Commands
357
+
358
+ ### Time Tracking
359
+
360
+ | Command | Description |
361
+ |---------|-------------|
362
+ | `start <tags...>` | Start tracking time with one or more tags |
363
+ | `stop` | Stop the currently active entry |
364
+ | `status` | Show the currently active entry |
365
+ | `continue` | Resume tracking with the same tags as the last entry |
366
+ | `cancel` | Discard the currently active entry without saving |
367
+
368
+ ### Viewing Entries
369
+
370
+ | Command | Description |
371
+ |---------|-------------|
372
+ | `log` | Show all time entries in a table |
373
+ | `report` | Show billing report (defaults to current month) |
374
+ | `rates` | Show configured rates for all tags |
375
+
376
+ Report options:
377
+ - `--tag`, `-t` - Filter by tag
378
+ - `--month`, `-m` - Current month (default when no filter is given)
379
+ - `--last-month`, `-l` - Previous month
380
+ - `--from` - Start date filter (YYYY-MM-DD)
381
+ - `--to` - End date filter (YYYY-MM-DD)
382
+
383
+ When no date filter is specified, the report shows the current month. If there are no entries in the current month, it falls back to all entries from the last day that has data.
384
+
385
+ ### Configuration
386
+
387
+ All configuration commands are grouped under the `configure` command:
388
+
389
+ | Command | Description |
390
+ |---------|-------------|
391
+ | `configure` | Show active config sources and available subcommands |
392
+ | `configure rate <tag> <amount> <currency>` | Set hourly rate for a tag |
393
+ | `configure workday <hours> [tag]` | Set workday hours (e.g., 9:00-18:00) |
394
+ | `configure afterhours <tag> <multiplier>` | Set afterhours rate multiplier |
395
+ | `configure holiday <tag> <multiplier>` | Set holiday/weekend rate multiplier |
396
+ | `configure week-start <0-6>` | Set week start day for calendar expressions (0=Mon, 6=Sun) |
397
+ | `configure style <name>` | Set table display style |
398
+
399
+ Personal information (for invoices):
400
+
401
+ | Command | Description |
402
+ |---------|-------------|
403
+ | `configure my-name <name>` | Set your name |
404
+ | `configure my-address <address>` | Set your address |
405
+ | `configure my-email <email>` | Set your email |
406
+ | `configure my-vat <vat>` | Set your VAT number |
407
+ | `configure my-bank <bank>` | Set your bank name |
408
+ | `configure my-bank-account <account>` | Set your bank account number |
409
+ | `configure my-bic <bic>` | Set your bank BIC/SWIFT code |
410
+ | `configure my-note <note>` | Set a note to appear on invoices |
411
+
412
+ Client information (for invoices):
413
+
414
+ | Command | Description |
415
+ |---------|-------------|
416
+ | `configure client-address <tag> <address>` | Set client address |
417
+ | `configure client-vat <tag> <vat>` | Set client VAT number |
418
+ | `configure client-note <tag> <note>` | Set client-specific note |
419
+
420
+ ### Entry Management
421
+
422
+ | Command | Description |
423
+ |---------|-------------|
424
+ | `modify <@N> [options]` | Modify an existing entry |
425
+ | `delete <@N or tag>` | Delete entries by reference or tag |
426
+
427
+ ### Invoicing
428
+
429
+ | Command | Description |
430
+ |---------|-------------|
431
+ | `invoice <tag> [options]` | Generate invoice for a tag |
432
+
433
+ Invoice options:
434
+ - `--output`, `-o` - Output file path (default: `invoice_<tag>_<date>.html`)
435
+ - `--format`, `-f` - Output format: `html` or `pdf` (default: html)
436
+ - `--from` - Start date filter (YYYY-MM-DD)
437
+ - `--to` - End date filter (YYYY-MM-DD)
438
+
439
+ ### Importing Data
440
+
441
+ | Command | Description |
442
+ |---------|-------------|
443
+ | `import <source> <file> [options]` | Import time entries from external tools |
444
+
445
+ Import options:
446
+ - `--dry-run` - Preview import without saving
447
+ - `--yes`, `-y` - Skip confirmation prompt
448
+
449
+ ### Other
450
+
451
+ | Command | Description |
452
+ |---------|-------------|
453
+ | `setup` | Run the setup wizard to configure stundu |
454
+
455
+ ## Supported Importers
456
+
457
+ Stundu can import data from other time tracking tools:
458
+
459
+ | Source | Description |
460
+ |--------|-------------|
461
+ | `timewarrior` | [Timewarrior](https://timewarrior.net/) data files |
462
+
463
+ ## Data Storage
464
+
465
+ All data is stored in a single CSV file (`stundu_data.csv`) containing:
466
+
467
+ - Time entries with start/end timestamps and tags
468
+ - Rate configuration commands (`SET_HOURLY_RATE`, `SET_WORKDAY`, `SET_WEEK_START`, etc.)
469
+ - Personal and client information (`SET_MY_*`, `SET_CLIENT_*`)
470
+
471
+ This makes it easy to backup, version control, or transfer your time tracking data.
472
+
473
+ ## Documentation
474
+
475
+ - [Examples](docs/examples.md) - Detailed usage examples
476
+ - [Concepts](docs/concepts.md) - Tags, rate types, MathJSON expressions
477
+
478
+ ## Contributing
479
+
480
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and contribution guidelines.
481
+
482
+ ## License
483
+
484
+ MIT