sfctl 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43943ce5d78b2cf30c6f6e171bd5ba8568adbce169ee8a9f66a64f1162b80ff8
4
- data.tar.gz: 8960aea77d4cd2d90170005d275625435497a6685a835729728523d62d81ee4a
3
+ metadata.gz: d0e63e770e0ea7a15b30d234c2f2b93adf1680ecacf25d0d3fa8af90522508c6
4
+ data.tar.gz: 0cd955de01130050c06e6e266700db3e0f5e0d429be255883b5df5cc30f770f7
5
5
  SHA512:
6
- metadata.gz: b48ecdfda4052022ad25b1e8008de8148c29e3a88e5b36b33261a597d58fcb49e6ee98b7d41c63443d01ed652d7c304e68a4622a119e707b1a8afe427f4271f2
7
- data.tar.gz: 332ce44447b8f1d2b020eda9b3f47c982a63a8bc4c119432446e93d101b19516fe34934e970dbfd5123cd3365fd4cb98d784b55e30f67011599095688d8e983f
6
+ metadata.gz: 58cafd72532a056a3fa7159c09aee07e0bd8d48a25e62d93bf3cb1848e77724e9c376ae910ab55e26c5dbb59dc1308b4761e349de6ae8e76e7d468bdeeb8efc5
7
+ data.tar.gz: 5150f1523bc6538d17a407c70c0537cb03826b8b6cef94b6f15f6683b710fe68f3abfe6c77c202a5f9b1ba9586a165ceaacffa7ea9f46e1958c8f8ef4cf8f645
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sfctl (0.0.4)
4
+ sfctl (0.1.0)
5
5
  faraday (~> 1.0)
6
6
  pastel (~> 0.7)
7
7
  rake (~> 12.0)
@@ -13,8 +13,10 @@ module Sfctl
13
13
  LINK_CONFIG_PATH = "#{Dir.pwd}/#{LINK_CONFIG_FILENAME}"
14
14
 
15
15
  TOGGL_PROVIDER = 'toggl'
16
+ HARVEST_PROVIDER = 'harvest'
16
17
  PROVIDERS_LIST = [
17
- TOGGL_PROVIDER
18
+ TOGGL_PROVIDER,
19
+ HARVEST_PROVIDER
18
20
  ].freeze
19
21
 
20
22
  def_delegators :command, :run
@@ -1,7 +1,7 @@
1
1
  require 'pastel'
2
2
  require 'tty-table'
3
3
  require_relative '../../command'
4
- require_relative '../../starfish'
4
+ require_relative '../../starfish/client'
5
5
 
6
6
  module Sfctl
7
7
  module Commands
@@ -15,7 +15,7 @@ module Sfctl
15
15
  def execute(output: $stdout)
16
16
  return unless config_present?(output)
17
17
 
18
- success, data = Starfish.account_assignments(@options['starfish-host'], @options['all'], access_token)
18
+ success, data = Starfish::Client.account_assignments(@options['starfish-host'], @options['all'], access_token)
19
19
 
20
20
  unless success
21
21
  output.puts @pastel.red('Something went wrong. Unable to fetch assignments')
@@ -1,7 +1,7 @@
1
1
  require 'pastel'
2
2
  require 'tty-table'
3
3
  require_relative '../../command'
4
- require_relative '../../starfish'
4
+ require_relative '../../starfish/client'
5
5
 
6
6
  module Sfctl
7
7
  module Commands
@@ -15,7 +15,7 @@ module Sfctl
15
15
  def execute(output: $stdout)
16
16
  return unless config_present?(output)
17
17
 
18
- success, info = Starfish.account_info(@options['starfish-host'], access_token)
18
+ success, info = Starfish::Client.account_info(@options['starfish-host'], access_token)
19
19
 
20
20
  unless success
21
21
  output.puts @pastel.red('Something went wrong. Unable to fetch account info')
@@ -1,5 +1,5 @@
1
1
  require_relative '../../command'
2
- require_relative '../../starfish'
2
+ require_relative '../../starfish/client'
3
3
  require 'pastel'
4
4
  require 'tty-prompt'
5
5
  require 'tty-spinner'
@@ -23,7 +23,7 @@ module Sfctl
23
23
  private
24
24
 
25
25
  def token_valid?(access_token)
26
- Starfish.check_authorization(@options['starfish-host'], access_token)
26
+ Starfish::Client.check_authorization(@options['starfish-host'], access_token)
27
27
  end
28
28
 
29
29
  def token_accepted_message
@@ -3,8 +3,9 @@ require 'pastel'
3
3
  require 'tty-spinner'
4
4
  require 'tty-prompt'
5
5
  require_relative '../../../command'
6
- require_relative '../../../starfish'
7
- require_relative '../../../toggl'
6
+ require_relative '../../../starfish/client'
7
+ require_relative '../../../toggl/client'
8
+ require_relative '../../../harvest/client'
8
9
 
9
10
  module Sfctl
10
11
  module Commands
