time_log_robot 0.1.0 → 0.1.1
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/.gitignore +0 -1
- data/.settings.yml +0 -1
- data/Gemfile.lock +1 -1
- data/README.md +0 -6
- data/bin/time_log_robot +1 -0
- data/lib/time_log_robot.rb +3 -3
- data/lib/time_log_robot/jira/issue_key_parser.rb +29 -0
- data/lib/time_log_robot/jira/payload_builder.rb +8 -14
- data/lib/time_log_robot/jira/work_logger.rb +85 -72
- data/lib/time_log_robot/toggl/report.rb +35 -30
- data/lib/time_log_robot/toggl/tagger.rb +28 -25
- data/lib/time_log_robot/version.rb +1 -1
- data/mapping.yml +3 -0
- data/time_log_robot.gemspec +2 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0559ea642c0065a0807d04c0df40d65b50e6e285
|
4
|
+
data.tar.gz: aa9f9fb89b4b9bfa8218fee09d5bfaa6b71b6af0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e71c80181ea7bae7b217a18f9a0c3e03e6218d2b53803b32ab7272b7b22f6de60bc15cdb1d47cbdf8d6f8d243194d10f27bae4855acb1b0cd7594ba2a6f89ba3
|
7
|
+
data.tar.gz: 90ece6f79709e99a83cf12040c687cfa3de7839c506e00ba877075674d5be22805bc992d05470bf1a73cf59bc13013d82dab308b4d51d39c8c0645747fe8e473
|
data/.gitignore
CHANGED
data/.settings.yml
CHANGED
@@ -1 +0,0 @@
|
|
1
|
-
---
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -5,14 +5,8 @@ This is an integration between project management tools (like JIRA) and time log
|
|
5
5
|
|
6
6
|
## Installation
|
7
7
|
|
8
|
-
~~Download this gem:~~ This is not yet on RubyGems, but once it is, you will be able to download like so:
|
9
|
-
|
10
8
|
gem install time_log_robot
|
11
9
|
|
12
|
-
Until then...
|
13
|
-
|
14
|
-
rake install
|
15
|
-
|
16
10
|
## Usage
|
17
11
|
|
18
12
|
The simplest usage is just to invoke the robot:
|
data/bin/time_log_robot
CHANGED
@@ -5,6 +5,7 @@ require 'time_log_robot'
|
|
5
5
|
require 'time_log_robot/version'
|
6
6
|
require 'time_log_robot/toggl/tagger'
|
7
7
|
require 'time_log_robot/toggl/report'
|
8
|
+
require 'time_log_robot/jira/issue_key_parser'
|
8
9
|
require 'time_log_robot/jira/payload_builder'
|
9
10
|
require 'time_log_robot/jira/work_logger'
|
10
11
|
require 'yaml'
|
data/lib/time_log_robot.rb
CHANGED
@@ -8,14 +8,14 @@ module TimeLogRobot
|
|
8
8
|
|
9
9
|
def self.start(since)
|
10
10
|
time_entries = fetch_time_entries(since)
|
11
|
-
JIRA::WorkLogger.
|
11
|
+
JIRA::WorkLogger.log_all(time_entries: time_entries)
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.fetch_time_entries(since)
|
15
15
|
if since.nil?
|
16
|
-
Toggl::Report.
|
16
|
+
Toggl::Report.fetch
|
17
17
|
else
|
18
|
-
Toggl::Report.
|
18
|
+
Toggl::Report.fetch(since: since)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module TimeLogRobot
|
2
|
+
module JIRA
|
3
|
+
class IssueKeyParser
|
4
|
+
class << self
|
5
|
+
def parse(entry)
|
6
|
+
matches = entry['description'].match(/(\[(?<issue_key>[^\]]*)\])/)
|
7
|
+
if matches.present?
|
8
|
+
matches['issue_key']
|
9
|
+
else
|
10
|
+
get_key_from_key_mapping(entry['description'])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def get_key_from_key_mapping(description)
|
17
|
+
mappings = YAML.load_file(mapping_file_path) || {}
|
18
|
+
if found_key = mappings.keys.find { |key| /#{description}/ =~ key }
|
19
|
+
mappings[found_key]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def mapping_file_path
|
24
|
+
File.join(TimeLogRobot.root, 'mapping.yml')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,20 +1,14 @@
|
|
1
1
|
module TimeLogRobot
|
2
2
|
module JIRA
|
3
3
|
class PayloadBuilder
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def build
|
13
|
-
{
|
14
|
-
"comment" => comment,
|
15
|
-
"started" => start.to_s,
|
16
|
-
"timeSpentSeconds" => duration_in_seconds
|
17
|
-
}.to_json
|
4
|
+
class << self
|
5
|
+
def build(start:, duration_in_seconds:, comment:)
|
6
|
+
{
|
7
|
+
"comment" => comment,
|
8
|
+
"started" => start.to_s,
|
9
|
+
"timeSpentSeconds" => duration_in_seconds
|
10
|
+
}.to_json
|
11
|
+
end
|
18
12
|
end
|
19
13
|
end
|
20
14
|
end
|
@@ -7,93 +7,106 @@ module TimeLogRobot
|
|
7
7
|
|
8
8
|
base_uri 'https://hranswerlink.atlassian.net/rest/api/2'
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
class << self
|
11
|
+
def log_all(time_entries:)
|
12
|
+
time_entries.each do |entry|
|
13
|
+
log(entry) unless is_logged?(entry)
|
14
|
+
end
|
15
|
+
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
private
|
18
|
+
|
19
|
+
def username
|
20
|
+
ENV['JIRA_USERNAME']
|
20
21
|
end
|
21
|
-
end
|
22
22
|
|
23
|
-
|
23
|
+
def password
|
24
|
+
ENV['JIRA_PASSWORD']
|
25
|
+
end
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
|
27
|
+
def log_tags
|
28
|
+
[ENV['TOGGL_DEFAULT_LOG_TAG']]
|
29
|
+
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
payload = build_payload(entry)
|
32
|
-
puts "Attempting to log #{human_readable_duration(parse_duration(entry))}"
|
33
|
-
puts "starting on #{parse_start(entry)}"
|
34
|
-
puts "to #{entry['description']}"
|
35
|
-
puts "with comment #{parse_comment(entry)}" unless parse_comment(entry).nil?
|
36
|
-
response = self.class.post("/issue/#{issue_key}/worklog", basic_auth: auth, headers: headers, body: payload)
|
37
|
-
if response.success?
|
38
|
-
puts "Success"
|
39
|
-
puts '*' * 20
|
40
|
-
set_entry_as_logged(entry)
|
41
|
-
else
|
42
|
-
puts "Failed! Response from JIRA:"
|
43
|
-
puts response
|
44
|
-
puts "(Hint: Did you forget to put the JIRA issue key in your Toggl entry?"
|
45
|
-
puts '*' * 20
|
31
|
+
def is_logged?(entry)
|
32
|
+
(log_tags - entry['tags']).size < log_tags.size
|
46
33
|
end
|
47
|
-
end
|
48
34
|
|
49
|
-
|
50
|
-
|
51
|
-
|
35
|
+
def log(entry)
|
36
|
+
issue_key = JIRA::IssueKeyParser.parse(entry)
|
37
|
+
payload = build_payload(entry)
|
38
|
+
puts "Attempting to log #{human_readable_duration(parse_duration(entry))}"
|
39
|
+
puts "starting on #{parse_start(entry)}"
|
40
|
+
puts "to #{entry['description']}"
|
41
|
+
puts "issue key #{issue_key}"
|
42
|
+
puts "with comment #{parse_comment(entry)}" unless parse_comment(entry).nil?
|
43
|
+
response = post("/issue/#{issue_key}/worklog", basic_auth: auth, headers: headers, body: payload)
|
44
|
+
if response.success?
|
45
|
+
puts "Success"
|
46
|
+
puts '*' * 20
|
47
|
+
set_entry_as_logged(entry)
|
48
|
+
else
|
49
|
+
puts response.code
|
50
|
+
puts "Failed! Response from JIRA:"
|
51
|
+
if response.code == 401
|
52
|
+
raise UnauthorizedError, "Please check your username and password and try again"
|
53
|
+
elsif response.code == 404
|
54
|
+
puts "Not Found - Did you forget to put the JIRA issue key in your Toggl entry?"
|
55
|
+
end
|
56
|
+
puts '*' * 20
|
57
|
+
end
|
58
|
+
end
|
59
|
+
class UnauthorizedError < Exception; end
|
52
60
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
password: password
|
57
|
-
}
|
58
|
-
end
|
61
|
+
def set_entry_as_logged(entry)
|
62
|
+
Toggl::Tagger.update(entry_id: entry['id'])
|
63
|
+
end
|
59
64
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
65
|
+
def auth
|
66
|
+
{
|
67
|
+
username: username,
|
68
|
+
password: password
|
69
|
+
}
|
70
|
+
end
|
64
71
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
comment: parse_comment(entry)
|
70
|
-
).build
|
71
|
-
end
|
72
|
+
# @TODO Extract since it's used in several different models
|
73
|
+
def headers
|
74
|
+
{ 'Content-Type' => 'application/json' }
|
75
|
+
end
|
72
76
|
|
73
|
-
|
74
|
-
|
75
|
-
|
77
|
+
def build_payload(entry)
|
78
|
+
JIRA::PayloadBuilder.build(
|
79
|
+
start: parse_start(entry),
|
80
|
+
duration_in_seconds: parse_duration(entry),
|
81
|
+
comment: parse_comment(entry)
|
82
|
+
)
|
83
|
+
end
|
76
84
|
|
77
|
-
|
78
|
-
|
79
|
-
|
85
|
+
def parse_start(entry)
|
86
|
+
DateTime.strptime(entry['start'], "%FT%T%:z").strftime("%FT%T.%L%z")
|
87
|
+
end
|
80
88
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
remaining_minutes = total_minutes - hours * 60
|
85
|
-
"#{hours}h #{remaining_minutes}m"
|
86
|
-
end
|
89
|
+
def parse_duration(entry)
|
90
|
+
entry['dur']/1000 # Toggl sends times in milliseconds
|
91
|
+
end
|
87
92
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
+
def human_readable_duration(seconds)
|
94
|
+
total_minutes = seconds/60
|
95
|
+
hours = total_minutes/60
|
96
|
+
remaining_minutes = total_minutes - hours * 60
|
97
|
+
"#{hours}h #{remaining_minutes}m"
|
98
|
+
end
|
93
99
|
|
94
|
-
|
95
|
-
|
96
|
-
|
100
|
+
# @TODO figure out how to capture both of this in one .match call with one set of regex
|
101
|
+
def parse_issue_key(entry)
|
102
|
+
matches = entry['description'].match(/(\[(?<issue_key>[^\]]*)\])/)
|
103
|
+
matches['issue_key'] if matches.present?
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_comment(entry)
|
107
|
+
matches = entry['description'].match(/(\{(?<comment>[^\}]*)\})/)
|
108
|
+
matches['comment'] if matches.present?
|
109
|
+
end
|
97
110
|
end
|
98
111
|
end
|
99
112
|
end
|
@@ -5,44 +5,49 @@ module TimeLogRobot
|
|
5
5
|
class Report
|
6
6
|
include HTTParty
|
7
7
|
|
8
|
-
attr_accessor :token, :workspace_id, :user_agent
|
9
|
-
|
10
8
|
base_uri 'https://toggl.com/reports/api/v2'
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
class << self
|
11
|
+
def fetch(since: nil)
|
12
|
+
since ||= Date.today.beginning_of_week(:saturday).to_time
|
13
|
+
response = get('/details', basic_auth: auth, query: query(since))
|
14
|
+
if response.success?
|
15
|
+
response['data']
|
16
|
+
else
|
17
|
+
raise FetchError, response['error']
|
18
|
+
end
|
19
|
+
end
|
20
|
+
class FetchError < Exception; end
|
17
21
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
private
|
23
|
+
|
24
|
+
def auth
|
25
|
+
{
|
26
|
+
username: token,
|
27
|
+
password: "api_token"
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def token
|
32
|
+
ENV['TOGGL_TOKEN']
|
25
33
|
end
|
26
|
-
end
|
27
|
-
class FetchError < Exception; end
|
28
34
|
|
29
|
-
|
35
|
+
def workspace_id
|
36
|
+
ENV['TOGGL_WORKSPACE_ID']
|
37
|
+
end
|
30
38
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
password: "api_token"
|
35
|
-
}
|
36
|
-
end
|
39
|
+
def user_agent
|
40
|
+
ENV['TOGGL_USER_AGENT']
|
41
|
+
end
|
37
42
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
43
|
+
def query(since)
|
44
|
+
{
|
45
|
+
workspace_id: workspace_id,
|
46
|
+
user_agent: user_agent,
|
47
|
+
since: since
|
48
|
+
}
|
49
|
+
end
|
44
50
|
end
|
45
51
|
end
|
46
|
-
|
47
52
|
end
|
48
53
|
end
|
@@ -3,39 +3,42 @@ module TimeLogRobot
|
|
3
3
|
class Tagger
|
4
4
|
include HTTParty
|
5
5
|
|
6
|
-
attr_accessor :token, :tags
|
7
|
-
|
8
6
|
base_uri 'https://toggl.com/api/v8/time_entries'
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
class << self
|
9
|
+
def update(entry_id:)
|
10
|
+
put("/#{entry_id}", basic_auth: auth, headers: headers, body: body)
|
11
|
+
end
|
14
12
|
|
15
|
-
|
16
|
-
self.class.put("/#{entry_id}", basic_auth: auth, headers: headers, body: body)
|
17
|
-
end
|
13
|
+
private
|
18
14
|
|
19
|
-
|
15
|
+
def auth
|
16
|
+
{
|
17
|
+
username: token,
|
18
|
+
password: "api_token"
|
19
|
+
}
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
password: "api_token"
|
25
|
-
}
|
26
|
-
end
|
22
|
+
def token
|
23
|
+
ENV['TOGGL_TOKEN']
|
24
|
+
end
|
27
25
|
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
def headers
|
27
|
+
{ 'Content-Type' => 'application/json' }
|
28
|
+
end
|
31
29
|
|
32
|
-
|
33
|
-
{
|
34
|
-
time_entry:
|
30
|
+
def body
|
35
31
|
{
|
36
|
-
|
37
|
-
|
38
|
-
|
32
|
+
time_entry:
|
33
|
+
{
|
34
|
+
tags: tags
|
35
|
+
}
|
36
|
+
}.to_json
|
37
|
+
end
|
38
|
+
|
39
|
+
def tags
|
40
|
+
[ENV['TOGGL_DEFAULT_LOG_TAG']]
|
41
|
+
end
|
39
42
|
end
|
40
43
|
end
|
41
44
|
end
|
data/mapping.yml
ADDED
data/time_log_robot.gemspec
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
|
4
|
+
require 'time_log_robot/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = 'time_log_robot'
|
8
|
-
spec.version =
|
9
|
-
# spec.version = TimeLogRobot::VERSION
|
8
|
+
spec.version = TimeLogRobot::VERSION
|
10
9
|
spec.authors = ['Mark J. Lehman']
|
11
10
|
spec.email = ['markopolo@gmail.com']
|
12
11
|
spec.description = %q{Automate time logging from tools like Toggl to project management software such as JIRA}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: time_log_robot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark J. Lehman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-05-
|
11
|
+
date: 2016-05-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -136,11 +136,13 @@ files:
|
|
136
136
|
- Rakefile
|
137
137
|
- bin/time_log_robot
|
138
138
|
- lib/time_log_robot.rb
|
139
|
+
- lib/time_log_robot/jira/issue_key_parser.rb
|
139
140
|
- lib/time_log_robot/jira/payload_builder.rb
|
140
141
|
- lib/time_log_robot/jira/work_logger.rb
|
141
142
|
- lib/time_log_robot/toggl/report.rb
|
142
143
|
- lib/time_log_robot/toggl/tagger.rb
|
143
144
|
- lib/time_log_robot/version.rb
|
145
|
+
- mapping.yml
|
144
146
|
- time_log_robot.gemspec
|
145
147
|
homepage: https://github.com/supremebeing7/time_log_robot
|
146
148
|
licenses:
|