Time tracking library with billing calculations
Project description
Stundu
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
billablesnap is deprecated — please installstunduinstead.
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.
Table of Contents
- Installation
- Quick Start
- Configuration
- Expression-Based Rates
- Localization
- Commands
- Supported Importers
- Data Storage
- Documentation
- Contributing
- License
Installation
There are three ways to get Stundu depending on your use case:
| Method | Best for |
|---|---|
| Snap | End users on Linux who want the CLI |
| pip | Developers who want to use the core library |
| From source | Contributing or hacking on the project |
Snap
The easiest way to install the CLI on Linux:
sudo snap install stundu
Or visit snapcraft.io/stundu.
pip
Install the core library (no CLI dependencies):
pip install stundu
Install with the CLI included:
pip install stundu[cli]
Install with PDF invoice support:
pip install stundu[cli,pdf]
The core library exposes the billing engine, data models, storage backend, and importers/exporters. The stundu command requires the [cli] extra.
From Source
Prerequisites
- Python 3.11 or higher
Setup
- Clone the repository and navigate to the project directory:
cd stundu
- Create a virtual environment:
python3 -m venv venv
- Activate the virtual environment:
# Linux/macOS
source venv/bin/activate
# Windows
venv\Scripts\activate
- Install dependencies:
pip install -r requirements.txt
Quick Start
python stundu.py start "Acme Corp" "Development"
Started tracking: Acme Corp, Development
Started at: 09:00
python stundu.py status
Currently tracking: Acme Corp, Development
Started at: 09:00
Elapsed: 2h 15m
python stundu.py stop
Stopped tracking: Acme Corp, Development
Duration: 3h 0m
python stundu.py log
@ Start End Duration Tags Status
-- ------- ------- ---------- ---------------------- --------
@1 09:00 12:00 3h 0m Acme Corp, Development
python stundu.py configure rate "Acme Corp" 50 EUR
python stundu.py report
Report for 'Acme Corp' (Jan 1, 2026 to Jan 31, 2026)
────────────────────────────────────────────────────────────
╭──────────────┬───────┬───────┬──────────┬─────────────┬────────────╮
│ Date │ Start │ End │ Duration │ Tags │ Cost │
├──────────────┼───────┼───────┼──────────┼─────────────┼────────────┤
│ Jan 15, 2026 │ 09:00 │ 12:00 │ 3h 0m │ Development │ 150.00 EUR │
│ │ 14:00 │ 18:00 │ 4h 0m │ Development │ 200.00 EUR │
│ │ 18:00 │ 20:00 │ 2h 0m │ Development │ 150.00 EUR │
├──────────────┼───────┼───────┼──────────┼─────────────┼────────────┤
│ Jan 18, 2026 │ 10:00 │ 14:00 │ 4h 0m │ Development │ 400.00 EUR │
╰──────────────┴───────┴───────┴──────────┴─────────────┴────────────╯
────────────────────────────────────────────────────────────
Total time: 13h 0m
Total:
900.00 EUR
See docs/examples.md for more detailed examples.
Configuration
First Run
On first run, Stundu displays a welcome message and prompts you to run the setup wizard:
Welcome to Stundu!
This appears to be your first run.
Run 'stundu.py setup' to configure.
Run stundu.py setup to create a configuration file interactively.
Config File
The default config file location is ~/.config/stundu/config.json:
{
"locale": "lv",
"style": "rounded_grid",
"data_file": "/path/to/my/timesheet.csv",
"formats": {
"date": "short",
"time": "short",
"datetime": "medium"
}
}
Run configure without arguments to see which config file is in use and what values are active.
Table Style
The style key controls how tables are rendered in report, log, and rates. Set it with:
python stundu.py configure style rounded_grid
Available styles (run configure style --help for the full list):
| Tier | Styles | Divider |
|---|---|---|
| ASCII | plain, simple, grid, github, psql, pipe, presto, pretty |
- |
| Light | simple_grid, rounded_grid, outline, simple_outline, rounded_outline |
─ |
| Heavy | heavy_grid, double_grid, fancy_grid, heavy_outline, double_outline, fancy_outline |
━ |
The default is simple. The Quick Start section shows an example report rendered with rounded_grid.
Date/Time Format Options
The formats section controls how dates and times are displayed. You can use:
Preset styles (locale-aware):
short- Compact format (e.g., "1/15/26" in English, "15.01.26" in German)medium- Default format (e.g., "Jan 15, 2026")long- Full month name (e.g., "January 15, 2026")full- Complete format (e.g., "Thursday, January 15, 2026")
Custom patterns (using Unicode CLDR format):
{
"formats": {
"date": "dd.MM.yyyy",
"time": "HH:mm",
"datetime": "yyyy-MM-dd HH:mm"
}
}
Environment Variables
| Variable | Description |
|---|---|
STUNDU_CONFIG |
Custom config file path |
STUNDU_DATA |
Custom data file path |
CLI Options
| Option | Description |
|---|---|
--lang, -L |
Override language (e.g., --lang=de) |
--data, -d |
Override data file path for this command |
Precedence
Data file location (highest to lowest priority):
--dataCLI optionSTUNDU_DATAenvironment variabledata_filein config file- Default:
stundu_data.csvin current directory
Config file location:
STUNDU_CONFIGenvironment variable- Default:
~/.config/stundu/config.json
Expression-Based Rates
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.
Expression-based rates are set by editing the data file directly (they are an advanced feature):
SET_HOURLY_RATE "Client" <expression> <currency>
First Hour Free, Then 50 EUR/h (Monthly Retainer)
SET_HOURLY_RATE "Acme Corp" ["If",[["Less",["Sum","[Acme Corp][0M:+1M][hours]"],1],0],50] EUR
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.
Volume Discount — Full Rate for First 20 h, Lower Rate After
SET_HOURLY_RATE "Acme Corp" ["If",[["Less",["Sum","[Acme Corp][0M:+1M][hours]"],20],80],60] EUR
First 20 hours each month: 80 EUR/h. Everything beyond: 60 EUR/h.
Loyalty Rate — Cheaper After 100 Lifetime Hours
SET_HOURLY_RATE "Acme Corp" ["If",[["Less",["Sum","[Acme Corp][:-1][hours]"],100],75],50] EUR
[:-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.
See 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.
Localization
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:
python stundu.py --lang=de status
python stundu.py --lang=es log
Date and time formatting automatically adapts to your locale using Babel:
| Locale | Date Example | Time |
|---|---|---|
| en | Jan 15, 2026 | 2:30 PM |
| de | 15.01.2026 | 14:30 |
| fr | 15 janv. 2026 | 14:30 |
| lv | 2026. gada 15. janv. | 14:30 |
Available Translations
| Code | Language |
|---|---|
| en | English |
| de | German |
| es | Spanish |
| et | Estonian |
| fr | French |
| it | Italian |
| lt | Lithuanian |
| lv | Latvian |
| pl | Polish |
| pt | Portuguese |
| uk | Ukrainian |
Commands
Time Tracking
| Command | Description |
|---|---|
start <tags...> |
Start tracking time with one or more tags |
stop |
Stop the currently active entry |
status |
Show the currently active entry |
continue |
Resume tracking with the same tags as the last entry |
cancel |
Discard the currently active entry without saving |
Viewing Entries
| Command | Description |
|---|---|
log |
Show all time entries in a table |
report |
Show billing report (defaults to current month) |
rates |
Show configured rates for all tags |
Report options:
--tag,-t- Filter by tag--month,-m- Current month (default when no filter is given)--last-month,-l- Previous month--from- Start date filter (YYYY-MM-DD)--to- End date filter (YYYY-MM-DD)
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.
Configuration
All configuration commands are grouped under the configure command:
| Command | Description |
|---|---|
configure |
Show active config sources and available subcommands |
configure rate <tag> <amount> <currency> |
Set hourly rate for a tag |
configure workday <hours> [tag] |
Set workday hours (e.g., 9:00-18:00) |
configure afterhours <tag> <multiplier> |
Set afterhours rate multiplier |
configure holiday <tag> <multiplier> |
Set holiday/weekend rate multiplier |
configure week-start <0-6> |
Set week start day for calendar expressions (0=Mon, 6=Sun) |
configure style <name> |
Set table display style |
Personal information (for invoices):
| Command | Description |
|---|---|
configure my-name <name> |
Set your name |
configure my-address <address> |
Set your address |
configure my-email <email> |
Set your email |
configure my-vat <vat> |
Set your VAT number |
configure my-bank <bank> |
Set your bank name |
configure my-bank-account <account> |
Set your bank account number |
configure my-bic <bic> |
Set your bank BIC/SWIFT code |
configure my-note <note> |
Set a note to appear on invoices |
Client information (for invoices):
| Command | Description |
|---|---|
configure client-address <tag> <address> |
Set client address |
configure client-vat <tag> <vat> |
Set client VAT number |
configure client-note <tag> <note> |
Set client-specific note |
Entry Management
| Command | Description |
|---|---|
modify <@N> [options] |
Modify an existing entry |
delete <@N or tag> |
Delete entries by reference or tag |
Invoicing
| Command | Description |
|---|---|
invoice <tag> [options] |
Generate invoice for a tag |
Invoice options:
--output,-o- Output file path (default:invoice_<tag>_<date>.html)--format,-f- Output format:htmlorpdf(default: html)--from- Start date filter (YYYY-MM-DD)--to- End date filter (YYYY-MM-DD)
Importing Data
| Command | Description |
|---|---|
import <source> <file> [options] |
Import time entries from external tools |
Import options:
--dry-run- Preview import without saving--yes,-y- Skip confirmation prompt
Other
| Command | Description |
|---|---|
setup |
Run the setup wizard to configure stundu |
Supported Importers
Stundu can import data from other time tracking tools:
| Source | Description |
|---|---|
timewarrior |
Timewarrior data files |
Data Storage
All data is stored in a single CSV file (stundu_data.csv) containing:
- Time entries with start/end timestamps and tags
- Rate configuration commands (
SET_HOURLY_RATE,SET_WORKDAY,SET_WEEK_START, etc.) - Personal and client information (
SET_MY_*,SET_CLIENT_*)
This makes it easy to backup, version control, or transfer your time tracking data.
Documentation
Contributing
See CONTRIBUTING.md for development setup, testing, and contribution guidelines.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stundu-0.3.0.tar.gz.
File metadata
- Download URL: stundu-0.3.0.tar.gz
- Upload date:
- Size: 404.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
354f5e9d09d5403675951f1c5907903cecc828167367183492e211a361a68ca3
|
|
| MD5 |
35f827d38164bf9a852b3d27b8a907a3
|
|
| BLAKE2b-256 |
1860de1d0f062835d4a95d89bbd6f74e71eff8f47118f26c7803143e13aec7e6
|
File details
Details for the file stundu-0.3.0-py3-none-any.whl.
File metadata
- Download URL: stundu-0.3.0-py3-none-any.whl
- Upload date:
- Size: 80.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f51a65bf0f933c8f9f1711fb911b44b276918280c2049238b56be296da9b901
|
|
| MD5 |
38166be0232891ea59b473cff5e858c1
|
|
| BLAKE2b-256 |
277cde636f5d802b5ee814929435bba25fdc06e6ec001e958816faf9d69966e4
|