clockify-export 0.2.0__tar.gz → 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 (31) hide show
  1. clockify_export-0.3.0/.vscode/settings.json +5 -0
  2. {clockify_export-0.2.0 → clockify_export-0.3.0}/CHANGELOG.md +12 -0
  3. {clockify_export-0.2.0 → clockify_export-0.3.0}/PKG-INFO +107 -18
  4. {clockify_export-0.2.0 → clockify_export-0.3.0}/PLAN.md +6 -0
  5. {clockify_export-0.2.0 → clockify_export-0.3.0}/README.md +106 -17
  6. {clockify_export-0.2.0 → clockify_export-0.3.0}/mise.toml +1 -1
  7. {clockify_export-0.2.0 → clockify_export-0.3.0}/pyproject.toml +5 -1
  8. {clockify_export-0.2.0 → clockify_export-0.3.0}/src/clockify_export/cli.py +50 -7
  9. {clockify_export-0.2.0 → clockify_export-0.3.0}/src/clockify_export/config.py +27 -4
  10. clockify_export-0.3.0/src/clockify_export/systemd/clockify-export.timer +10 -0
  11. clockify_export-0.3.0/src/clockify_export/systemd_install.py +30 -0
  12. clockify_export-0.3.0/systemd/clockify-export.service +8 -0
  13. {clockify_export-0.2.0 → clockify_export-0.3.0}/tests/test_cli.py +28 -4
  14. {clockify_export-0.2.0 → clockify_export-0.3.0}/tests/test_config.py +20 -0
  15. {clockify_export-0.2.0 → clockify_export-0.3.0}/uv.lock +2 -2
  16. {clockify_export-0.2.0 → clockify_export-0.3.0}/.env.example +0 -0
  17. {clockify_export-0.2.0 → clockify_export-0.3.0}/.github/workflows/publish.yml +0 -0
  18. {clockify_export-0.2.0 → clockify_export-0.3.0}/.gitignore +0 -0
  19. {clockify_export-0.2.0 → clockify_export-0.3.0}/AGENTS.md +0 -0
  20. {clockify_export-0.2.0 → clockify_export-0.3.0}/LICENSE +0 -0
  21. {clockify_export-0.2.0 → clockify_export-0.3.0}/mise-tasks/bump-version +0 -0
  22. {clockify_export-0.2.0 → clockify_export-0.3.0}/src/clockify_export/__init__.py +0 -0
  23. {clockify_export-0.2.0 → clockify_export-0.3.0}/src/clockify_export/api.py +0 -0
  24. {clockify_export-0.2.0 → clockify_export-0.3.0}/src/clockify_export/cache.py +0 -0
  25. {clockify_export-0.2.0 → clockify_export-0.3.0}/src/clockify_export/export.py +0 -0
  26. {clockify_export-0.2.0 → clockify_export-0.3.0/src/clockify_export}/systemd/clockify-export.service +0 -0
  27. {clockify_export-0.2.0 → clockify_export-0.3.0}/systemd/clockify-export.timer +0 -0
  28. {clockify_export-0.2.0 → clockify_export-0.3.0}/tests/conftest.py +0 -0
  29. {clockify_export-0.2.0 → clockify_export-0.3.0}/tests/test_api.py +0 -0
  30. {clockify_export-0.2.0 → clockify_export-0.3.0}/tests/test_cache.py +0 -0
  31. {clockify_export-0.2.0 → clockify_export-0.3.0}/tests/test_export.py +0 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "chat.tools.terminal.autoApprove": {
3
+ "uv": true
4
+ }
5
+ }
@@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [0.2.0]
11
11
 
12
+ ### Added
13
+ - CLI config help now documents supported config entries and example values (`--config-help`).
14
+ - Workspace subset exports can be configured via `workspaces = [...]` in config or repeated `-w/--workspace` flags.
15
+ - `clockify-export --install-systemd` and `mise install-systemd` installs the user-level systemd service and timer for daily exports.
16
+ - `--status` now shows workspace names with IDs and marks stale state entries that are missing from the API.
17
+
18
+ ### Changed
19
+ - Export state now stores each workspace name alongside the last exported timestamp so deleted or renamed workspaces remain identifiable.
20
+ - Updated project plan status: workspace subset workflow, config help, and user systemd installation are complete and documented.
21
+
22
+ ## [0.2.0]
23
+
12
24
  ### Added
13
25
  - Auto-refresh cache when unknown project/client/tag ID encountered during entry processing
14
26
  - Cache for workspaces, projects, clients, tags with 1-week expiry
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: clockify-export
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Export Clockify time entries to CSV with incremental support
5
5
  Project-URL: Homepage, https://github.com/ptandler/clockify-export
