sfctl 0.0.3 → 1.0.2

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: f1109b58130d490175bb38ccb1db428be1c02d377b8faf0f509a783468028f68
4
- data.tar.gz: 3e1b593dac5dcea2aa4cba66d4a918b375db86cd91a491be58b2254034295e60
3
+ metadata.gz: 30138753cd7927ab7010451f88efb8845d5f8b3034db46cefaafd15fc36c2795
4
+ data.tar.gz: 3be71cf7f62719cd3c56d82c1c4741872bf48c341f047c986756f8ec76c84277
5
5
  SHA512:
6
- metadata.gz: 3db0dae715ec0afafc5aaa6439025f41d9f52433298d27161686ef1fe8d4ecc66b04fef0b2662694796506f8c92d7f752829148ad8d8687c59eb631a821e8e6f
7
- data.tar.gz: 7391aa72c018bb25f28a1aa999e72892a9412d9e9964c8903954382a19148f31164ebf9d677995ea08536a2addba70315e162bb0bf3d77bcc329e07e0bfcf05d
6
+ metadata.gz: ed03923a889a0b07dcc31e155e54a67152b6b918f8f3e4eb3baac97e44f1cf1ed37ee6e633142076418d4bc2b4721eafe181fae01d712d8f43e49e2df2c75eb3
7
+ data.tar.gz: 22afbfcb272debee7d7d716ab80d8ea22a66da59316d0175932639134073eb0b1806a54cd026cf442f777561bc6da1f7386c54b1ef72e60fc2b7faeb1026b136
data/.gitignore CHANGED
@@ -12,3 +12,4 @@
12
12
  .DS_Store
13
13
  sfctl-*.gem
14
14
  /.sflink
15
+ .byebug_history
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sfctl (0.0.2)
4
+ sfctl (1.0.1)
5
5
  faraday (~> 1.0)
6
6
  pastel (~> 0.7)
7
7
  rake (~> 12.0)
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # sfctl [![Build status](https://badge.buildkite.com/22ecc67f358163f4714383ff0fde8e847d1e3ae488fc10312f.svg)](https://buildkite.com/starfish/sf-control)
2
+ [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Falphatier-works%2Fsfctl.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Falphatier-works%2Fsfctl?ref=badge_shield)
2
3
 
3
4
  ```
4
5
  sfctl is a command line interface for the Starfish API.
@@ -270,3 +271,7 @@ Uploading to starfish.team: [IN PROGRESS|DONE]
270
271
 
271
272
  [NEXT CONNECTION]
272
273
  ```
274
+
275
+
276
+ ## License
277
+ [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Falphatier-works%2Fsfctl.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Falphatier-works%2Fsfctl?ref=badge_large)
@@ -21,7 +21,7 @@ module Sfctl
21
21
  end
22
22
 
23
23
  class_option :"no-color", type: :boolean, default: false, desc: 'Disable colorization in output'
24
- class_option :"starfish-host", type: :string, default: 'https://starfish.team',
24
+ class_option :"starfish-host", type: :string, default: 'https://app.starfish.team',
25
25
  desc: 'The starfish API endpoint',
26
26
  banner: 'HOST'
27
27
 
@@ -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
@@ -12,6 +12,8 @@ module Sfctl
12
12
  desc: 'Check the data first respectively prevent data from being overwritten'
13
13
  method_option :touchy, aliases: '-touchy', type: :boolean, default: false,
14
14
  desc: 'The synchronizsation will be skipped if there is preexisting data.'
15
+ method_option :all, aliases: '-all', type: :boolean, default: false,
16
+ desc: 'Skip selecting assignments and sync all of them.'
15
17
  long_desc <<~HEREDOC
16
18
  It will gets for each assignment the next reporting segment from starfish.team
17
19
  and loads the corresponding time reports from the provider.
@@ -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
@@ -20,7 +21,7 @@ module Sfctl
20
21
  def execute(output: $stdout)