@@ -22,7 +23,7 @@ module Sfctl
22
23
 
23
24
  ltoken = access_token
24
25
  config.delete(:access_token)
25
- success, data = Starfish.account_assignments(@options['starfish-host'], @options['all'], ltoken)
26
+ success, data = Starfish::Client.account_assignments(@options['starfish-host'], @options['all'], ltoken)
26
27
  unless success
27
28
  output.puts @pastel.red('Something went wrong. Unable to fetch assignments')
28
29
  return
@@ -38,14 +39,20 @@ module Sfctl
38
39
 
39
40
  assignment_obj = select_assignment(assignments)
40
41
 
42
+ setup_connection!(provider, output, assignment_obj)
43
+ end
44
+
45
+ private
46
+
47
+ def setup_connection!(provider, output, assignment_obj)
41
48
  case provider
42
49
  when TOGGL_PROVIDER
43
50
  setup_toggl_connection!(output, assignment_obj)
51
+ when HARVEST_PROVIDER
52
+ setup_harvest_connection!(output, assignment_obj)
44
53
  end
45
54
  end
46
55
 
47
- private
48
-
49
56
  def clear_conf_and_print_success!(output)
50
57
  delete_providers_from_link_config!
51
58
  save_link_config!
@@ -75,6 +82,14 @@ module Sfctl
75
82
  list
76
83
  end
77
84
 
85
+ def ask_for_billable
86
+ @prompt.select('Billable?', %w[yes no both])
87
+ end
88
+
89
+ def ask_for_rounding
90
+ @prompt.select('Rounding?', %w[on off])
91
+ end
92
+
78
93
  def setup_toggl_connection!(output, assignment_obj) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
79
94
  spinner = ::TTY::Spinner.new('[:spinner] Loading ...')
80
95
 
@@ -82,7 +97,7 @@ module Sfctl
82
97
  toggl_token = read_link_config['providers'][TOGGL_PROVIDER]['access_token']
83
98
 
84
99
  spinner.auto_spin
85
- _success, workspaces = Toggl.workspaces(toggl_token)
100
+ _success, workspaces = Toggl::Client.workspaces(toggl_token)
86
101
  spinner.pause
87
102
  output.puts
88
103
  workspace = @prompt.select('Please select Workspace:') do |menu|
@@ -93,7 +108,7 @@ module Sfctl
93
108
  workspace_id = workspace['id']
94
109
 
95
110
  spinner.resume
96
- _success, projects = Toggl.workspace_projects(toggl_token, workspace_id)
111
+ _success, projects = Toggl::Client.workspace_projects(toggl_token, workspace_id)
97
112
 
98
113
  if projects.nil? || projects.empty?
99
114
  spinner.stop
@@ -113,7 +128,7 @@ module Sfctl
113
128
  spinner.resume
114
129
  tasks_objs = []
115
130
  project_ids.each do |pj_id|
116
- _success, tasks = Toggl.project_tasks(toggl_token, pj_id)
131
+ _success, tasks = Toggl::Client.project_tasks(toggl_token, pj_id)
117
132
  tasks_objs << tasks
118
133
  end
119
134
  tasks_objs.flatten!
@@ -131,9 +146,9 @@ module Sfctl
131
146
  output.puts @pastel.yellow("You don't have tasks. Continue...")
132
147
  end
133
148
 
134
- billable = @prompt.select('Billable?', %w[yes no both])
149
+ billable = ask_for_billable
135
150
 
136
- rounding = @prompt.select('Rounding?', %w[on off])
151
+ rounding = ask_for_rounding
137
152
 
138
153
  config.set("connections.#{assignment_id}.name", value: assignment_obj['name'])
139
154
  config.set("connections.#{assignment_id}.service", value: assignment_obj['service'])
@@ -146,6 +161,66 @@ module Sfctl
146
161
 
147
162
  clear_conf_and_print_success!(output)
148
163
  end