6
6
  Project-URL: Repository, https://github.com/ptandler/clockify-export
@@ -47,9 +47,19 @@ and writes one CSV per calendar month.
47
47
 
48
48
  ## Installation
49
49
 
50
- Requires [uv](https://docs.astral.sh/uv/) (or any Python 3.11+ + pip).
50
+ Requires Python 3.11+ and either [mise](https://mise.jdx.dev/) or [uv](https://docs.astral.sh/uv/) for tool installation.
51
51
 
52
- ### Global install (recommended, adds `clockify-export` to your PATH)
52
+ ### Install with mise
53
+
54
+ If you already use `mise`, it supports pipx as backend and can use uv if installed.
55
+ The simplest install path is:
56
+
57
+ ```bash
58
+ mise use -g uv
59
+ mise use -g pipx:clockify-export
60
+ ```
61
+
62
+ ### Install directly with uv
53
63
 
54
64
  ```bash
55
65
  uv tool install clockify-export # from PyPI
@@ -67,33 +77,40 @@ uv sync
67
77
 
68
78
  ## Setup
69
79
 
70
- ### 1. API key
80
+ ### 1. Create and edit the config file
71
81
 
72
82
  Generate an API key in Clockify: **Profile → Preferences → API → Manage API Keys → Generate**.
73
-
74
- Provide it via one of (highest priority first):
83
+ Then create the config file and set your key and any defaults you want to use:
75
84
 
76
85
  ```bash
77
- # a) Config file (recommended)
78
86
  clockify-export --init-config
79
87
  # then edit ~/.config/clockify-export/config.toml:
80
88
  #
81
- # # Clockify export configuration
82
89
  # api_key = "YOUR_KEY"
83
90
  # export_dir = "export"
91
+ # refetch = 1
92
+ # workspaces = ["Primary Workspace", "Client Projects"]
93
+ ```
94
+
95
+ You can also print the supported keys anytime:
96
+
97
+ ```bash
98
+ clockify-export --config-help
99
+ ```
100
+
101
+ Alternatively, provide the key via one of the other supported sources (highest priority first):
84
102
 
85
- # b) Environment variable
103
+ ```bash
104
+ # Environment variable
86
105
  export CLOCKIFY_API_KEY=YOUR_KEY
87
106
 
88
- # c) .env file in the project directory
107
+ # .env file in the project directory
89
108
  echo "CLOCKIFY_API_KEY=YOUR_KEY" > .env
90
109
 
91
- # d) Command line flag
110
+ # Command line flag
92
111
  clockify-export --api-key YOUR_KEY
93
112
  ```
94
113
 
95
- > Note: TOML is parsed with Python's stdlib `tomllib` — no extra dependency.
96
-
97
114
  ### 2. Export directory
98
115
 
99
116
  Defaults to `./export`. Override via config file or `-o` flag:
@@ -102,7 +119,71 @@ Defaults to `./export`. Override via config file or `-o` flag:
102
119
  clockify-export -o /data/clockify
103
120
  ```
104
121
 
105
- ## Usage
122
+ ### 3. Check the setup and workspace status
123
+
124
+ Before enabling the daily timer, confirm that the config and workspace discovery look correct:
125
+
126
+ ```bash
127
+ clockify-export --status
128
+ ```
129
+
130
+ This shows the known workspaces and any saved export state, which helps catch missing or renamed workspaces early.
131
+
132
+ ### 4. Install daily systemd timer
133
+
134
+ If you installed via PyPI, you can install the user-level systemd service and timer without needing the repo checkout:
135
+
136
+ ```bash
137
+ clockify-export --install-systemd
138
+ ```
139
+
140
+ This copies the bundled service/timer into `~/.config/systemd/user/` and enables the timer.
141
+
142
+ See below for other options.
143
+
144
+ ## Command-line options
145
+
146
+ ```bash
147
+ clockify-export --help
148
+ ```
149
+
150
+ The CLI supports the following options:
151
+
152
+ - `-o, --output-dir` — output directory for CSV files; defaults to `./export` or the config value
153
+ - `--api-key` — override the Clockify API key for this run
154
+ - `--full` — export everything from the beginning and ignore incremental state
155
+ - `--from YYYY-MM[,YYYY-MM-DD]` — export from a specific month/day; always starts at the beginning of the month
156
+ - `--last N` — export the last N complete months
157
+ - `--refetch N` — re-fetch N months back from the last exported timestamp
158
+ - `-w, --workspace NAME` — export only matching workspace names; repeat to select multiple workspaces
159
+ - `--status` — show workspace information plus stored export state; includes missing workspaces by ID if the API no longer returns them
160
+ - `--config-help` — print supported config keys and example values
161
+ - `--init-config` — create a default config file in `~/.config/clockify-export/config.toml`
162
+ - `--install-systemd` — install the user-level systemd service and timer for daily exports
163
+ - `--no-cache` — skip cached projects/clients/tags and fetch fresh data
164
+ - `--version` — print the installed version
165
+
166
+ ## Config file
167
+
168
+ The config file is read from `~/.config/clockify-export/config.toml`.
169
+
170
+ Supported keys:
171
+
172
+ ```toml
173
+ api_key = "YOUR_KEY"
174
+ export_dir = "export"
175
+ refetch = 1
176
+ workspaces = ["Primary Workspace", "Client Projects"]
177
+ ```
178
+
179
+ Priority order is:
180
+
181
+ 1. CLI flags
182
+ 2. environment variables (`CLOCKIFY_API_KEY`, `CLOCKIFY_EXPORT_DIR`)
183
+ 3. config file
184
+ 4. defaults
185
+
186
+ ### Example usage
106
187
 
107
188
  ```bash
108
189
  # Full export of all workspaces (everything since the beginning)
@@ -111,13 +192,17 @@ clockify-export --full
111
192
  # Incremental export (default — only fetches entries after last run)
112
193
  clockify-export
113
194
 
114
- # Export only specific workspace(s)
115
- clockify-export -w "Peter's workspace"
195
+ # Export only specific workspace(s) by name
196
+ clockify-export -w "Primary Workspace"
197
+ clockify-export -w "Primary Workspace" -w "Client Projects"
198
+
199
+ # Or in config.toml:
200
+ # workspaces = ["Primary Workspace", "Client Projects"]
116
201
 
117
202
  # Export since a specific date (ignores incremental state)
118
203
  clockify-export --from 2026-01-01
119
204
 
120
- # Show export state
205
+ # Show workspace status and saved export state
121
206
  clockify-export --status
122
207
  ```
123
208
 
@@ -134,7 +219,7 @@ Each filename carries the workspace and year, so files stay identifiable even
134
219
  if you copy or move them out of the directory tree:
135
220
 
136
221
  ```
137
- export/etalytics workspace/2026/etalytics workspace_2026-09.csv
222
+ export/Example Workspace/2026/Example Workspace_2026-09.csv
138
223
  ```
139
224
 
140
225
  ### CSV columns
@@ -246,3 +331,7 @@ systemctl --user disable --now clockify-export.timer
246
331
 
247
332
  > Note: The service uses `Type=oneshot` so it completes and exits each day.
248
333
  > Logs can be viewed with `journalctl --user -u clockify-export.service`.
334
+
335
+ ## Thanks
336
+
337
+ This whole project was generated via opencode and I tried several free models. 🙏
@@ -19,6 +19,12 @@
19
19
 
20
20
  - [ ] Auto-refresh cache when unknown project/client/tag ID encountered during entry processing
21
21
 
22
+ ## Next
23
+
24
+ - [x] show config help, i.e. supported config entries
25
+ - [x] add command to install systemd unit & timer as user
26
+ - [x] Workspace subset workflow: use workspace names in config and CLI, and `--status` now lists available workspaces alongside saved state, including missing workspaces by ID.
27
+
22
28
  ---
23
29
 
24
30
  ## 📋 Backlog
@@ -21,9 +21,19 @@ and writes one CSV per calendar month.
21
21
 
22
22
  ## Installation
23
23
 
24
- Requires [uv](https://docs.astral.sh/uv/) (or any Python 3.11+ + pip).
24
+ Requires Python 3.11+ and either [mise](https://mise.jdx.dev/) or [uv](https://docs.astral.sh/uv/) for tool installation.
25
25
 
26
- ### Global install (recommended, adds `clockify-export` to your PATH)
26
+ ### Install with mise
27
+
28
+ If you already use `mise`, it supports pipx as backend and can use uv if installed.
29
+ The simplest install path is:
30
+
31
+ ```bash
32
+ mise use -g uv
33
+ mise use -g pipx:clockify-export
34
+ ```
35
+
36
+ ### Install directly with uv
27
37
 
28
38
  ```bash
29
39
  uv tool install clockify-export # from PyPI
@@ -41,33 +51,40 @@ uv sync
41
51
 
42
52
  ## Setup
43
53
 
44
- ### 1. API key
54
+ ### 1. Create and edit the config file
45
55
 
46
56
  Generate an API key in Clockify: **Profile → Preferences → API → Manage API Keys → Generate**.
47
-
48
- Provide it via one of (highest priority first):
57
+ Then create the config file and set your key and any defaults you want to use:
49
58
 
50
59
  ```bash
51
- # a) Config file (recommended)
52
60
  clockify-export --init-config
53
61
  # then edit ~/.config/clockify-export/config.toml:
54
62
  #
55
- # # Clockify export configuration
56
63
  # api_key = "YOUR_KEY"
57
64
  # export_dir = "export"
65
+ # refetch = 1
66
+ # workspaces = ["Primary Workspace", "Client Projects"]
67
+ ```
68
+
69
+ You can also print the supported keys anytime:
70
+
71
+ ```bash
72
+ clockify-export --config-help
73
+ ```
74
+
75
+ Alternatively, provide the key via one of the other supported sources (highest priority first):
58
76
 
59
- # b) Environment variable
77
+ ```bash
78
+ # Environment variable
60
79
  export CLOCKIFY_API_KEY=YOUR_KEY
61
80
 
62
- # c) .env file in the project directory
81
+ # .env file in the project directory
63
82
  echo "CLOCKIFY_API_KEY=YOUR_KEY" > .env
64
83
 
65
- # d) Command line flag
84
+ # Command line flag
66
85
  clockify-export --api-key YOUR_KEY
67
86
  ```
68
87
 
69
- > Note: TOML is parsed with Python's stdlib `tomllib` — no extra dependency.
70
-
71
88
  ### 2. Export directory
72
89
 
73
90
  Defaults to `./export`. Override via config file or `-o` flag:
@@ -76,7 +93,71 @@ Defaults to `./export`. Override via config file or `-o` flag:
76
93
  clockify-export -o /data/clockify
77
94
  ```
78
95
 
79
- ## Usage
96
+ ### 3. Check the setup and workspace status
97
+
98
+ Before enabling the daily timer, confirm that the config and workspace discovery look correct:
99
+
100
+ ```bash
101
+ clockify-export --status
102
+ ```
103
+
104
+ This shows the known workspaces and any saved export state, which helps catch missing or renamed workspaces early.
105
+
106
+ ### 4. Install daily systemd timer
107
+
108
+ If you installed via PyPI, you can install the user-level systemd service and timer without needing the repo checkout:
109
+
110
+ ```bash
111
+ clockify-export --install-systemd
112
+ ```
113
+
114
+ This copies the bundled service/timer into `~/.config/systemd/user/` and enables the timer.
115
+
116
+ See below for other options.
117
+
118
+ ## Command-line options
119
+
120
+ ```bash
121
+ clockify-export --help
122
+ ```
123
+
124
+ The CLI supports the following options:
125
+
126
+ - `-o, --output-dir` — output directory for CSV files; defaults to `./export` or the config value
127
+ - `--api-key` — override the Clockify API key for this run
128
+ - `--full` — export everything from the beginning and ignore incremental state
129
+ - `--from YYYY-MM[,YYYY-MM-DD]` — export from a specific month/day; always starts at the beginning of the month
130
+ - `--last N` — export the last N complete months
131
+ - `--refetch N` — re-fetch N months back from the last exported timestamp
132
+ - `-w, --workspace NAME` — export only matching workspace names; repeat to select multiple workspaces
133
+ - `--status` — show workspace information plus stored export state; includes missing workspaces by ID if the API no longer returns them
134
+ - `--config-help` — print supported config keys and example values
135
+ - `--init-config` — create a default config file in `~/.config/clockify-export/config.toml`
136
+ - `--install-systemd` — install the user-level systemd service and timer for daily exports
137
+ - `--no-cache` — skip cached projects/clients/tags and fetch fresh data
138
+ - `--version` — print the installed version
139
+
140
+ ## Config file
141
+
142
+ The config file is read from `~/.config/clockify-export/config.toml`.
143
+
144
+ Supported keys:
145
+
146
+ ```toml
147
+ api_key = "YOUR_KEY"
148
+ export_dir = "export"
149
+ refetch = 1
150
+ workspaces = ["Primary Workspace", "Client Projects"]
151
+ ```
152
+
153
+ Priority order is:
154
+
155
+ 1. CLI flags
156
+ 2. environment variables (`CLOCKIFY_API_KEY`, `CLOCKIFY_EXPORT_DIR`)
157
+ 3. config file
158
+ 4. defaults
159
+
160
+ ### Example usage
80
161
 
81
162
  ```bash
82
163
  # Full export of all workspaces (everything since the beginning)
@@ -85,13 +166,17 @@ clockify-export --full
85
166
  # Incremental export (default — only fetches entries after last run)
86
167
  clockify-export
87
168
 
88
- # Export only specific workspace(s)
89
- clockify-export -w "Peter's workspace"
169
+ # Export only specific workspace(s) by name
170
+ clockify-export -w "Primary Workspace"
171
+ clockify-export -w "Primary Workspace" -w "Client Projects"
172
+
173
+ # Or in config.toml:
174
+ # workspaces = ["Primary Workspace", "Client Projects"]
90
175
 
91
176
  # Export since a specific date (ignores incremental state)
92
177
  clockify-export --from 2026-01-01
93
178
 
94
- # Show export state
179
+ # Show workspace status and saved export state
95
180
  clockify-export --status
96
181
  ```
97
182
 
@@ -108,7 +193,7 @@ Each filename carries the workspace and year, so files stay identifiable even
108
193
  if you copy or move them out of the directory tree:
109
194
 
110
195
  ```
111
- export/etalytics workspace/2026/etalytics workspace_2026-09.csv
196
+ export/Example Workspace/2026/Example Workspace_2026-09.csv
112
197
  ```
113
198
 
114
199
  ### CSV columns
@@ -220,3 +305,7 @@ systemctl --user disable --now clockify-export.timer
220
305
 
221
306
  > Note: The service uses `Type=oneshot` so it completes and exits each day.
222
307
  > Logs can be viewed with `journalctl --user -u clockify-export.service`.
308
+
309
+ ## Thanks
310
+
311
+ This whole project was generated via opencode and I tried several free models. 🙏
@@ -12,4 +12,4 @@ export-full = "uv run clockify-export --full --output-dir ./export"
12
12
  status = "uv run clockify-export --status"
13
13
  init-config = "uv run clockify-export --init-config"
14
14
  test = "uv run python -c \"from clockify_export.api import ClockifyAPI; print('import ok')\""
15
- install-systemd = "cp systemd/clockify-export.service \"$HOME/.config/systemd/user/\" && cp systemd/clockify-export.timer \"$HOME/.config/systemd/user/\" && systemctl --user daemon-reload && systemctl --user enable --now clockify-export.timer"
15
+ install-systemd = "cp systemd/clockify-export/clockify-export.service \"$HOME/.config/systemd/user/\" && cp systemd/clockify-export.timer \"$HOME/.config/systemd/user/\" && systemctl --user daemon-reload && systemctl --user enable --now clockify-export.timer"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clockify-export"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Export Clockify time entries to CSV with incremental support"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -40,6 +40,10 @@ build-backend = "hatchling.build"
40
40
 
41
41
  [tool.hatch.build.targets.wheel]
42
42
  packages = ["src/clockify_export"]
43
+ include = [
44
+ "src/clockify_export/systemd/*.service",
45
+ "src/clockify_export/systemd/*.timer",
46
+ ]
43
47
 
44
48
  [tool.uv]
45
49
  package = true
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta, timezone
9
9
  from .api import ClockifyAPI, ClockifyError
10
10
  from .config import CONFIG_FILE, Config, CONFIG_HELP, create_config_file, get_last_exported, set_last_exported, parse_from_date, calculate_last_months, calculate_refetch_date
11
11
  from .export import export_workspace
12
+ from .systemd_install import install_systemd_user_units
12
13
  from . import __version__
13
14
 
14
15
 
@@ -74,11 +75,21 @@ def main() -> None:
74
75
  action="store_true",
75
76
  help="Show export state and exit",
76
77
  )
78
+ parser.add_argument(
79
+ "--config-help",
80
+ action="store_true",
81
+ help="Print supported configuration entries and exit",
82
+ )
77
83
  parser.add_argument(
78
84
  "--init-config",
79
85
  action="store_true",
80
86
  help="Create ~/.config/clockify-export/config.toml and exit",
81
87
  )
88
+ parser.add_argument(
89
+ "--install-systemd",
90
+ action="store_true",
91
+ help="Install the user-level systemd service and timer for daily exports",
92
+ )
82
93
  parser.add_argument(
83
94
  "--no-cache",
84
95
  action="store_true",
@@ -86,12 +97,20 @@ def main() -> None:
86
97
  )
87
98
  args = parser.parse_args()
88
99
 
100
+ if args.config_help:
101
+ print(CONFIG_HELP)
102
+ raise SystemExit(0)
103
+
89
104
  if args.init_config:
90
105
  path = create_config_file()
91
106
  print(f"Created config file: {path}")
92
107
  print("Edit it to set your API key and export directory.")
93
108
  return
94
109
 
110
+ if args.install_systemd:
111
+ install_systemd_user_units()
112
+ return
113
+
95
114
  # Load configuration
96
115
  cfg = Config.load(args)
97
116
 
@@ -112,17 +131,41 @@ def main() -> None:
112
131
 
113
132
  # Status mode
114
133
  if args.status:
115
- import json
116
134
  from .config import get_state
117
135
  state = get_state()
118
136
  print(f"Export directory: {cfg.export_dir}")
119
137
  print(f"Config file: {CONFIG_FILE}")
120
- if not state:
121
- print("No export state found.")
138
+ try:
139
+ workspaces = api.get_workspaces()
140
+ except ClockifyError as e:
141
+ print(f"Error fetching workspaces: {e}", file=sys.stderr)
142
+ workspaces = []
143
+
144
+ available_by_id = {str(ws.get("id")): ws for ws in workspaces if ws.get("id")}
145
+ workspace_ids = sorted(set(available_by_id) | set(state), key=lambda ws_id: (
146
+ (available_by_id.get(ws_id, {}).get("name") or state.get(ws_id, {}).get("name") or "").lower(),
147
+ ws_id,
148
+ ))
149
+
150
+ print("Workspace status:")
151
+ if not workspace_ids:
152
+ print(" No workspaces found.")
122
153
  return
123
- print("Export state:")
124
- for ws_id, info in state.items():
125
- print(f" {ws_id}: last_exported={info.get('last_exported', 'never')}")
154
+
155
+ for ws_id in workspace_ids:
156
+ info = state.get(ws_id, {})
157
+ api_ws = available_by_id.get(ws_id, {})
158
+ name = api_ws.get("name") or info.get("name") or "(unknown workspace)"
159
+ if ws_id in available_by_id:
160
+ status = ""
161
+ else:
162
+ status = " - missing from API"
163
+ last_exported = info.get("last_exported")
164
+ if last_exported:
165
+ detail = f" - last_exported={last_exported}"
166
+ else:
167
+ detail = " - no export state"
168
+ print(f" {name} ({ws_id}){status}{detail}")
126
169
  return
127
170
 
128
171
  # Discover workspaces
@@ -193,7 +236,7 @@ def main() -> None:
193
236
  if counts:
194
237
  # Update state to the actual latest entry date (not end of month)
195
238
  if latest_entry_dt:
196
- set_last_exported(ws_id, latest_entry_dt.isoformat())
239
+ set_last_exported(ws_id, latest_entry_dt.isoformat(), ws_name)
197
240
  total = sum(counts.values())
198
241
  print(f" Total: {total} entries across {len(counts)} month(s)")
199
242
  else:
@@ -26,6 +26,7 @@ DEFAULTS = {
26
26
  "api_key": None,
27
27
  "export_dir": "export",
28
28
  "refetch": 1,
29
+ "workspaces": [],
29
30
  }
30
31
 
31
32
  CONFIG_HELP = """
@@ -33,12 +34,14 @@ Supported configuration options (config.toml):
33
34
  api_key - Clockify API key (required)
34
35
  export_dir - Output directory for CSV files (default: "export")
35
36
  refetch - Number of months to re-fetch from last exported date (default: 1)
37
+ workspaces - Optional list of workspace names to export (e.g. ["Workspace One", "Workspace Two"])
36
38
 
37
39
  Example config.toml:
38
40
  # Clockify export configuration
39
41
  api_key = "YOUR_API_KEY"
40
42
  export_dir = "export"
41
43
  refetch = 1
44
+ workspaces = ["Workspace One", "Workspace Two"]
42
45
  """
43
46
 
44
47
 
@@ -74,9 +77,20 @@ class Config:
74
77
  )
75
78
 
76
79
  full = getattr(args, "full", False) if args else False
77
- workspaces = getattr(args, "workspace", []) if args else []
80
+ cli_workspaces = getattr(args, "workspace", None) if args else None
81
+ if cli_workspaces is None:
82
+ cli_workspaces = []
83
+ if isinstance(cli_workspaces, str):
84
+ workspaces = [cli_workspaces]
85
+ else:
86
+ workspaces = list(cli_workspaces)
87
+ if not workspaces:
88
+ file_workspaces = file_cfg.get("workspaces", DEFAULTS["workspaces"])
89
+ if isinstance(file_workspaces, str):
90
+ file_workspaces = [file_workspaces]
91
+ workspaces = list(file_workspaces)
78
92
  from_date = getattr(args, "from_date", None) if args else None
79
-
93
+
80
94
  # For refetch: CLI arg wins, then config file, then default
81
95
  if args and getattr(args, "refetch", None) is not None:
82
96
  refetch = args.refetch
@@ -135,6 +149,8 @@ def create_config_file(api_key: str = "", export_dir: str = "export") -> Path:
135
149
  f'export_dir = "{export_dir}"\n'
136
150
  "# Number of months to re-fetch from last exported date (default: 1)\n"
137
151
  "refetch = 1\n"
152
+ "# Optional: export only specific workspace names\n"
153
+ "# workspaces = [\"Workspace One\", \"Workspace Two\"]\n"
138
154
  )
139
155
  return CONFIG_FILE
140
156
 
@@ -194,9 +210,16 @@ def get_last_exported(workspace_id: str) -> str | None:
194
210
  return state.get(workspace_id, {}).get("last_exported")
195
211
 
196
212
 
197
- def set_last_exported(workspace_id: str, timestamp: str) -> None:
213
+ def set_last_exported(workspace_id: str, timestamp: str, workspace_name: str | None = None) -> None:
198
214
  state = get_state()
199
215
  ws = state.setdefault(workspace_id, {})
200
216
  ws["last_exported"] = timestamp
217
+ if workspace_name is not None:
218
+ ws["name"] = workspace_name
201
219
  ws["updated_at"] = datetime.now(timezone.utc).isoformat()
202
- save_state(state)
220
+ save_state(state)
221
+
222
+
223
+ def get_workspace_name(workspace_id: str) -> str | None:
224
+ state = get_state()
225
+ return state.get(workspace_id, {}).get("name")
@@ -0,0 +1,10 @@
1
+ [Unit]
2
+ Description=Run Clockify Export daily
3
+
4
+ [Timer]
5
+ OnBootSec=15min
6
+ OnUnitActiveSec=24h
7
+ Persistent=true
8
+
9
+ [Install]
10
+ WantedBy=timers.target
@@ -0,0 +1,30 @@
1
+ """Helpers for installing the daily systemd timer for clockify-export."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def install_systemd_user_units() -> None:
11
+ """Install the user systemd timer and service for daily exports."""
12
+ target_dir = Path.home() / ".config" / "systemd" / "user"
13
+ target_dir.mkdir(parents=True, exist_ok=True)
14
+
15
+ bundled_dir = Path(__file__).resolve().parent / "systemd"
16
+ for target_name in ("clockify-export.service", "clockify-export.timer"):
17
+ source = bundled_dir / target_name
18
+ (target_dir / target_name).write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
19
+
20
+ for command in (
21
+ ["systemctl", "--user", "daemon-reload"],
22
+ ["systemctl", "--user", "enable", "--now", "clockify-export.timer"],
23
+ ):
24
+ try:
25
+ subprocess.run(command, check=True, capture_output=True, text=True)
26
+ except (FileNotFoundError, subprocess.CalledProcessError) as exc:
27
+ print(f"Warning: unable to run {' '.join(command)}: {exc}", file=sys.stderr)
28
+
29
+ print(f"Installed systemd user units in {target_dir}")
30
+ print("Use: systemctl --user status clockify-export.timer")
@@ -0,0 +1,8 @@
1
+ [Unit]
2
+ Description=Clockify Export - Daily export of time entries to CSV
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=oneshot
7
+ ExecStart=%h/.local/bin/clockify-export
8
+ WorkingDirectory=%h
@@ -64,6 +64,25 @@ class TestCLI:
64
64
  assert "api_key" in content
65
65
  assert "export_dir" in content
66
66
 
67
+ def test_config_help_flag(self, capsys):
68
+ with pytest.raises(SystemExit) as exc:
69
+ run_main(["--config-help"])
70
+ assert exc.value.code == 0
71
+ captured = capsys.readouterr()
72
+ assert "api_key" in captured.out
73
+ assert "refetch" in captured.out
74
+
75
+ def test_install_systemd_flag(self, tmp_path):
76
+ with isolated_env(tmp_path):
77
+ expected_dir = tmp_path / ".config" / "systemd" / "user"
78
+ with patch("clockify_export.systemd_install.subprocess.run") as mock_run:
79
+ run_main(["--install-systemd"])
80
+ assert mock_run.call_count >= 2
81
+ assert any(call.args[0][:3] == ["systemctl", "--user", "daemon-reload"] for call in mock_run.call_args_list)
82
+ installed_files = list(expected_dir.glob("*"))
83
+ assert any(f.name == "clockify-export.service" for f in installed_files)
84
+ assert any(f.name == "clockify-export.timer" for f in installed_files)
85
+
67
86
  def test_missing_api_key_exits(self, capsys, tmp_path):
68
87
  with isolated_env(tmp_path) as config_dir:
69
88
  with patch("clockify_export.config.CONFIG_DIR", config_dir):
@@ -92,20 +111,25 @@ class TestCLI:
92
111
  def test_status_flag_shows_state(self, capsys, tmp_path, sample_state):
93
112
  with isolated_env(tmp_path) as config_dir:
94
113
  (config_dir / "config.toml").write_text('api_key = "test_key"\nexport_dir = "export"\n')
95
- (config_dir / "state.json").write_text('{"ws123": {"last_exported": "2026-01-15T17:00:00+00:00"}}')
114
+ (config_dir / "state.json").write_text('{"ws123": {"last_exported": "2026-01-15T17:00:00+00:00", "name": "Old Workspace"}}')
96
115
  with patch("clockify_export.config.CONFIG_DIR", config_dir):
97
116
  with patch("clockify_export.config.CONFIG_FILE", config_dir / "config.toml"):
98
117
  with patch("clockify_export.config.STATE_FILE", config_dir / "state.json"):
99
118
  with patch("clockify_export.cli.ClockifyAPI") as mock_api_class:
100
119
  mock_api = MagicMock()
101
- mock_api.get_workspaces.return_value = [{"id": "ws1", "name": "WS"}]
120
+ mock_api.get_workspaces.return_value = [{"id": "ws1", "name": "WS"}, {"id": "ws2", "name": "Other Workspace"}]
102
121
  mock_api_class.return_value = mock_api
103
122
 
104
123
  run_main(["--status"])
105
124
  captured = capsys.readouterr()
106
125
  assert "Export directory:" in captured.out
107
- assert "Export state:" in captured.out
108
- assert "ws123" in captured.out
126
+ assert "Workspace status:" in captured.out
127
+ assert "WS (ws1)" in captured.out
128
+ assert "Other Workspace (ws2)" in captured.out
129
+ assert "Old Workspace (ws123)" in captured.out
130
+ assert "available" not in captured.out.lower()
131
+ assert "missing from API" in captured.out
132
+ assert "last_exported" in captured.out
109
133
 
110
134
  def test_workspace_filtering(self, tmp_path):
111
135
  with isolated_env(tmp_path) as config_dir:
@@ -11,7 +11,9 @@ from clockify_export.config import (
11
11
  Config,
12
12
  calculate_last_months,
13
13
  calculate_refetch_date,
14
+ get_state,
14
15
  parse_from_date,
16
+ set_last_exported,
15
17
  )
16
18
 
17
19
 
@@ -172,6 +174,15 @@ class TestConfigLoad:
172
174
  cfg = Config.load(args)
173
175
  assert cfg.workspaces == ["WS1", "WS2"]
174
176
 
177
+ def test_config_file_workspaces(self, monkeypatch, temp_config_dir):
178
+ import clockify_export.config as config_module
179
+ config_module.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
180
+ config_module.CONFIG_FILE.write_text('api_key = "file_key"\nworkspaces = ["WS1", "WS2"]\n')
181
+ monkeypatch.delenv("CLOCKIFY_API_KEY", raising=False)
182
+ args = type("Args", (), {"api_key": None, "output_dir": None, "full": False, "workspace": None, "from_date": None, "refetch": None})()
183
+ cfg = Config.load(args)
184
+ assert cfg.workspaces == ["WS1", "WS2"]
185
+
175
186
  def test_cli_full(self, monkeypatch, temp_config_dir):
176
187
  monkeypatch.setenv("CLOCKIFY_API_KEY", "test_key")
177
188
  args = type("Args", (), {"api_key": None, "output_dir": None, "full": True, "workspace": [], "from_date": None, "refetch": None})()
@@ -190,6 +201,15 @@ class TestConfigLoad:
190
201
  cfg = Config.load(args)
191
202
  assert cfg.refetch == 5
192
203
 
204
+ def test_set_last_exported_keeps_workspace_name(self, monkeypatch, tmp_path):
205
+ import clockify_export.config as config_module
206
+ config_module.CONFIG_DIR = tmp_path / ".config" / "clockify-export"
207
+ config_module.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
208
+ config_module.STATE_FILE = config_module.CONFIG_DIR / "state.json"
209
+ set_last_exported("ws123", "2026-01-15T17:00:00+00:00", "Old Workspace")
210
+ state = get_state()
211
+ assert state["ws123"]["name"] == "Old Workspace"
212
+
193
213
  def test_dotenv_fallback(self, monkeypatch, tmp_path):
194
214
  import clockify_export.config as config_module
195
215
  env_file = tmp_path / ".env"
@@ -1,10 +1,10 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 5
3
3
  requires-python = ">=3.11"
4
4
 
5
5
  [[package]]
6
6
  name = "clockify-export"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  source = { editable = "." }
9
9
 
10
10
  [package.optional-dependencies]
File without changes