21
22
  return if !config_present?(output) || !link_config_present?(output)
22
23
 
23
- if read_link_config['connections'].length.zero?
24
+ if connections.length.zero?
24
25
  output.puts @pastel.red('Please add a connection before continue.')
25
26
  return
26
27
  end
@@ -33,8 +34,12 @@ module Sfctl
33
34
 
34
35
  private
35
36
 
37
+ def connections
38
+ @connections ||= read_link_config.fetch('connections', [])
39
+ end
40
+
36
41
  def assignments_from_connections
37
- read_link_config['connections'].map do |con|
42
+ connections.map do |con|
38
43
  id = con[0]
39
44
  asmnt = con[1]
40
45
  {
@@ -47,6 +52,9 @@ module Sfctl
47
52
 
48
53
  def assignments_to_sync
49
54
  assignments = assignments_from_connections
55
+
56
+ return assignments if @options['all']
57
+
50
58
  assignment_id = select_assignment(assignments)
51
59
 
52
60
  return assignments if assignment_id == 'all'
@@ -66,38 +74,47 @@ module Sfctl
66
74
  def sync_assignments(output, list)
67
75
  list.each do |assignment|
68
76
  assignment_id = assignment['id'].to_s
69
- connection = read_link_config['connections'].select { |c| c == assignment_id }
70
-
71
- if connection.empty?
72
- output.puts @pastel.red("Unable to find a connection for assignment \"#{assignment['name']}\"")
73
- next
74
- end
75
-
77
+ connection = connections.select { |c| c == assignment_id }
76
78
  sync(output, assignment, connection[assignment_id])
77
79
  end
78
80
  end
79
81
 
80
- def sync(output, assignment, connection)
81
- case connection['provider']
82
- when TOGGL_PROVIDER
83
- sync_with_toggl!(output, assignment, connection)
84
- end
85
- end
86
-
87
- def sync_with_toggl!(output, assignment, connection) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
82
+ def sync(output, assignment, connection) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
88
83
  output.puts "Synchronizing: #{@pastel.cyan("[#{assignment['name']} / #{assignment['service']}]")}"
89
84
 
90
- success, next_report = Starfish.next_report(@options['starfish-host'], access_token, assignment['id'])
85
+ success, next_report = Starfish::Client.next_report(@options['starfish-host'], access_token, assignment['id'])
91
86
 
92
87
  print_no_next_reporting_segment(output) && return if !success || next_report.empty?
93
88
 
94
- time_entries = load_data_from_toggl(output, next_report, connection)
89
+ time_entries = load_time_entries(output, next_report, connection)
95
90
 
96
91
  print_dry_run_enabled(output) && return if @options['dry_run']
97
92
 
98
93
  print_report_contains_data(output, next_report) && return if touchy?(next_report)
99
94
 
100
- uploading_to_starfish(output, assignment, time_entries)
95
+ uploading_to_starfish(output, assignment, time_entries, connection)
96
+ end
97
+
98
+ def report_interval(record)
99
+ start_date = Date.parse("#{record['year']}-#{record['month']}-01")
100
+ end_date = start_date.next_month.prev_day
101
+ [start_date, end_date]
102
+ end
103
+
104
+ def load_time_entries(output, next_report, connection)
105
+ output.puts "Next Report: #{@pastel.cyan(report_name(next_report))}"
106
+ next_report_interval = report_interval(next_report)
107
+
108
+ case connection['provider']
109
+ when TOGGL_PROVIDER
110
+ Toggl::Sync.load_data(
111
+ output, connection, read_link_config['providers'][TOGGL_PROVIDER], @pastel, next_report_interval
112
+ )
113
+ when HARVEST_PROVIDER
114
+ Harvest::Sync.load_data(
115
+ output, connection, read_link_config['providers'][HARVEST_PROVIDER], @pastel, next_report_interval
116
+ )
117
+ end
101
118
  end
102
119
 
103
120
  def touchy?(next_report)
@@ -130,93 +147,21 @@ module Sfctl
130
147
  true
131
148
  end
132
149
 
133
- def load_data_from_toggl(output, next_report, connection)
134
- output.puts "Next Report: #{@pastel.cyan(report_name(next_report))}"
135
-
136
- spinner = TTY::Spinner.new("Loaded data from #{TOGGL_PROVIDER}: [:spinner]", format: :dots)
137
- spinner.auto_spin
138
-
139
- time_entries = get_toggle_time_entries(next_report, connection)
140
-
141
- spinner.success(@pastel.green('Done'))
142
-
143
- table = TTY::Table.new %w[Date Comment Time], time_entries_table_rows(time_entries)
144
- output.puts
145
- output.print table.render(:unicode, padding: [0, 1], alignments: %i[left left right])
146
- output.puts
147
- output.puts
148
-
149
- time_entries['data']
150
- end
151
-
152
- def time_entries_table_rows(time_entries)
153
- rows = time_entries['data'].sort_by { |te| te['start'] }.map do |te|
154
- [
155
- Date.parse(te['start']).to_s,
156
- te['description'],
157
- "#{humanize_duration(te['dur'])}h"
158
- ]
159
- end
160
- rows.push(['Total:', '', "#{humanize_duration(time_entries['total_grand'])}h"])
161
- rows
162
- end
163
-
164
- def get_toggle_time_entries(next_report, connection)
165
- _success, data = Toggl.time_entries(
166
- read_link_config['providers'][TOGGL_PROVIDER]['access_token'], time_entries_params(next_report, connection)
167
- )
168
-
169
- data
170
- end
171
-
172
- def time_entries_params(next_report, connection)
173
- start_date = Date.parse("#{next_report['year']}-#{next_report['month']}-01")
174
- end_date = start_date.next_month.prev_day
175
- params = {
176
- workspace_id: connection['workspace_id'],
177
- project_ids: connection['project_ids'],
178
- billable: connection['billable'],
179
- rounding: connection['rounding'],
180
- since: start_date.to_s,
181
- until: end_date.to_s
182
- }
183
- params[:task_ids] = connection['task_ids'] if connection['task_ids'].length.positive?
184
- params
185
- end
186
-
187
- def humanize_duration(milliseconds)
188
- return '0' if milliseconds.nil?
189
-
190
- seconds = milliseconds / 1000
191
- minutes = seconds / 60
192
- int = (minutes / 60).ceil
193
- dec = minutes % 60
194
- amount = (dec * 100) / 60
195
- amount = if dec.zero?
196
- ''
197
- elsif amount.to_s.length == 1
198
- ".0#{amount}"
199
- else
200
- ".#{amount}"
201
- end
202
- "#{int}#{amount}"
203
- end
204
-
205
- def assignment_items(time_entries)
206
- time_entries.map do |te|
207
- {
208
- time: humanize_duration(te['dur']).to_f,
209
- date: Date.parse(te['start']).to_s,
210
- comment: te['description']
211
- }
150
+ def assignment_items(time_entries, connection)
151
+ case connection['provider']
152
+ when TOGGL_PROVIDER
153
+ Toggl::Sync.assignment_items(time_entries)
154
+ when HARVEST_PROVIDER
155
+ Harvest::Sync.assignment_items(time_entries, connection)
212
156
  end
213
157
  end
214
158
 
215
- def uploading_to_starfish(output, assignment, time_entries)
159
+ def uploading_to_starfish(output, assignment, time_entries, connection)
216
160
  spinner = TTY::Spinner.new('Uploading to starfish.team: [:spinner]', format: :dots)
217
161
  spinner.auto_spin
218
- success = Starfish.update_next_report(
219
- @options['starfish-host'], access_token, assignment['id'], assignment_items(time_entries)
162
+
163
+ success = Starfish::Client.update_next_report(
164
+ @options['starfish-host'], access_token, assignment['id'], assignment_items(time_entries, connection)
220
165
  )
221
166
  print_upload_results(output, success, spinner)
222
167
  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,105 @@
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
24
+ end
25
+
26
+ def self.time_entries_table_rows(time_entries)
27
+ rows = time_entries.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
+ total_grand = time_entries.sum { |te| te['dur'] }
35
+ rows.push(['Total:', '', "#{humanize_duration(total_grand)}h"])
36
+ rows
37
+ end
38
+
39
+ def self.get_time_entries(connection, toggl_config, report_interval)
40
+ entries_list = []
41
+
42
+ page = 1
43
+ loop do
44
+ _success, body = Toggl::Client.time_entries(
45
+ toggl_config['access_token'],
46
+ time_entries_params(connection, report_interval, page)
47
+ )
48
+
49
+ entries_list << body['data']
50
+ entries_list.flatten!
51
+ entries_list.compact!
52
+
53
+ break if entries_list.length >= body['total_count']
54
+
55
+ page += 1
56
+ end
57
+
58
+ entries_list
59
+ end
60
+
61
+ def self.time_entries_params(connection, report_interval, page = 1)
62
+ start_date, end_date = report_interval
63
+ params = {
64
+ workspace_id: connection['workspace_id'],
65
+ project_ids: connection['project_ids'],
66
+ billable: connection['billable'],
67
+ rounding: connection['rounding'],
68
+ since: start_date.to_s,
69
+ until: end_date.to_s,
70
+ page: page
71
+ }
72
+ params[:task_ids] = connection['task_ids'] if connection['task_ids'].length.positive?
73
+ params
74
+ end
75
+
76
+ def self.humanize_duration(milliseconds)
77
+ return '0' if milliseconds.nil?
78
+
79
+ seconds = milliseconds / 1000
80
+ minutes = seconds / 60
81
+ int = (minutes / 60).ceil
82
+ dec = minutes % 60
83
+ amount = (dec * 100) / 60
84
+ amount = if dec.zero?
85
+ ''
86
+ elsif amount.to_s.length == 1
87
+ ".0#{amount}"
88
+ else
89
+ ".#{amount}"
90
+ end
91
+ "#{int}#{amount}"
92
+ end
93
+
94
+ def self.assignment_items(time_entries)
95
+ time_entries.map do |te|
96
+ {
97
+ time: humanize_duration(te['dur']).to_f,
98
+ date: Date.parse(te['start']).to_s,
99
+ comment: te['description']
100
+ }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,3 +1,3 @@
1
1
  module Sfctl
2
- VERSION = '0.0.3'.freeze
2
+ VERSION = '1.0.2'.freeze
3
3
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sfctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Serhii Rudik
8
8
  - Markus Kuhnt
9
- autorequire:
9
+ autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-04-15 00:00:00.000000000 Z
12
+ date: 2020-07-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
@@ -375,7 +375,7 @@ dependencies:
375
375
  - - "~>"
376
376
  - !ruby/object:Gem::Version
377
377
  version: 0.4.2
378
- description:
378
+ description:
379
379
  email:
380
380
  - serhii@starfish.team
381
381
  executables:
@@ -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
@@ -429,7 +432,7 @@ licenses:
429
432
  metadata:
430
433
  bug_tracker_uri: https://github.com/alphatier-works/sfctl/issues
431
434
  source_code_uri: https://github.com/alphatier-works/sfctl
432
- post_install_message:
435
+ post_install_message:
433
436
  rdoc_options: []
434
437
  require_paths:
435
438
  - lib
@@ -444,8 +447,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
444
447
  - !ruby/object:Gem::Version
445
448
  version: '0'
446
449
  requirements: []
447
- rubygems_version: 3.1.2
448
- signing_key:
450
+ rubygems_version: 3.1.3
451
+ signing_key:
449
452
  specification_version: 4
450
453
  summary: sfctl is a command line interface for the Starfish API.
451
454
  test_files: []
@@ -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