sfctl 0.0.4 → 0.1.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 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