164
+
165
+ def setup_harvest_connection!(output, assignment_obj) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
166
+ spinner = ::TTY::Spinner.new('[:spinner] Loading ...')
167
+
168
+ assignment_id = assignment_obj['id']
169
+ harvest_account_id = read_link_config['providers'][HARVEST_PROVIDER]['account_id']
170
+ harvest_token = read_link_config['providers'][HARVEST_PROVIDER]['access_token']
171
+
172
+ spinner.auto_spin
173
+ _success, projects = Harvest::Client.projects(harvest_account_id, harvest_token)
174
+
175
+ if projects.nil? || projects.empty?
176
+ spinner.stop
177
+ error_message = "There is no projects. Please visit #{HARVEST_PROVIDER} and create them before continue."
178
+ output.puts @pastel.red(error_message)
179
+ return
180
+ end
181
+
182
+ spinner.pause
183
+ output.puts
184
+ project = @prompt.select('Please select Project:') do |menu|
185
+ projects.each do |pj|
186
+ menu.choice name: pj['name'], value: pj
187
+ end
188
+ end
189
+ project_id = project['id']
190
+
191
+ spinner.resume
192
+ _success, tasks = Harvest::Client.tasks(harvest_account_id, harvest_token)
193
+
194
+ if tasks.nil? || tasks.empty?
195
+ spinner.stop
196
+ error_message = "There is no tasks. Please visit #{HARVEST_PROVIDER} and create them before continue."
197
+ output.puts @pastel.red(error_message)
198
+ return
199
+ end
200
+
201
+ spinner.success
202
+ output.puts
203
+ task = @prompt.select('Please select Task:') do |menu|
204
+ tasks.each do |t|
205
+ menu.choice name: t['name'], value: t
206
+ end
207
+ end
208
+ task_id = task['id']
209
+
210
+ billable = ask_for_billable
211
+
212
+ rounding = ask_for_rounding
213
+
214
+ config.set("connections.#{assignment_id}.name", value: assignment_obj['name'])
215
+ config.set("connections.#{assignment_id}.service", value: assignment_obj['service'])
216
+ config.set("connections.#{assignment_id}.provider", value: HARVEST_PROVIDER)
217
+ config.set("connections.#{assignment_id}.project_id", value: project_id.to_s)
218
+ config.set("connections.#{assignment_id}.task_id", value: task_id.to_s)
219
+ config.set("connections.#{assignment_id}.billable", value: billable)
220
+ config.set("connections.#{assignment_id}.rounding", value: rounding)
221
+
222
+ clear_conf_and_print_success!(output)
223
+ end
149
224
  end
150
225
  end
151
226
  end
@@ -29,23 +29,41 @@ module Sfctl
29
29
 
30
30
  def print_connections(output)
31
31
  config.fetch(:connections).each_key do |assignment_id|
32
+ print_header!(output, assignment_id)
33
+
32
34
  case config.fetch(:connections, assignment_id, :provider)
33
35
  when TOGGL_PROVIDER
34
36
  print_toggl_connection!(output, assignment_id)
37
+ when HARVEST_PROVIDER
38
+ print_harvest_connection!(output, assignment_id)
35
39
  end
40
+
41
+ print_footer!(output, assignment_id)
36
42
  end
37
43
  end
38
44
 
39
- def print_toggl_connection!(output, assignment_id) # rubocop:disable Metrics/AbcSize
45
+ def print_header!(output, assignment_id)
40
46
  output.puts "Connection: #{config.fetch(:connections, assignment_id, :name)}"
41
47
  output.puts " service: #{config.fetch(:connections, assignment_id, :service)}"
48
+ end
49
+
50
+ def print_footer!(output, assignment_id)
51
+ output.puts " billable: #{config.fetch(:connections, assignment_id, :billable)}"
52
+ output.puts " rounding: #{config.fetch(:connections, assignment_id, :rounding)}"
53
+ output.puts
54
+ end
55
+
56
+ def print_toggl_connection!(output, assignment_id)
42
57
  output.puts " provider: #{TOGGL_PROVIDER}"
43
58
  output.puts " workspace_id: #{config.fetch(:connections, assignment_id, :workspace_id)}"
44
59
  output.puts " project_ids: #{config.fetch(:connections, assignment_id, :project_ids)}"
45
60
  output.puts " task_ids: #{config.fetch(:connections, assignment_id, :task_ids)}"
46
- output.puts " billable: #{config.fetch(:connections, assignment_id, :billable)}"
47
- output.puts " rounding: #{config.fetch(:connections, assignment_id, :rounding)}"
48
- output.puts
61
+ end
62
+
63
+ def print_harvest_connection!(output, assignment_id)
64
+ output.puts " provider: #{HARVEST_PROVIDER}"
65
+ output.puts " project_id: #{config.fetch(:connections, assignment_id, :project_id)}"
66
+ output.puts " task_id: #{config.fetch(:connections, assignment_id, :task_id)}"
49
67
  end
50
68
  end
51
69
  end
@@ -28,7 +28,7 @@ module Sfctl
28
28
  else
29
29
  output.puts "Provider: #{@pastel.cyan(provider)}"
30
30
  info.each_key do |k|
31
- output.puts " #{k.upcase}: #{info[k]}"
31
+ output.puts " #{k.upcase}: #{@pastel.magenta(info[k])}"
32
32
  end
33
33
  end
34
34
  end
@@ -18,11 +18,13 @@ module Sfctl
18
18
  prompt = ::TTY::Prompt.new
19
19
  provider = prompt.select('Setting up:', PROVIDERS_LIST)
20
20
 
21
- !ask_for_replace(output, prompt) && return unless config.fetch("providers.#{TOGGL_PROVIDER}").nil?
21
+ !ask_for_replace(output, prompt) && return unless config.fetch("providers.#{provider}").nil?
22
22
 
23
23
  case provider
24
24
  when TOGGL_PROVIDER
25
25
  setup_toggl_provider!(output, prompt)
26
+ when HARVEST_PROVIDER
27
+ setup_harvest_provider!(output, prompt)
26
28
  end
