harvest-worklog 0.5.0 → 0.6.0
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.
- checksums.yaml +4 -4
- data/README.md +17 -1
- data/lib/harvest_worklog/aggregate_cli.rb +97 -0
- data/lib/harvest_worklog/version.rb +1 -1
- data/lib/harvest_worklog.rb +6 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6dcc34ed6ba1cee2e39e8dea71a9b05d80fa42b598366f16332eb1676e197789
|
|
4
|
+
data.tar.gz: 30e312eecb4f435930b74c2aa72a2a68e4f973d22f946e824bb8fee863ae8faa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fed0b9fa6998e23a607cb447e108d9849192ebdc209fc263e65ac850f19306a6430f628ccb2c5f8c9548d16d10197793ef2283e917391c2bca553aa12f7a5350
|
|
7
|
+
data.tar.gz: 1fcb49e4868b5c8e3d5eaadd37a0c6961c5be474164a6dc5ddd346f1286e82093b6b6698e176fc95c08a92f785873d7a2e8232b8b7f723daf1a351a3d572467a
|
data/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Personal Access Tokens are the intended Harvest authentication method for person
|
|
|
22
22
|
```text
|
|
23
23
|
harvest-worklog time-off FROM TO --project NAME --task NAME [options]
|
|
24
24
|
harvest-worklog work-entry DATE --project NAME --task NAME --hours HOURS --notes NOTES [options]
|
|
25
|
+
harvest-worklog aggregate FROM TO [--project NAME] [--task NAME]
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
### Time off
|
|
@@ -44,6 +45,18 @@ A time-off block never creates weekend entries. When names are supplied, it reso
|
|
|
44
45
|
|
|
45
46
|
The `work-entry` command checks for existing or locked entries for its project, task, and date before creating a new duration entry. Use `--dry-run` to complete that preflight without a write. OMP Project Time uses the same path.
|
|
46
47
|
|
|
48
|
+
### Read-only aggregates
|
|
49
|
+
|
|
50
|
+
The `aggregate` command reads all matching Harvest time-entry pages and prints totals grouped by spent date and project/task. It lists every date in the inclusive range, including weekend or empty dates, and never writes a Harvest record.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
harvest-worklog aggregate 2026-07-17 2026-07-19 \
|
|
54
|
+
--project WRAP \
|
|
55
|
+
--task Programming
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The `harvest_time_aggregates` OMP tool exposes the same optional filters and is approval-gated as a read.
|
|
59
|
+
|
|
47
60
|
## OMP settings
|
|
48
61
|
|
|
49
62
|
- `defaultHours`: hours per business day when a time-off tool call omits `hours`; defaults to `7`.
|
|
@@ -80,8 +93,11 @@ harvest-worklog time-off 2026-08-17 2026-08-28 \
|
|
|
80
93
|
--task 'Vacation / PTO' \
|
|
81
94
|
--holiday-region ca_yt \
|
|
82
95
|
--dry-run
|
|
96
|
+
VERSION="$(ruby -Ilib -rharvest_worklog/version -e 'puts HarvestWorklog::VERSION')"
|
|
83
97
|
gem build harvest-worklog.gemspec
|
|
84
|
-
gem push harvest-worklog
|
|
98
|
+
gem push "harvest-worklog-${VERSION}.gem"
|
|
99
|
+
gem uninstall harvest-time-off --all --executables --ignore-dependencies
|
|
100
|
+
gem install --clear-sources --source https://rubygems.org harvest-worklog --version "$VERSION" --no-document
|
|
85
101
|
omp plugin uninstall harvest-time-off
|
|
86
102
|
omp plugin install --force github:klondikemarlen/harvest-worklog
|
|
87
103
|
omp plugin list
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HarvestWorklog
|
|
4
|
+
class AggregateCLI
|
|
5
|
+
PER_PAGE = 100
|
|
6
|
+
|
|
7
|
+
def self.run(arguments, output: $stdout, error: $stderr, client: nil)
|
|
8
|
+
options = {}
|
|
9
|
+
parser = option_parser(options)
|
|
10
|
+
dates = parser.parse(arguments)
|
|
11
|
+
validate!(dates)
|
|
12
|
+
|
|
13
|
+
from = Date.iso8601(dates[0])
|
|
14
|
+
to = Date.iso8601(dates[1])
|
|
15
|
+
raise Error, "end date must not be before start date" if to < from
|
|
16
|
+
|
|
17
|
+
client ||= Marlens::HarvestApiV2::Client.from_environment
|
|
18
|
+
entries = fetch_entries(client, from:, to:).select { |entry| matches?(entry, options) }
|
|
19
|
+
print_summary(output, entries, from:, to:)
|
|
20
|
+
0
|
|
21
|
+
rescue Error, Marlens::HarvestApiV2::Error, OptionParser::ParseError, Date::Error => e
|
|
22
|
+
error.puts "Error: #{e.message}"
|
|
23
|
+
error.puts parser if parser
|
|
24
|
+
1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.option_parser(options)
|
|
28
|
+
OptionParser.new do |opts|
|
|
29
|
+
opts.banner = "Usage: harvest-worklog aggregate FROM TO [--project NAME] [--task NAME]"
|
|
30
|
+
opts.on("--project NAME", "Filter by Harvest project name") { |value| options[:project] = value }
|
|
31
|
+
opts.on("--task NAME", "Filter by Harvest task name") { |value| options[:task] = value }
|
|
32
|
+
opts.on("-h", "--help", "Show this help") do
|
|
33
|
+
puts opts
|
|
34
|
+
exit 0
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.validate!(dates)
|
|
40
|
+
raise Error, "FROM and TO are required" unless dates.length == 2
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.fetch_entries(client, from:, to:)
|
|
44
|
+
entries = []
|
|
45
|
+
page = 1
|
|
46
|
+
loop do
|
|
47
|
+
response = client.request(
|
|
48
|
+
:get,
|
|
49
|
+
"/v2/time_entries",
|
|
50
|
+
params: { from: from.iso8601, to: to.iso8601, page:, per_page: PER_PAGE }
|
|
51
|
+
)
|
|
52
|
+
entries.concat(response.fetch("time_entries"))
|
|
53
|
+
page = response["next_page"]
|
|
54
|
+
break if page.nil?
|
|
55
|
+
end
|
|
56
|
+
entries
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.matches?(entry, options)
|
|
60
|
+
name_matches?(entry.dig("project", "name"), options[:project]) &&
|
|
61
|
+
name_matches?(entry.dig("task", "name"), options[:task])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.name_matches?(actual, expected)
|
|
65
|
+
expected.nil? || actual&.casecmp?(expected)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.print_summary(output, entries, from:, to:)
|
|
69
|
+
output.puts "#{entries.length} #{entry_label(entries.length)}, #{HarvestWorklog.display_hours(total_hours(entries))}h from #{from.iso8601} through #{to.iso8601}"
|
|
70
|
+
output.puts "By date:"
|
|
71
|
+
(from..to).each do |date|
|
|
72
|
+
date_entries = entries.select { |entry| entry.fetch("spent_date") == date.iso8601 }
|
|
73
|
+
output.puts " #{date.iso8601}: #{date_entries.length} #{entry_label(date_entries.length)}, #{HarvestWorklog.display_hours(total_hours(date_entries))}h"
|
|
74
|
+
end
|
|
75
|
+
output.puts "By project/task:"
|
|
76
|
+
|
|
77
|
+
groups = entries.group_by { |entry| [entry.dig("project", "name"), entry.dig("task", "name")] }
|
|
78
|
+
if groups.empty?
|
|
79
|
+
output.puts " none"
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
groups.keys.sort_by { |project, task| [project.downcase, task.downcase] }.each do |project, task|
|
|
84
|
+
group = groups.fetch([project, task])
|
|
85
|
+
output.puts " #{project} / #{task}: #{group.length} #{entry_label(group.length)}, #{HarvestWorklog.display_hours(total_hours(group))}h"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.total_hours(entries)
|
|
90
|
+
entries.sum { |entry| Float(entry.fetch("hours")) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.entry_label(count)
|
|
94
|
+
count == 1 ? "entry" : "entries"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/harvest_worklog.rb
CHANGED
|
@@ -34,11 +34,13 @@ module HarvestWorklog
|
|
|
34
34
|
TimeOffCLI.run(command_arguments, output:, error:, client:)
|
|
35
35
|
when "work-entry"
|
|
36
36
|
WorkEntryCLI.run(command_arguments, output:, error:, client:)
|
|
37
|
+
when "aggregate"
|
|
38
|
+
AggregateCLI.run(command_arguments, output:, error:, client:)
|
|
37
39
|
when "-h", "--help"
|
|
38
40
|
output.puts usage
|
|
39
41
|
0
|
|
40
42
|
else
|
|
41
|
-
error.puts "Error: choose time-off
|
|
43
|
+
error.puts "Error: choose time-off, work-entry, or aggregate"
|
|
42
44
|
error.puts usage
|
|
43
45
|
1
|
|
44
46
|
end
|
|
@@ -49,10 +51,12 @@ module HarvestWorklog
|
|
|
49
51
|
Usage:
|
|
50
52
|
harvest-worklog time-off FROM TO --project NAME --task NAME [options]
|
|
51
53
|
harvest-worklog work-entry DATE --project NAME --task NAME --hours HOURS --notes NOTES [options]
|
|
54
|
+
harvest-worklog aggregate FROM TO [--project NAME] [--task NAME]
|
|
52
55
|
|
|
53
56
|
Commands:
|
|
54
57
|
time-off Create one entry per local business day in a date range.
|
|
55
58
|
work-entry Create one reviewed ordinary-work entry.
|
|
59
|
+
aggregate Read time-entry totals without writing Harvest records.
|
|
56
60
|
USAGE
|
|
57
61
|
end
|
|
58
62
|
end
|
|
@@ -165,3 +169,4 @@ module HarvestWorklog
|
|
|
165
169
|
end
|
|
166
170
|
|
|
167
171
|
require "harvest_worklog/work_entry_cli"
|
|
172
|
+
require "harvest_worklog/aggregate_cli"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: harvest-worklog
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marlen Brunner
|
|
@@ -62,6 +62,7 @@ files:
|
|
|
62
62
|
- bin/harvest-worklog
|
|
63
63
|
- harvest-worklog.rb
|
|
64
64
|
- lib/harvest_worklog.rb
|
|
65
|
+
- lib/harvest_worklog/aggregate_cli.rb
|
|
65
66
|
- lib/harvest_worklog/version.rb
|
|
66
67
|
- lib/harvest_worklog/work_entry_cli.rb
|
|
67
68
|
homepage: https://github.com/klondikemarlen/harvest-worklog
|