jira-time-tracker 0.1.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.
- jira_time_tracker-0.1.0/LICENSE +21 -0
- jira_time_tracker-0.1.0/PKG-INFO +212 -0
- jira_time_tracker-0.1.0/README.md +177 -0
- jira_time_tracker-0.1.0/pyproject.toml +156 -0
- jira_time_tracker-0.1.0/pyproject.toml.orig +110 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/__init__.py +7 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/__main__.py +3 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/cli.py +1186 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/client.py +256 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/config.py +163 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/credentials.py +126 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/export.py +158 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/period.py +149 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/py.typed +0 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/render.py +413 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/tracking.py +351 -0
- jira_time_tracker-0.1.0/src/jira_time_tracker/units.py +89 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 codeonym-oss
|
|
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,
|
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
18
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
19
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
20
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
21
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jira-time-tracker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Track how much estimated work (story points, hours, days…) came into being in a Jira project over a period, per issue and per person.
|
|
5
|
+
Keywords: jira,atlassian,story-points,estimates,time-tracking,cli
|
|
6
|
+
Author: codeonym-oss
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Office/Business
|
|
22
|
+
Classifier: Topic :: Software Development :: Bug Tracking
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Dist: keyring>=24
|
|
25
|
+
Requires-Dist: platformdirs>=4
|
|
26
|
+
Requires-Dist: requests>=2.31
|
|
27
|
+
Requires-Dist: rich>=13
|
|
28
|
+
Requires-Dist: typer>=0.12
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Project-URL: Homepage, https://github.com/codeonym-oss/jira-time-tracker
|
|
31
|
+
Project-URL: Repository, https://github.com/codeonym-oss/jira-time-tracker
|
|
32
|
+
Project-URL: Issues, https://github.com/codeonym-oss/jira-time-tracker/issues
|
|
33
|
+
Project-URL: Changelog, https://github.com/codeonym-oss/jira-time-tracker/blob/main/CHANGELOG.md
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# jira-time-tracker
|
|
37
|
+
|
|
38
|
+
`jtt` answers one question about a Jira Cloud project: **how much estimated work
|
|
39
|
+
came into being over a period, on which issues, and by whom?**
|
|
40
|
+
|
|
41
|
+
The estimate is whatever field your organisation tracks. That could be story
|
|
42
|
+
points, an hours or days custom field, or Jira's own original estimate or time
|
|
43
|
+
spent. You pick the field once per project.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
$ jtt contributions --from 2026-09-24 --to 2026-09-30 --by day
|
|
47
|
+
╭─ Contributions · DEMO ─────────────────────────────────────────────────────────╮
|
|
48
|
+
│ Field Story point estimate (customfield_10016, points) │
|
|
49
|
+
│ Period Thu 24 Sep 2026 included → Wed 30 Sep 2026 excluded · 6 days │
|
|
50
|
+
│ Credit each change goes to whoever held the issue when it happened │
|
|
51
|
+
╰───────────────────────────────────────────────────────────────────────────────╯
|
|
52
|
+
Person Net SP Added Share Issues Created Done
|
|
53
|
+
─────────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
Alice Martin +16 SP 16 SP 100% 3 4 (2 SP) 2 (8 SP) ████████
|
|
55
|
+
|
|
56
|
+
Per day
|
|
57
|
+
When Net SP
|
|
58
|
+
─────────────────────────────────────────
|
|
59
|
+
Thu 24 Sep +5 SP ██████████████████
|
|
60
|
+
Fri 25 Sep +8.5 SP ██████████████████████████████
|
|
61
|
+
Sat 26 Sep ±0
|
|
62
|
+
…
|
|
63
|
+
╭─ Statistics ───────────────────────────────────────────────────────────────────╮
|
|
64
|
+
│ Net added +16 SP │
|
|
65
|
+
│ Per changed issue 5.33 SP average over 3 │
|
|
66
|
+
│ Biggest DEMO-729 +8 SP Share a published report through a public link │
|
|
67
|
+
│ Done in period 2 issues (8 SP) │
|
|
68
|
+
│ Daily trend ▅█ ▂ │
|
|
69
|
+
│ Busiest day Fri 25 Sep (8.5 SP, 3 active days) │
|
|
70
|
+
╰────────────────────────────────────────────────────────────────────────────────╯
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
It needs Python 3.10 or newer and works the same on Linux, macOS and Windows.
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
uv tool install git+https://github.com/codeonym/jira-time-tracker # or: pipx install git+…
|
|
79
|
+
jtt --help
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
To install from a local checkout, run `uv tool install .` or `pipx install .`.
|
|
83
|
+
|
|
84
|
+
## Getting started
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
jtt login # site URL, account email, API token (typed hidden)
|
|
88
|
+
jtt init # for each project: choose the field that holds the estimate, and its unit
|
|
89
|
+
jtt calculate --period last-week
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
1. **`jtt login`** checks the token against Jira before storing it. Create a
|
|
93
|
+
token at <https://id.atlassian.com/manage-profile/security/api-tokens>. To
|
|
94
|
+
switch account or rotate the token, run `jtt logout` first, then `jtt login`
|
|
95
|
+
again.
|
|
96
|
+
2. **`jtt init`** walks through every project you can see and lists its numeric
|
|
97
|
+
fields. A ★ marks the fields on the project's create screens, and those come
|
|
98
|
+
first. You pick one and say what its numbers mean: points, seconds, minutes,
|
|
99
|
+
hours, days or weeks. For time units you also pick the unit reports should
|
|
100
|
+
show. Jira's built-in time tracking fields always hold seconds, so the tool
|
|
101
|
+
sets that unit for you. Your last choice is offered as the default for the
|
|
102
|
+
next project. Type `s` to skip a project or `q` to stop; progress is saved
|
|
103
|
+
after each project.
|
|
104
|
+
|
|
105
|
+
To skip the questions, pass everything as options:
|
|
106
|
+
`jtt init -p DEMO --field customfield_10016 --unit points`.
|
|
107
|
+
|
|
108
|
+
## Commands
|
|
109
|
+
|
|
110
|
+
| Command | What it does |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `login` / `logout [--purge]` / `whoami [--check]` | Manage the account. `logout` keeps project settings; `--purge` forgets them too |
|
|
113
|
+
| `init [-p KEY…] [--reconfigure]` | Choose each project's tracked field and unit |
|
|
114
|
+
| `config show \| path \| default KEY \| set KEY … \| remove KEY \| calendar …` | Inspect or edit settings without re-running `init` |
|
|
115
|
+
| `projects [--configured]` | Projects you can see and what each one tracks |
|
|
116
|
+
| `fields [-p KEY] [--counts]` | A project's numeric fields, optionally with how many issues hold a value |
|
|
117
|
+
| `fetch` | Issues touched in a period, with their current value (`--json` for raw output) |
|
|
118
|
+
| `calculate` | Net amount per issue for the whole project or one person, at four levels of detail |
|
|
119
|
+
| `collaborators` | People who can be assigned to the project, with their current open, in-progress and done issues |
|
|
120
|
+
| `contributions [--by day\|week]` | Per-person net, added, removed, share, issues created and issues finished, plus statistics and a daily trend |
|
|
121
|
+
| `export -o FILE [-k issues\|changes\|contributions\|daily]` | Write CSV (opens in Excel), JSON or Markdown |
|
|
122
|
+
|
|
123
|
+
Every report takes `-p KEY` (default: the project set by `init`), a period, and
|
|
124
|
+
`--as hours|days|…` to show time amounts in another unit.
|
|
125
|
+
|
|
126
|
+
### Periods
|
|
127
|
+
|
|
128
|
+
Periods are **half-open: the start day is included and the end day is
|
|
129
|
+
excluded.** `--from 2026-09-01 --to 2026-10-01` is all of September. Leave out
|
|
130
|
+
`--to` and the period runs through today.
|
|
131
|
+
|
|
132
|
+
You can also use a named period instead:
|
|
133
|
+
`--period today | yesterday | this-week | last-week | this-month | last-month | this-quarter | last-quarter | this-year | last-7-days | last-30-days`.
|
|
134
|
+
Weeks start on Monday.
|
|
135
|
+
|
|
136
|
+
Days are the calendar days Jira shows in *your* Jira profile's time zone, which
|
|
137
|
+
is also how Jira reads dates in queries. The tool never works out time zone
|
|
138
|
+
offsets itself, because local time zone data can disagree with Jira's. For
|
|
139
|
+
example, tzdata 2026d keeps Morocco at +00 after Ramadan 2026, while Jira still
|
|
140
|
+
shows +01.
|
|
141
|
+
|
|
142
|
+
### Choosing whose work to count (`--user`)
|
|
143
|
+
|
|
144
|
+
`--user` accepts `me`, an account id, or part of a name or email.
|
|
145
|
+
|
|
146
|
+
- `calculate --user X` (the default, `--attribution was`) counts every change
|
|
147
|
+
made in the period on issues X held at some point in the period.
|
|
148
|
+
- `--attribution holder` only counts changes made while X held the issue.
|
|
149
|
+
- `contributions` always credits each change to whoever held the issue when it
|
|
150
|
+
happened. The value an issue was created with goes to the first person
|
|
151
|
+
assigned to it. That way the per-person totals add up to the project total.
|
|
152
|
+
|
|
153
|
+
### Levels of detail
|
|
154
|
+
|
|
155
|
+
`-v`, `-vv` (or `--vv`) and `-vvv` (or `--vvv`) work on `calculate` and
|
|
156
|
+
`contributions`:
|
|
157
|
+
|
|
158
|
+
- **`-v`** adds current value, type, status and current assignee (↪ marks a
|
|
159
|
+
task that has since moved to someone else), and where each amount came from.
|
|
160
|
+
- **`-vv`** also lists issues with no change, and adds a per-issue timeline of
|
|
161
|
+
each change: when it happened, the value before and after, and who made it.
|
|
162
|
+
- **`-vvv`** also shows the query sent to Jira, a trace of every API request,
|
|
163
|
+
and changes outside the period (struck through).
|
|
164
|
+
|
|
165
|
+
## How the delta is computed
|
|
166
|
+
|
|
167
|
+
For each issue touched in the period, the tool reads its full change history
|
|
168
|
+
from Jira and adds up the changes to the tracked field that happened inside the
|
|
169
|
+
period. Two Jira behaviours shape this:
|
|
170
|
+
|
|
171
|
+
- **The query can't use "last updated" as an end date.** Jira only records when
|
|
172
|
+
an issue was *last* updated. An issue edited inside the period and again
|
|
173
|
+
afterwards would drop out of a query ending at the period's end. So the query
|
|
174
|
+
only filters on that date from below, and the change history decides what
|
|
175
|
+
falls inside the period.
|
|
176
|
+
- **A value entered when an issue is created leaves no history entry.** Jira's
|
|
177
|
+
history starts at the first edit. For an issue created inside the period, the
|
|
178
|
+
tool adds back the value it was created with. That value is the "before" side
|
|
179
|
+
of its first later change, or its current value if the field was never
|
|
180
|
+
edited.
|
|
181
|
+
|
|
182
|
+
Dates in queries are always sent with an explicit `00:00`. Tested on a live
|
|
183
|
+
site, bare dates did not give exact bounds: `assignee WAS X DURING ("2026-09-24",
|
|
184
|
+
"2026-09-25")` matched issues first assigned in the afternoon of the 25th, and
|
|
185
|
+
`DURING ("2026-09-25", "2026-09-25")` matched nothing.
|
|
186
|
+
|
|
187
|
+
## Where things are kept
|
|
188
|
+
|
|
189
|
+
| What | Where |
|
|
190
|
+
|---|---|
|
|
191
|
+
| Settings (site, projects, fields, units) | `config.json` in the user config directory; `jtt config path` prints it. On Linux: `~/.config/jira-time-tracker`, macOS: `~/Library/Application Support/jira-time-tracker`, Windows: `%LOCALAPPDATA%\jira-time-tracker` |
|
|
192
|
+
| API token | The system keyring: Windows Credential Manager, macOS Keychain, or Secret Service (GNOME Keyring / KWallet) on Linux. Where no keyring is available (headless servers, WSL, containers), it goes in `credentials.json` next to the settings, created **0600** inside a **0700** directory |
|
|
193
|
+
|
|
194
|
+
The token is never written to `config.json` or printed.
|
|
195
|
+
|
|
196
|
+
Environment overrides:
|
|
197
|
+
- `JTT_CREDENTIAL_BACKEND=file|keyring` forces where the token is stored.
|
|
198
|
+
- `JTT_API_TOKEN` supplies a token for CI; it is never stored.
|
|
199
|
+
- `JTT_CONFIG_DIR` moves the settings directory.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
uv sync
|
|
205
|
+
uv run pytest
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The tests run the real CLI and HTTP client against a local fake Jira that
|
|
209
|
+
replays issue data captured from a live site.
|
|
210
|
+
|
|
211
|
+
`legacy/jira_tasks.py` is the single-file script this project grew from, kept
|
|
212
|
+
unchanged.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# jira-time-tracker
|
|
2
|
+
|
|
3
|
+
`jtt` answers one question about a Jira Cloud project: **how much estimated work
|
|
4
|
+
came into being over a period, on which issues, and by whom?**
|
|
5
|
+
|
|
6
|
+
The estimate is whatever field your organisation tracks. That could be story
|
|
7
|
+
points, an hours or days custom field, or Jira's own original estimate or time
|
|
8
|
+
spent. You pick the field once per project.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
$ jtt contributions --from 2026-09-24 --to 2026-09-30 --by day
|
|
12
|
+
╭─ Contributions · DEMO ─────────────────────────────────────────────────────────╮
|
|
13
|
+
│ Field Story point estimate (customfield_10016, points) │
|
|
14
|
+
│ Period Thu 24 Sep 2026 included → Wed 30 Sep 2026 excluded · 6 days │
|
|
15
|
+
│ Credit each change goes to whoever held the issue when it happened │
|
|
16
|
+
╰───────────────────────────────────────────────────────────────────────────────╯
|
|
17
|
+
Person Net SP Added Share Issues Created Done
|
|
18
|
+
─────────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
Alice Martin +16 SP 16 SP 100% 3 4 (2 SP) 2 (8 SP) ████████
|
|
20
|
+
|
|
21
|
+
Per day
|
|
22
|
+
When Net SP
|
|
23
|
+
─────────────────────────────────────────
|
|
24
|
+
Thu 24 Sep +5 SP ██████████████████
|
|
25
|
+
Fri 25 Sep +8.5 SP ██████████████████████████████
|
|
26
|
+
Sat 26 Sep ±0
|
|
27
|
+
…
|
|
28
|
+
╭─ Statistics ───────────────────────────────────────────────────────────────────╮
|
|
29
|
+
│ Net added +16 SP │
|
|
30
|
+
│ Per changed issue 5.33 SP average over 3 │
|
|
31
|
+
│ Biggest DEMO-729 +8 SP Share a published report through a public link │
|
|
32
|
+
│ Done in period 2 issues (8 SP) │
|
|
33
|
+
│ Daily trend ▅█ ▂ │
|
|
34
|
+
│ Busiest day Fri 25 Sep (8.5 SP, 3 active days) │
|
|
35
|
+
╰────────────────────────────────────────────────────────────────────────────────╯
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
It needs Python 3.10 or newer and works the same on Linux, macOS and Windows.
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
uv tool install git+https://github.com/codeonym/jira-time-tracker # or: pipx install git+…
|
|
44
|
+
jtt --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
To install from a local checkout, run `uv tool install .` or `pipx install .`.
|
|
48
|
+
|
|
49
|
+
## Getting started
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
jtt login # site URL, account email, API token (typed hidden)
|
|
53
|
+
jtt init # for each project: choose the field that holds the estimate, and its unit
|
|
54
|
+
jtt calculate --period last-week
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
1. **`jtt login`** checks the token against Jira before storing it. Create a
|
|
58
|
+
token at <https://id.atlassian.com/manage-profile/security/api-tokens>. To
|
|
59
|
+
switch account or rotate the token, run `jtt logout` first, then `jtt login`
|
|
60
|
+
again.
|
|
61
|
+
2. **`jtt init`** walks through every project you can see and lists its numeric
|
|
62
|
+
fields. A ★ marks the fields on the project's create screens, and those come
|
|
63
|
+
first. You pick one and say what its numbers mean: points, seconds, minutes,
|
|
64
|
+
hours, days or weeks. For time units you also pick the unit reports should
|
|
65
|
+
show. Jira's built-in time tracking fields always hold seconds, so the tool
|
|
66
|
+
sets that unit for you. Your last choice is offered as the default for the
|
|
67
|
+
next project. Type `s` to skip a project or `q` to stop; progress is saved
|
|
68
|
+
after each project.
|
|
69
|
+
|
|
70
|
+
To skip the questions, pass everything as options:
|
|
71
|
+
`jtt init -p DEMO --field customfield_10016 --unit points`.
|
|
72
|
+
|
|
73
|
+
## Commands
|
|
74
|
+
|
|
75
|
+
| Command | What it does |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `login` / `logout [--purge]` / `whoami [--check]` | Manage the account. `logout` keeps project settings; `--purge` forgets them too |
|
|
78
|
+
| `init [-p KEY…] [--reconfigure]` | Choose each project's tracked field and unit |
|
|
79
|
+
| `config show \| path \| default KEY \| set KEY … \| remove KEY \| calendar …` | Inspect or edit settings without re-running `init` |
|
|
80
|
+
| `projects [--configured]` | Projects you can see and what each one tracks |
|
|
81
|
+
| `fields [-p KEY] [--counts]` | A project's numeric fields, optionally with how many issues hold a value |
|
|
82
|
+
| `fetch` | Issues touched in a period, with their current value (`--json` for raw output) |
|
|
83
|
+
| `calculate` | Net amount per issue for the whole project or one person, at four levels of detail |
|
|
84
|
+
| `collaborators` | People who can be assigned to the project, with their current open, in-progress and done issues |
|
|
85
|
+
| `contributions [--by day\|week]` | Per-person net, added, removed, share, issues created and issues finished, plus statistics and a daily trend |
|
|
86
|
+
| `export -o FILE [-k issues\|changes\|contributions\|daily]` | Write CSV (opens in Excel), JSON or Markdown |
|
|
87
|
+
|
|
88
|
+
Every report takes `-p KEY` (default: the project set by `init`), a period, and
|
|
89
|
+
`--as hours|days|…` to show time amounts in another unit.
|
|
90
|
+
|
|
91
|
+
### Periods
|
|
92
|
+
|
|
93
|
+
Periods are **half-open: the start day is included and the end day is
|
|
94
|
+
excluded.** `--from 2026-09-01 --to 2026-10-01` is all of September. Leave out
|
|
95
|
+
`--to` and the period runs through today.
|
|
96
|
+
|
|
97
|
+
You can also use a named period instead:
|
|
98
|
+
`--period today | yesterday | this-week | last-week | this-month | last-month | this-quarter | last-quarter | this-year | last-7-days | last-30-days`.
|
|
99
|
+
Weeks start on Monday.
|
|
100
|
+
|
|
101
|
+
Days are the calendar days Jira shows in *your* Jira profile's time zone, which
|
|
102
|
+
is also how Jira reads dates in queries. The tool never works out time zone
|
|
103
|
+
offsets itself, because local time zone data can disagree with Jira's. For
|
|
104
|
+
example, tzdata 2026d keeps Morocco at +00 after Ramadan 2026, while Jira still
|
|
105
|
+
shows +01.
|
|
106
|
+
|
|
107
|
+
### Choosing whose work to count (`--user`)
|
|
108
|
+
|
|
109
|
+
`--user` accepts `me`, an account id, or part of a name or email.
|
|
110
|
+
|
|
111
|
+
- `calculate --user X` (the default, `--attribution was`) counts every change
|
|
112
|
+
made in the period on issues X held at some point in the period.
|
|
113
|
+
- `--attribution holder` only counts changes made while X held the issue.
|
|
114
|
+
- `contributions` always credits each change to whoever held the issue when it
|
|
115
|
+
happened. The value an issue was created with goes to the first person
|
|
116
|
+
assigned to it. That way the per-person totals add up to the project total.
|
|
117
|
+
|
|
118
|
+
### Levels of detail
|
|
119
|
+
|
|
120
|
+
`-v`, `-vv` (or `--vv`) and `-vvv` (or `--vvv`) work on `calculate` and
|
|
121
|
+
`contributions`:
|
|
122
|
+
|
|
123
|
+
- **`-v`** adds current value, type, status and current assignee (↪ marks a
|
|
124
|
+
task that has since moved to someone else), and where each amount came from.
|
|
125
|
+
- **`-vv`** also lists issues with no change, and adds a per-issue timeline of
|
|
126
|
+
each change: when it happened, the value before and after, and who made it.
|
|
127
|
+
- **`-vvv`** also shows the query sent to Jira, a trace of every API request,
|
|
128
|
+
and changes outside the period (struck through).
|
|
129
|
+
|
|
130
|
+
## How the delta is computed
|
|
131
|
+
|
|
132
|
+
For each issue touched in the period, the tool reads its full change history
|
|
133
|
+
from Jira and adds up the changes to the tracked field that happened inside the
|
|
134
|
+
period. Two Jira behaviours shape this:
|
|
135
|
+
|
|
136
|
+
- **The query can't use "last updated" as an end date.** Jira only records when
|
|
137
|
+
an issue was *last* updated. An issue edited inside the period and again
|
|
138
|
+
afterwards would drop out of a query ending at the period's end. So the query
|
|
139
|
+
only filters on that date from below, and the change history decides what
|
|
140
|
+
falls inside the period.
|
|
141
|
+
- **A value entered when an issue is created leaves no history entry.** Jira's
|
|
142
|
+
history starts at the first edit. For an issue created inside the period, the
|
|
143
|
+
tool adds back the value it was created with. That value is the "before" side
|
|
144
|
+
of its first later change, or its current value if the field was never
|
|
145
|
+
edited.
|
|
146
|
+
|
|
147
|
+
Dates in queries are always sent with an explicit `00:00`. Tested on a live
|
|
148
|
+
site, bare dates did not give exact bounds: `assignee WAS X DURING ("2026-09-24",
|
|
149
|
+
"2026-09-25")` matched issues first assigned in the afternoon of the 25th, and
|
|
150
|
+
`DURING ("2026-09-25", "2026-09-25")` matched nothing.
|
|
151
|
+
|
|
152
|
+
## Where things are kept
|
|
153
|
+
|
|
154
|
+
| What | Where |
|
|
155
|
+
|---|---|
|
|
156
|
+
| Settings (site, projects, fields, units) | `config.json` in the user config directory; `jtt config path` prints it. On Linux: `~/.config/jira-time-tracker`, macOS: `~/Library/Application Support/jira-time-tracker`, Windows: `%LOCALAPPDATA%\jira-time-tracker` |
|
|
157
|
+
| API token | The system keyring: Windows Credential Manager, macOS Keychain, or Secret Service (GNOME Keyring / KWallet) on Linux. Where no keyring is available (headless servers, WSL, containers), it goes in `credentials.json` next to the settings, created **0600** inside a **0700** directory |
|
|
158
|
+
|
|
159
|
+
The token is never written to `config.json` or printed.
|
|
160
|
+
|
|
161
|
+
Environment overrides:
|
|
162
|
+
- `JTT_CREDENTIAL_BACKEND=file|keyring` forces where the token is stored.
|
|
163
|
+
- `JTT_API_TOKEN` supplies a token for CI; it is never stored.
|
|
164
|
+
- `JTT_CONFIG_DIR` moves the settings directory.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
uv sync
|
|
170
|
+
uv run pytest
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The tests run the real CLI and HTTP client against a local fake Jira that
|
|
174
|
+
replays issue data captured from a live site.
|
|
175
|
+
|
|
176
|
+
`legacy/jira_tasks.py` is the single-file script this project grew from, kept
|
|
177
|
+
unchanged.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jira-time-tracker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Track how much estimated work (story points, hours, days…) came into being in a Jira project over a period, per issue and per person."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"keyring>=24",
|
|
11
|
+
"platformdirs>=4",
|
|
12
|
+
"requests>=2.31",
|
|
13
|
+
"rich>=13",
|
|
14
|
+
"typer>=0.12",
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"jira",
|
|
18
|
+
"atlassian",
|
|
19
|
+
"story-points",
|
|
20
|
+
"estimates",
|
|
21
|
+
"time-tracking",
|
|
22
|
+
"cli",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Environment :: Console",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Intended Audience :: Information Technology",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Programming Language :: Python :: 3.14",
|
|
37
|
+
"Topic :: Office/Business",
|
|
38
|
+
"Topic :: Software Development :: Bug Tracking",
|
|
39
|
+
"Typing :: Typed",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[project.authors]]
|
|
43
|
+
name = "codeonym-oss"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/codeonym-oss/jira-time-tracker"
|
|
47
|
+
Repository = "https://github.com/codeonym-oss/jira-time-tracker"
|
|
48
|
+
Issues = "https://github.com/codeonym-oss/jira-time-tracker/issues"
|
|
49
|
+
Changelog = "https://github.com/codeonym-oss/jira-time-tracker/blob/main/CHANGELOG.md"
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
jtt = "jira_time_tracker.cli:app"
|
|
53
|
+
jira-time-tracker = "jira_time_tracker.cli:app"
|
|
54
|
+
|
|
55
|
+
[build-system]
|
|
56
|
+
requires = ["uv_build>=0.8.22,<0.13"]
|
|
57
|
+
build-backend = "uv_build"
|
|
58
|
+
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
test = [
|
|
61
|
+
"pytest>=8",
|
|
62
|
+
"pytest-cov>=7",
|
|
63
|
+
]
|
|
64
|
+
dev = [
|
|
65
|
+
{ include-group = "test" },
|
|
66
|
+
"commitizen>=4.8",
|
|
67
|
+
"pre-commit>=4.3",
|
|
68
|
+
"pyright>=1.1.400",
|
|
69
|
+
"ruff>=0.16",
|
|
70
|
+
"types-requests>=2.31",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 100
|
|
75
|
+
target-version = "py310"
|
|
76
|
+
extend-exclude = ["legacy"]
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint]
|
|
79
|
+
select = [
|
|
80
|
+
"E",
|
|
81
|
+
"W",
|
|
82
|
+
"F",
|
|
83
|
+
"I",
|
|
84
|
+
"B",
|
|
85
|
+
"UP",
|
|
86
|
+
"SIM",
|
|
87
|
+
"RUF",
|
|
88
|
+
"C4",
|
|
89
|
+
"PT",
|
|
90
|
+
"PIE",
|
|
91
|
+
"RET",
|
|
92
|
+
"TC",
|
|
93
|
+
"PERF",
|
|
94
|
+
"N",
|
|
95
|
+
"D",
|
|
96
|
+
]
|
|
97
|
+
ignore = [
|
|
98
|
+
"D100",
|
|
99
|
+
"D104",
|
|
100
|
+
"D105",
|
|
101
|
+
"D107",
|
|
102
|
+
"D203",
|
|
103
|
+
"D213",
|
|
104
|
+
"RUF001",
|
|
105
|
+
"RUF002",
|
|
106
|
+
"RUF003",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
[tool.ruff.lint.per-file-ignores]
|
|
110
|
+
"tests/**" = ["D"]
|
|
111
|
+
"src/jira_time_tracker/cli.py" = ["TC"]
|
|
112
|
+
|
|
113
|
+
[tool.commitizen]
|
|
114
|
+
name = "cz_conventional_commits"
|
|
115
|
+
allowed_prefixes = [
|
|
116
|
+
"Merge",
|
|
117
|
+
"Revert",
|
|
118
|
+
"fixup!",
|
|
119
|
+
"squash!",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[tool.pyright]
|
|
123
|
+
include = [
|
|
124
|
+
"src",
|
|
125
|
+
"tests",
|
|
126
|
+
"scripts",
|
|
127
|
+
]
|
|
128
|
+
pythonVersion = "3.10"
|
|
129
|
+
typeCheckingMode = "standard"
|
|
130
|
+
|
|
131
|
+
[tool.pytest.ini_options]
|
|
132
|
+
addopts = [
|
|
133
|
+
"--strict-markers",
|
|
134
|
+
"--strict-config",
|
|
135
|
+
"-ra",
|
|
136
|
+
"--cov",
|
|
137
|
+
"--cov-report=term-missing",
|
|
138
|
+
]
|
|
139
|
+
testpaths = ["tests"]
|
|
140
|
+
xfail_strict = true
|
|
141
|
+
filterwarnings = ["error"]
|
|
142
|
+
|
|
143
|
+
[tool.coverage.run]
|
|
144
|
+
source = ["jira_time_tracker"]
|
|
145
|
+
branch = true
|
|
146
|
+
|
|
147
|
+
[tool.coverage.report]
|
|
148
|
+
fail_under = 80
|
|
149
|
+
show_missing = true
|
|
150
|
+
skip_covered = true
|
|
151
|
+
exclude_also = [
|
|
152
|
+
"if TYPE_CHECKING:",
|
|
153
|
+
"@overload",
|
|
154
|
+
"raise NotImplementedError",
|
|
155
|
+
"if __name__ == .__main__.:",
|
|
156
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jira-time-tracker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Track how much estimated work (story points, hours, days…) came into being in a Jira project over a period, per issue and per person."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "codeonym-oss" }]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"keyring>=24",
|
|
12
|
+
"platformdirs>=4",
|
|
13
|
+
"requests>=2.31",
|
|
14
|
+
"rich>=13",
|
|
15
|
+
"typer>=0.12",
|
|
16
|
+
]
|
|
17
|
+
keywords = ["jira", "atlassian", "story-points", "estimates", "time-tracking", "cli"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Information Technology",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
31
|
+
"Topic :: Office/Business",
|
|
32
|
+
"Topic :: Software Development :: Bug Tracking",
|
|
33
|
+
"Typing :: Typed",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/codeonym-oss/jira-time-tracker"
|
|
38
|
+
Repository = "https://github.com/codeonym-oss/jira-time-tracker"
|
|
39
|
+
Issues = "https://github.com/codeonym-oss/jira-time-tracker/issues"
|
|
40
|
+
Changelog = "https://github.com/codeonym-oss/jira-time-tracker/blob/main/CHANGELOG.md"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
jtt = "jira_time_tracker.cli:app"
|
|
44
|
+
jira-time-tracker = "jira_time_tracker.cli:app"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["uv_build>=0.8.22,<0.13"]
|
|
48
|
+
build-backend = "uv_build"
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
test = [
|
|
52
|
+
"pytest>=8",
|
|
53
|
+
"pytest-cov>=7",
|
|
54
|
+
]
|
|
55
|
+
dev = [
|
|
56
|
+
{include-group = "test"},
|
|
57
|
+
"commitizen>=4.8",
|
|
58
|
+
"pre-commit>=4.3",
|
|
59
|
+
"pyright>=1.1.400",
|
|
60
|
+
"ruff>=0.16",
|
|
61
|
+
"types-requests>=2.31",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
line-length = 100
|
|
66
|
+
target-version = "py310"
|
|
67
|
+
# The pre-project script, kept verbatim.
|
|
68
|
+
extend-exclude = ["legacy"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint]
|
|
71
|
+
select = [
|
|
72
|
+
"E", "W", "F", "I", "B", "UP", "SIM", "RUF", "C4", "PT", "PIE", "RET",
|
|
73
|
+
"TC", "PERF", "N", "D",
|
|
74
|
+
]
|
|
75
|
+
ignore = [
|
|
76
|
+
"D100", "D104", "D105", "D107", # module/package/magic/__init__ docstrings
|
|
77
|
+
"D203", "D213", # pick D211 + D212 (summary on the first line)
|
|
78
|
+
"RUF001", "RUF002", "RUF003", # the terminal UI deliberately uses →, ×, ±, ★ and friends
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.per-file-ignores]
|
|
82
|
+
"tests/**" = ["D"]
|
|
83
|
+
# Typer reads the command signatures' annotations at runtime, so their imports must stay real.
|
|
84
|
+
"src/jira_time_tracker/cli.py" = ["TC"]
|
|
85
|
+
|
|
86
|
+
[tool.commitizen]
|
|
87
|
+
# Only used to validate messages (commit-msg hook + CI); versions and changelog are release-please's job.
|
|
88
|
+
name = "cz_conventional_commits"
|
|
89
|
+
allowed_prefixes = ["Merge", "Revert", "fixup!", "squash!"]
|
|
90
|
+
|
|
91
|
+
[tool.pyright]
|
|
92
|
+
include = ["src", "tests", "scripts"]
|
|
93
|
+
pythonVersion = "3.10"
|
|
94
|
+
typeCheckingMode = "standard"
|
|
95
|
+
|
|
96
|
+
[tool.pytest.ini_options]
|
|
97
|
+
addopts = ["--strict-markers", "--strict-config", "-ra", "--cov", "--cov-report=term-missing"]
|
|
98
|
+
testpaths = ["tests"]
|
|
99
|
+
xfail_strict = true
|
|
100
|
+
filterwarnings = ["error"]
|
|
101
|
+
|
|
102
|
+
[tool.coverage.run]
|
|
103
|
+
source = ["jira_time_tracker"]
|
|
104
|
+
branch = true
|
|
105
|
+
|
|
106
|
+
[tool.coverage.report]
|
|
107
|
+
fail_under = 80
|
|
108
|
+
show_missing = true
|
|
109
|
+
skip_covered = true
|
|
110
|
+
exclude_also = ["if TYPE_CHECKING:", "@overload", "raise NotImplementedError", "if __name__ == .__main__.:"]
|