27
29
  end
28
30
 
@@ -33,22 +35,46 @@ module Sfctl
33
35
  prompt.yes?('Do you want to replace it?')
34
36
  end
35
37
 
36
- def save_toggl_config!(output, access_token)
37
- config.set("providers.#{TOGGL_PROVIDER}.access_token", value: access_token)
38
+ def correct?(prompt)
39
+ prompt.yes?('Is that information correct?')
40
+ end
41
+
42
+ def save_config_and_print_message!(output)
38
43
  save_config!
39
44
  output.puts @pastel.green('Everything saved.')
40
45
  end
41
46
 
47
+ def save_toggl_config!(output, access_token)
48
+ config.set("providers.#{TOGGL_PROVIDER}.access_token", value: access_token)
49
+ save_config_and_print_message!(output)
50
+ end
51
+
42
52
  def setup_toggl_provider!(output, prompt)
43
53
  output.puts
44
54
  access_token = prompt.ask("Your access token at [#{@pastel.green(TOGGL_PROVIDER)}]:", required: true)
45
- is_correct = prompt.yes?('Is that information correct?')
46
- if is_correct
55
+ if correct?(prompt)
47
56
  save_toggl_config!(output, access_token)
48
57
  else
49
58
  setup_toggl_provider!(output, prompt)
50
59
  end
51
60
  end
61
+
62
+ def save_harvest_config!(output, account_id, access_token)
63
+ config.set("providers.#{HARVEST_PROVIDER}.account_id", value: account_id)
64
+ config.set("providers.#{HARVEST_PROVIDER}.access_token", value: access_token)
65
+ save_config_and_print_message!(output)
66
+ end
67
+
68
+ def setup_harvest_provider!(output, prompt)
69
+ output.puts
70
+ account_id = prompt.ask("Your Account ID at [#{@pastel.green(HARVEST_PROVIDER)}]:", required: true)
71
+ access_token = prompt.ask("Your Token at [#{@pastel.green(HARVEST_PROVIDER)}]:", required: true)
72
+ if correct?(prompt)
73
+ save_harvest_config!(output, account_id, access_token)
74
+ else
75
+ setup_harvest_provider!(output, prompt)
76
+ end
77
+ end
52
78
  end
53
79
  end
54
80
  end
@@ -4,8 +4,9 @@ require 'tty-prompt'
4
4
  require 'tty-spinner'
5
5
  require 'tty-table'
6
6
  require_relative '../../command'
7
- require_relative '../../starfish'
8
- require_relative '../../toggl'
7
+ require_relative '../../starfish/client'
8
+ require_relative '../../toggl/sync'
9
+ require_relative '../../harvest/sync'
9
10
 
10
11
  module Sfctl
11
12
  module Commands
@@ -70,37 +71,46 @@ module Sfctl
70
71
  list.each do |assignment|
71
72
  assignment_id = assignment['id'].to_s
72
73
  connection = read_link_config['connections'].select { |c| c == assignment_id }
73
-
74
- if connection.empty?
75
- output.puts @pastel.red("Unable to find a connection for assignment \"#{assignment['name']}\"")
76
- next
77
- end
78
-
79
74
  sync(output, assignment, connection[assignment_id])
80
75
  end
81
76
  end
82
77
 
83
- def sync(output, assignment, connection)
84
- case connection['provider']
85
- when TOGGL_PROVIDER
86
- sync_with_toggl!(output, assignment, connection)
87
- end
88
- end
89
-
90
- def sync_with_toggl!(output, assignment, connection) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
78
+ def sync(output, assignment, connection) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
91
79
  output.puts "Synchronizing: #{@pastel.cyan("[#{assignment['name']} / #{assignment['service']}]")}"
92
80
 
93
- success, next_report = Starfish.next_report(@options['starfish-host'], access_token, assignment['id'])
81
+ success, next_report = Starfish::Client.next_report(@options['starfish-host'], access_token, assignment['id'])
94
82
 
95
83
  print_no_next_reporting_segment(output) && return if !success || next_report.empty?
96
84
 
97
- time_entries = load_data_from_toggl(output, next_report, connection)
85
+ time_entries = load_time_entries(output, next_report, connection)
98
86
 
99
87
  print_dry_run_enabled(output) && return if @options['dry_run']
100
88
 
101
89
  print_report_contains_data(output, next_report) && return if touchy?(next_report)
102
90
 
103
- uploading_to_starfish(output, assignment, time_entries)
91
+ uploading_to_starfish(output, assignment, time_entries, connection)
92
+ end
93
+
94
+ def report_interval(record)
95
+ start_date = Date.parse("#{record['year']}-#{record['month']}-01")
96
+ end_date = start_date.next_month.prev_day
97
+ [start_date, end_date]
98
+ end
99
+
100
+ def load_time_entries(output, next_report, connection)
101
+ output.puts "Next Report: #{@pastel.cyan(report_name(next_report))}"
102
+ next_report_interval = report_interval(next_report)
103
+
104
+ case connection['provider']
105
+ when TOGGL_PROVIDER
106
+ Toggl::Sync.load_data(
107
+ output, connection, read_link_config['providers'][TOGGL_PROVIDER], @pastel, next_report_interval
108
+ )
109
+ when HARVEST_PROVIDER
110
+ Harvest::Sync.load_data(
111
+ output, connection, read_link_config['providers'][HARVEST_PROVIDER], @pastel, next_report_interval
112
+ )
113
+ end
104
114
  end
105
115
 
106
116
  def touchy?(next_report)
@@ -133,93 +143,21 @@ module Sfctl
133
143
  true
134
144
  end
135
145
 
136
- def load_data_from_toggl(output, next_report, connection)
137
- output.puts "Next Report: #{@pastel.cyan(report_name(next_report))}"
138
-
139
- spinner = TTY::Spinner.new("Loaded data from #{TOGGL_PROVIDER}: [:spinner]", format: :dots)
140
- spinner.auto_spin
141
-
142
- time_entries = get_toggle_time_entries(next_report, connection)
143
-
144
- spinner.success(@pastel.green('Done'))
145
-
146
- table = TTY::Table.new %w[Date Comment Time], time_entries_table_rows(time_entries)
147
- output.puts
148
- output.print table.render(:unicode, padding: [0, 1], alignments: %i[left left right])
149
- output.puts
150
- output.puts
151
-
152
- time_entries['data']
153
- end
154
-
155
- def time_entries_table_rows(time_entries)
156
- rows = time_entries['data'].sort_by { |te| te['start'] }.map do |te|
157
- [
158
- Date.parse(te['start']).to_s,
159
- te['description'],
160
- "#{humanize_duration(te['dur'])}h"
161
- ]
162
- end
163
- rows.push(['Total:', '', "#{humanize_duration(time_entries['total_grand'])}h"])
164
- rows
165
- end
166
-
167
- def get_toggle_time_entries(next_report, connection)
168
- _success, data = Toggl.time_entries(
169
- read_link_config['providers'][TOGGL_PROVIDER]['access_token'], time_entries_params(next_report, connection)
170
- )
171
-
172
- data
173
- end
174
-
175
- def time_entries_params(next_report, connection)
176
- start_date = Date.parse("#{next_report['year']}-#{next_report['month']}-01")
177
- end_date = start_date.next_month.prev_day
178
- params = {
179
- workspace_id: connection['workspace_id'],
180
- project_ids: connection['project_ids'],
181
- billable: connection['billable'],
182
- rounding: connection['rounding'],
183
- since: start_date.to_s,
184
- until: end_date.to_s
185
- }
186
- params[:task_ids] = connection['task_ids'] if connection['task_ids'].length.positive?
187
- params
188
- end
189
-
190
- def humanize_duration(milliseconds)
191
- return '0' if milliseconds.nil?
192
-
193
- seconds = milliseconds / 1000
194
- minutes = seconds / 60
195
- int = (minutes / 60).ceil
196
- dec = minutes % 60
197
- amount = (dec * 100) / 60
198
- amount = if dec.zero?
199
- ''
200
- elsif amount.to_s.length == 1
201
- ".0#{amount}"
202
- else
203
- ".#{amount}"
204
- end
205
- "#{int}#{amount}"
206
- end
207
-
208
- def assignment_items(time_entries)
209
- time_entries.map do |te|
210
- {
211
- time: humanize_duration(te['dur']).to_f,
212
- date: Date.parse(te['start']).to_s,
213
- comment: te['description']
214
- }
146
+ def assignment_items(time_entries, connection)
147
+ case connection['provider']
148
+ when TOGGL_PROVIDER
149
+ Toggl::Sync.assignment_items(time_entries)
150
+ when HARVEST_PROVIDER
151
+ Harvest::Sync.assignment_items(time_entries, connection)
215
152
  end
216
153
  end
217
154
 
218
- def uploading_to_starfish(output, assignment, time_entries)
155
+ def uploading_to_starfish(output, assignment, time_entries, connection)
219
156
  spinner = TTY::Spinner.new('Uploading to starfish.team: [:spinner]', format: :dots)
220
157
  spinner.auto_spin
221
- success = Starfish.update_next_report(
222
- @options['starfish-host'], access_token, assignment['id'], assignment_items(time_entries)
158
+
159
+ success = Starfish::Client.update_next_report(
160
+ @options['starfish-host'], access_token, assignment['id'], assignment_items(time_entries, connection)
223
161
  )
224
162
  print_upload_results(output, success, spinner)
225
163
  end
@@ -0,0 +1,44 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module Sfctl
5
+ module Harvest
6
+ module Client
7
+ API_V2_PATH = 'api/v2/'.freeze
8
+
9
+ def self.conn(account_id, token)
10
+ raise 'Please set Harvest provider before continue.' if account_id.nil? || token.nil?
11
+
12
+ headers = {
13
+ 'Content-Type' => 'application/json',
14
+ 'Harvest-Account-ID' => account_id,
15
+ 'Authorization' => "Bearer #{token}"
16
+ }
17
+
18
+ Faraday.new(url: "https://api.harvestapp.com/#{API_V2_PATH}", headers: headers) do |builder|
19
+ builder.request :retry
20
+ builder.adapter :net_http
21
+ end
22
+ end
23
+
24
+ def self.parsed_response(response, key)
25
+ [response.status == 200, JSON.parse(response.body)[key]]
26
+ end
27
+
28
+ def self.projects(account_id, token)
29
+ response = conn(account_id, token).get('projects')
30
+ parsed_response(response, 'projects')
31
+ end
32
+
33
+ def self.tasks(account_id, token)
34
+ response = conn(account_id, token).get('tasks')
35
+ parsed_response(response, 'tasks')
36
+ end
37
+
38
+ def self.time_entries(account_id, token, params)
39
+ response = conn(account_id, token).get('time_entries', params)
40
+ parsed_response(response, 'time_entries')
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ require 'tty-spinner'
2
+ require 'tty-table'
3
+ require_relative '../command'
4
+ require_relative './client'
5
+
6
+ module Sfctl
7
+ module Harvest
8
+ module Sync
9
+ def self.load_data(output, connection, harvest_config, pastel, report_interval)
10
+ spinner = TTY::Spinner.new("Loaded data from #{Sfctl::Command::HARVEST_PROVIDER}: [:spinner]", format: :dots)
11
+ spinner.auto_spin
12
+
13
+ time_entries = get_time_entries(connection, harvest_config, report_interval)
14
+
15
+ spinner.success(pastel.green('Done'))
16
+
17
+ table = TTY::Table.new %w[Date Comment Time], time_entries_table_rows(time_entries, connection)
18
+ output.puts
19
+ output.print table.render(:unicode, padding: [0, 1], alignments: %i[left left right])
20
+ output.puts
21
+ output.puts
22
+
23
+ time_entries
24
+ end
25
+
26
+ def self.get_time_entries(connection, harvest_config, report_interval)
27
+ _success, data = Harvest::Client.time_entries(
28
+ harvest_config['account_id'],
29
+ harvest_config['access_token'],
30
+ time_entries_params(connection, report_interval)
31
+ )
32
+
33
+ data
34
+ end
35
+
36
+ def self.hours_field(rounding)
37
+ return 'rounded_hours' if rounding == 'on'
38
+
39
+ 'hours'
40
+ end
41
+
42
+ def self.time_entries_table_rows(time_entries, connection)
43
+ hours_field = hours_field(connection['rounding'])
44
+ rows = time_entries.sort_by { |te| te['spent_date'] }.map do |te|
45
+ [
46
+ te['spent_date'],
47
+ te['notes'],
48
+ "#{te[hours_field]}h"
49
+ ]
50
+ end
51
+ total_grand = time_entries.map { |te| te[hours_field] }.sum
52
+ rows.push(['Total:', '', "#{total_grand}h"])
53
+ rows
54
+ end
55
+
56
+ def self.time_entries_params(connection, report_interval)
57
+ start_date, end_date = report_interval
58
+ params = {
59
+ project_id: connection['project_id'],
60
+ task_id: connection['task_id'],
61
+ from: start_date.to_s,
62
+ to: end_date.to_s
63
+ }
64
+ params[:is_billed] = connection['billable'] == 'yes' unless connection['billable'] == 'both'
65
+ params
66
+ end
67
+
68
+ def self.assignment_items(time_entries, connection)
69
+ hours_field = hours_field(connection['rounding'])
70
+ time_entries.map do |te|
71
+ {
72
+ time: te[hours_field],
73
+ date: te['spent_date'].to_s,
74
+ comment: te['notes']
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,53 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module Sfctl
5
+ module Starfish
6
+ module Client
7
+ def self.conn(endpoint, token)
8
+ raise 'Before continue please pass endpoint and token.' if endpoint.nil? || token.nil?
9
+
10
+ headers = {
11
+ 'Content-Type' => 'application/json',
12
+ 'X-Starfish-Auth' => token
13
+ }
14
+ Faraday.new(url: "#{endpoint}/api/v1", headers: headers) do |builder|
15
+ builder.request :retry
16
+ builder.adapter :net_http
17
+ end
18
+ end
19
+
20
+ def self.parsed_response(response)
21
+ [response.status == 200, JSON.parse(response.body)]
22
+ end
23
+
24
+ def self.check_authorization(endpoint, token)
25
+ response = conn(endpoint, token).get('profile')
26
+ response.status == 200
27
+ end
28
+
29
+ def self.account_info(endpoint, token)
30
+ response = conn(endpoint, token).get('profile')
31
+ parsed_response(response)
32
+ end
33
+
34
+ def self.account_assignments(endpoint, all, token)
35
+ api_conn = conn(endpoint, token)
36
+ response = all ? api_conn.get('assignments?all=1') : api_conn.get('assignments')
37
+ parsed_response(response)
38
+ end
39
+
40
+ def self.next_report(endpoint, token, assignment_id)
41
+ api_conn = conn(endpoint, token)
42
+ response = api_conn.get("assignments/#{assignment_id}/next_report")
43
+ parsed_response(response)
44
+ end
45
+
46
+ def self.update_next_report(endpoint, token, assignment_id, items)
47
+ api_conn = conn(endpoint, token)
48
+ response = api_conn.put("assignments/#{assignment_id}/next_report", JSON.generate(items: items))
49
+ response.status == 204
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module Sfctl
5
+ module Toggl
6
+ module Client
7
+ DEFAULT_API_PATH = 'api/v8/'.freeze
8
+ REPORTS_API_PATH = 'reports/api/v2/'.freeze
9
+
10
+ def self.conn(token, api = 'default')
11
+ raise 'Please set toggl provider before continue.' if token.nil?
12
+
13
+ api_path = api == 'reports' ? REPORTS_API_PATH : DEFAULT_API_PATH
14
+
15
+ headers = { 'Content-Type' => 'application/json' }
16
+ Faraday.new(url: "https://#{token}:api_token@www.toggl.com/#{api_path}", headers: headers) do |builder|
17
+ builder.request :retry
18
+ builder.adapter :net_http
19
+ end
20
+ end
21
+
22
+ def self.parsed_response(response)
23
+ [response.status == 200, JSON.parse(response.body)]
24
+ end
25
+
26
+ def self.workspaces(token)
27
+ response = conn(token).get('workspaces')
28
+ parsed_response(response)
29
+ end
30
+
31
+ def self.workspace_projects(token, workspace_id)
32
+ response = conn(token).get("workspaces/#{workspace_id}/projects")
33
+ parsed_response(response)
34
+ end
35
+
36
+ def self.project_tasks(token, project_id)
37
+ response = conn(token).get("workspaces/#{project_id}/tasks")
38
+
39
+ return [] if response.body.length.zero?
40
+
41
+ parsed_response(response)
42
+ end
43
+
44
+ def self.time_entries(token, params)
45
+ params[:user_agent] = 'api_test'
46
+ response = conn(token, 'reports').get('details', params)
47
+ parsed_response(response)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,90 @@
1
+ require 'tty-spinner'
2
+ require 'tty-table'
3
+ require_relative '../command'
4
+ require_relative './client'
5
+
6
+ module Sfctl
7
+ module Toggl
8
+ module Sync
9
+ def self.load_data(output, connection, toggl_config, pastel, report_interval)
10
+ spinner = TTY::Spinner.new("Loaded data from #{Sfctl::Command::TOGGL_PROVIDER}: [:spinner]", format: :dots)
11
+ spinner.auto_spin
12
+
13
+ time_entries = get_time_entries(connection, toggl_config, report_interval)
14
+
15
+ spinner.success(pastel.green('Done'))
16
+
17
+ table = TTY::Table.new %w[Date Comment Time], time_entries_table_rows(time_entries)
18
+ output.puts
19
+ output.print table.render(:unicode, padding: [0, 1], alignments: %i[left left right])
20
+ output.puts
21
+ output.puts
22
+
23
+ time_entries['data']
24
+ end
25
+
26
+ def self.time_entries_table_rows(time_entries)
27
+ rows = time_entries['data'].sort_by { |te| te['start'] }.map do |te|
28
+ [
29
+ Date.parse(te['start']).to_s,
30
+ te['description'],
31
+ "#{humanize_duration(te['dur'])}h"
32
+ ]
33
+ end
34
+ rows.push(['Total:', '', "#{humanize_duration(time_entries['total_grand'])}h"])
35
+ rows
36
+ end
37
+
38
+ def self.get_time_entries(connection, toggl_config, report_interval)
39
+ _success, data = Toggl::Client.time_entries(
40
+ toggl_config['access_token'],
41
+ time_entries_params(connection, report_interval)
42
+ )
43
+
44
+ data
45
+ end
46
+
47
+ def self.time_entries_params(connection, report_interval)
48
+ start_date, end_date = report_interval
49
+ params = {
50
+ workspace_id: connection['workspace_id'],
51
+ project_ids: connection['project_ids'],
52
+ billable: connection['billable'],
53
+ rounding: connection['rounding'],
54
+ since: start_date.to_s,
55
+ until: end_date.to_s
56
+ }
57
+ params[:task_ids] = connection['task_ids'] if connection['task_ids'].length.positive?
58
+ params
59
+ end
60
+
61
+ def self.humanize_duration(milliseconds)
62
+ return '0' if milliseconds.nil?
63
+
64
+ seconds = milliseconds / 1000
65
+ minutes = seconds / 60
66
+ int = (minutes / 60).ceil
67
+ dec = minutes % 60
68
+ amount = (dec * 100) / 60
69
+ amount = if dec.zero?
70
+ ''
71
+ elsif amount.to_s.length == 1
72
+ ".0#{amount}"
73
+ else
74
+ ".#{amount}"
75
+ end
76
+ "#{int}#{amount}"
77
+ end
78
+
79
+ def self.assignment_items(time_entries)
80
+ time_entries.map do |te|
81
+ {
82
+ time: humanize_duration(te['dur']).to_f,
83
+ date: Date.parse(te['start']).to_s,
84
+ comment: te['description']
85
+ }
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,3 +1,3 @@
1
1
  module Sfctl
2
- VERSION = '0.0.4'.freeze
2
+ VERSION = '0.1.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sfctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Serhii Rudik
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-04-23 00:00:00.000000000 Z
12
+ date: 2020-05-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
@@ -417,9 +417,12 @@ files:
417
417
  - lib/sfctl/commands/time/providers/set.rb
418
418
  - lib/sfctl/commands/time/providers/unset.rb
419
419
  - lib/sfctl/commands/time/sync.rb
420
- - lib/sfctl/starfish.rb
420
+ - lib/sfctl/harvest/client.rb
421
+ - lib/sfctl/harvest/sync.rb
422
+ - lib/sfctl/starfish/client.rb
421
423
  - lib/sfctl/templates/.gitkeep
422
- - lib/sfctl/toggl.rb
424
+ - lib/sfctl/toggl/client.rb
425
+ - lib/sfctl/toggl/sync.rb
423
426
  - lib/sfctl/version.rb
424
427
  - scripts/test.sh
425
428
  - sfctl.gemspec
@@ -1,51 +0,0 @@
1
- require 'faraday'
2
- require 'json'
3
-
4
- module Sfctl
5
- module Starfish
6
- def self.conn(endpoint, token)
7
- raise 'Before continue please pass endpoint and token.' if endpoint.nil? || token.nil?
8
-
9
- headers = {
10
- 'Content-Type' => 'application/json',
11
- 'X-Starfish-Auth' => token
12
- }
13
- Faraday.new(url: "#{endpoint}/api/v1", headers: headers) do |builder|
14
- builder.request :retry
15
- builder.adapter :net_http
16
- end
17
- end
18
-
19
- def self.parsed_response(response)
20
- [response.status == 200, JSON.parse(response.body)]
21
- end
22
-
23
- def self.check_authorization(endpoint, token)
24
- response = conn(endpoint, token).get('profile')
25
- response.status == 200
26
- end
27
-
28
- def self.account_info(endpoint, token)
29
- response = conn(endpoint, token).get('profile')
30
- parsed_response(response)
31
- end
32
-
33
- def self.account_assignments(endpoint, all, token)
34
- api_conn = conn(endpoint, token)
35
- response = all ? api_conn.get('assignments?all=1') : api_conn.get('assignments')
36
- parsed_response(response)
37
- end
38
-
39
- def self.next_report(endpoint, token, assignment_id)
40
- api_conn = conn(endpoint, token)
41
- response = api_conn.get("assignments/#{assignment_id}/next_report")
42
- parsed_response(response)
43
- end
44
-
45
- def self.update_next_report(endpoint, token, assignment_id, items)
46
- api_conn = conn(endpoint, token)
47
- response = api_conn.put("assignments/#{assignment_id}/next_report", JSON.generate(items: items))
48
- response.status == 204
49
- end
50
- end
51
- end
@@ -1,49 +0,0 @@
1
- require 'faraday'
2
- require 'json'
3
-
4
- module Sfctl
5
- module Toggl
6
- DEFAULT_API_PATH = 'api/v8/'.freeze
7
- REPORTS_API_PATH = 'reports/api/v2/'.freeze
8
-
9
- def self.conn(token, api = 'default')
10
- raise 'Please set toggl provider before continue.' if token.nil?
11
-
12
- api_path = api == 'reports' ? REPORTS_API_PATH : DEFAULT_API_PATH
13
-
14
- headers = { 'Content-Type' => 'application/json' }
15
- Faraday.new(url: "https://#{token}:api_token@www.toggl.com/#{api_path}", headers: headers) do |builder|
16
- builder.request :retry
17
- builder.adapter :net_http
18
- end
19
- end
20
-
21
- def self.parsed_response(response)
22
- [response.status == 200, JSON.parse(response.body)]
23
- end
24
-
25
- def self.workspaces(token)
26
- response = conn(token).get('workspaces')
27
- parsed_response(response)
28
- end
29
-
30
- def self.workspace_projects(token, workspace_id)
31
- response = conn(token).get("workspaces/#{workspace_id}/projects")
32
- parsed_response(response)
33
- end
34
-
35
- def self.project_tasks(token, project_id)
36
- response = conn(token).get("workspaces/#{project_id}/tasks")
37
-
38
- return [] if response.body.length.zero?
39
-
40
- parsed_response(response)
41
- end
42
-
43
- def self.time_entries(token, params)
44
- params[:user_agent] = 'api_test'
45
- response = conn(token, 'reports').get('details', params)
46
- parsed_response(response)
47
- end
48
- end
49
- end