sfctl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ require 'pastel'
2
+ require 'tty-prompt'
3
+ require_relative '../../../command'
4
+
5
+ module Sfctl
6
+ module Commands
7
+ class Time
8
+ class Providers
9
+ class Unset < Sfctl::Command
10
+ def initialize(options)
11
+ @options = options
12
+ @pastel = Pastel.new(enabled: !@options['no-color'])
13
+ end
14
+
15
+ def execute(output: $stdout)
16
+ read_link_config
17
+
18
+ prompt = ::TTY::Prompt.new
19
+ provider = prompt.select('Unsetting:', PROVIDERS_LIST)
20
+
21
+ if config.fetch(:providers, provider).nil?
22
+ output.puts @pastel.yellow("[#{provider}] is already deleted from configuration.")
23
+ elsif prompt.yes?('Do you want to remove the delete the configuration?')
24
+ remove_provider!(provider, output)
25
+ end
26
+ rescue TTY::Config::ReadError
27
+ output.puts @pastel.yellow('Please initialize time before continue.')
28
+ end
29
+
30
+ private
31
+
32
+ def remove_provider!(provider, output)
33
+ providers = config.fetch(:providers)
34
+ providers.delete(provider)
35
+ config.set(:providers, value: providers)
36
+ save_link_config!
37
+ output.puts @pastel.green("Configuration for provider [#{provider}] was successfully deleted.")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,215 @@
1
+ require 'date'
2
+ require 'pastel'
3
+ require 'tty-prompt'
4
+ require 'tty-spinner'
5
+ require 'tty-table'
6
+ require_relative '../../command'
7
+ require_relative '../../starfish'
8
+ require_relative '../../toggl'
9
+
10
+ module Sfctl
11
+ module Commands
12
+ class Time
13
+ class Sync < Sfctl::Command # rubocop:disable Metrics/ClassLength
14
+ def initialize(options)
15
+ @options = options
16
+ @pastel = Pastel.new(enabled: !@options['no-color'])
17
+ @prompt = ::TTY::Prompt.new
18
+ end
19
+
20
+ def execute(output: $stdout)
21
+ return if !config_present?(output) || !link_config_present?(output)
22
+
23
+ success, data = Starfish.account_assignments(@options['starfish-host'], @options['all'], access_token)
24
+ unless success
25
+ output.puts @pastel.red('Something went wrong. Unable to fetch assignments')
26
+ return
27
+ end
28
+
29
+ sync_assignments(output, assignments_to_sync(data['assignments']))
30
+ rescue ThreadError, JSON::ParserError
31
+ output.puts @pastel.red('Something went wrong.')
32
+ end
33
+
34
+ private
35
+
36
+ def assignments_to_sync(assignments)
37
+ assignment_name = select_assignment(assignments)
38
+
39
+ return assignments if assignment_name == 'all'
40
+
41
+ assignments.select { |a| a['name'] == assignment_name }.to_a
42
+ end
43
+
44
+ def select_assignment(assignments)
45
+ @prompt.select('Which assignment do you want to sync?') do |menu|
46
+ assignments.each do |asmnt|
47
+ menu.choice name: "#{asmnt['name']} / #{asmnt['service']}", value: asmnt['name']
48
+ end
49
+ menu.choice name: 'All', value: 'all'
50
+ end
51
+ end
52
+
53
+ def sync_assignments(output, list)
54
+ list.each do |assignment|
55
+ assignment_name = assignment['name']
56
+ connection = read_link_config['connections'].select { |c| c == assignment_name }
57
+
58
+ if connection.empty?
59
+ output.puts @pastel.red("Unable to find a connection for assignment \"#{assignment_name}\"")
60
+ next
61
+ end
62
+
63
+ sync(output, assignment, connection[assignment_name])
64
+ end
65
+ end
66
+
67
+ def sync(output, assignment, connection)
68
+ case connection['provider']
69
+ when TOGGL_PROVIDER
70
+ sync_with_toggl!(output, assignment, connection)
71
+ end
72
+ end
73
+
74
+ def sync_with_toggl!(output, assignment, connection) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
75
+ output.puts "Synchronizing: #{@pastel.cyan("[#{assignment['name']} / #{assignment['service']}]")}"
76
+
77
+ success, next_report = Starfish.next_report(@options['starfish-host'], access_token, assignment['id'])
78
+
79
+ print_no_next_reporting_segment(output) && return if !success || next_report.empty?
80
+
81
+ time_entries = load_data_from_toggl(output, next_report, connection)
82
+
83
+ print_dry_run_enabled(output) && return if @options['dry_run']
84
+
85
+ print_report_contains_data(output, next_report) && return if touchy?(next_report)
86
+
87
+ uploading_to_starfish(output, assignment, time_entries)
88
+ end
89
+
90
+ def touchy?(next_report)
91
+ @options['touchy'] && next_report['data'] == 'present'
92
+ end
93
+
94
+ def report_name(next_report)
95
+ "[#{next_report['year']}-#{next_report['month']}]"
96
+ end
97
+
98
+ def print_no_next_reporting_segment(output)
99
+ message = <<~HEREDOC
100
+ No next reporting segment on Starfish that accepts time report data, the synchronization will be skipped.
101
+ HEREDOC
102
+ output.puts @pastel.red(message)
103
+ true
104
+ end
105
+
106
+ def print_dry_run_enabled(output)
107
+ output.puts @pastel.yellow('Dry run enabled. Skipping upload to starfish.team.')
108
+ output.puts
109
+ true
110
+ end
111
+
112
+ def print_report_contains_data(output, next_report)
113
+ output.puts @pastel.yellow(
114
+ "Report #{report_name(next_report)} contains data. Skipping upload to starfish.team."
115
+ )
116
+ output.puts
117
+ true
118
+ end
119
+
120
+ def load_data_from_toggl(output, next_report, connection)
121
+ output.puts "Next Report: #{@pastel.cyan(report_name(next_report))}"
122
+
123
+ spinner = TTY::Spinner.new("Loaded data from #{TOGGL_PROVIDER}: [:spinner]", format: :dots)
124
+ spinner.auto_spin
125
+
126
+ time_entries = get_toggle_time_entries(next_report, connection)
127
+
128
+ spinner.success(@pastel.green('Done'))
129
+
130
+ table = TTY::Table.new %w[Date Comment Time], time_entries_table_rows(time_entries)
131
+ output.puts
132
+ output.print table.render(:unicode, padding: [0, 1], alignments: %i[left left right])
133
+ output.puts
134
+ output.puts
135
+
136
+ time_entries
137
+ end
138
+
139
+ def time_entries_table_rows(time_entries)
140
+ rows = time_entries.map do |te|
141
+ [
142
+ Date.parse(te['start']).to_s,
143
+ te['description'],
144
+ "#{humanize_duration(te['duration'])}h"
145
+ ]
146
+ end
147
+ rows.push(['Total:', '', "#{humanize_duration(time_entries.map { |te| te['duration'] }.sum)}h"])
148
+ rows
149
+ end
150
+
151
+ def get_toggle_time_entries(next_report, connection)
152
+ _success, time_entries = Toggl.time_entries(
153
+ read_link_config['providers'][TOGGL_PROVIDER]['access_token'], time_entries_params(next_report, connection)
154
+ )
155
+ unless connection['task_ids'].empty?
156
+ time_entries.delete_if { |te| !connection['task_ids'].include?(te['id'].to_s) }
157
+ end
158
+
159
+ time_entries
160
+ end
161
+
162
+ def time_entries_params(next_report, connection)
163
+ start_date = Date.parse("#{next_report['year']}-#{next_report['month']}-01")
164
+ end_date = start_date.next_month.prev_day
165
+ {
166
+ wid: connection['workspace_id'],
167
+ pid: connection['project_ids'],
168
+ start_date: start_date.to_datetime.to_s,
169
+ end_date: "#{end_date}T23:59:59+00:00"
170
+ }
171
+ end
172
+
173
+ def humanize_duration(seconds)
174
+ minutes = seconds / 60
175
+ int = (minutes / 60).ceil
176
+ dec = minutes % 60
177
+ amount = (dec * 100) / 60
178
+ amount = dec.zero? ? '' : ".#{amount}"
179
+ "#{int}#{amount}"
180
+ end
181
+
182
+ def assignment_items(time_entries)
183
+ time_entries.map do |te|
184
+ {
185
+ time: humanize_duration(te['duration']),
186
+ date: Date.parse(te['start']).to_s,
187
+ comment: te['description']
188
+ }
189
+ end
190
+ end
191
+
192
+ def uploading_to_starfish(output, assignment, time_entries)
193
+ spinner = TTY::Spinner.new('Uploading to starfish.team: [:spinner]', format: :dots)
194
+ spinner.auto_spin
195
+ success = Starfish.update_next_report(
196
+ @options['starfish-host'], access_token, assignment['id'], assignment_items(time_entries)
197
+ )
198
+ print_upload_results(output, success, spinner)
199
+ end
200
+
201
+ def print_upload_results(output, success, spinner)
202
+ if success
203
+ spinner.success(@pastel.green('Done'))
204
+ else
205
+ spinner.error
206
+ output.puts @pastel.red('Something went wrong. Unable to upload time entries to starfish.team')
207
+ end
208
+ output.puts
209
+ output.puts
210
+ true
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,51 @@
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
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1,27 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module Sfctl
5
+ module Toggl
6
+ def self.conn(token)
7
+ raise 'Please set toggl provider before continue.' if token.nil?
8
+
9
+ headers = {
10
+ 'Content-Type' => 'application/json'
11
+ }
12
+ Faraday.new(url: "https://#{token}:api_token@www.toggl.com/api/v8/", headers: headers) do |builder|
13
+ builder.request :retry
14
+ builder.adapter :net_http
15
+ end
16
+ end
17
+
18
+ def self.parsed_response(response)
19
+ [response.status == 200, JSON.parse(response.body)]
20
+ end
21
+
22
+ def self.time_entries(token, params)
23
+ response = conn(token).get('time_entries', params)
24
+ parsed_response(response)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Sfctl
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ echo "--- Prepare :bundler: dependencies"
5
+ bundle install
6
+
7
+ echo "+++ Run :rspec:"
8
+ bundle exec rspec
@@ -0,0 +1,28 @@
1
+ require_relative 'lib/sfctl/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'sfctl'
5
+ spec.license = 'MIT'
6
+ spec.version = Sfctl::VERSION
7
+ spec.authors = ['Serhii Rudik', 'Markus Kuhnt']
8
+ spec.email = ['serhii@starfish.team']
9
+
10
+ spec.summary = 'sfctl is a command line interface for the Starfish API.'
11
+ # spec.homepage = ''
12
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
13
+
14
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
15
+
16
+ # spec.metadata['homepage_uri'] = spec.homepage
17
+ # spec.metadata['source_code_uri'] = ''
18
+ # spec.metadata['changelog_uri'] = ''
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sfctl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Serhii Rudik
8
+ - Markus Kuhnt
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2020-04-08 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email:
16
+ - serhii@starfish.team
17
+ executables:
18
+ - sfctl
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".buildkite/hooks/pre-command"
23
+ - ".buildkite/pipeline.yml"
24
+ - ".gitignore"
25
+ - ".rspec"
26
+ - ".rubocop.yml"
27
+ - ".tool-versions"
28
+ - CODE_OF_CONDUCT.md
29
+ - Gemfile
30
+ - Gemfile.lock
31
+ - README.md
32
+ - Rakefile
33
+ - bin/console
34
+ - bin/setup
35
+ - exe/sfctl
36
+ - lib/sfctl.rb
37
+ - lib/sfctl/cli.rb
38
+ - lib/sfctl/command.rb
39
+ - lib/sfctl/commands/account.rb
40
+ - lib/sfctl/commands/account/assignments.rb
41
+ - lib/sfctl/commands/account/info.rb
42
+ - lib/sfctl/commands/auth.rb
43
+ - lib/sfctl/commands/auth/bye.rb
44
+ - lib/sfctl/commands/auth/init.rb
45
+ - lib/sfctl/commands/time.rb
46
+ - lib/sfctl/commands/time/connections.rb
47
+ - lib/sfctl/commands/time/connections/add.rb
48
+ - lib/sfctl/commands/time/connections/get.rb
49
+ - lib/sfctl/commands/time/init.rb
50
+ - lib/sfctl/commands/time/providers.rb
51
+ - lib/sfctl/commands/time/providers/get.rb
52
+ - lib/sfctl/commands/time/providers/set.rb
53
+ - lib/sfctl/commands/time/providers/unset.rb
54
+ - lib/sfctl/commands/time/sync.rb
55
+ - lib/sfctl/starfish.rb
56
+ - lib/sfctl/templates/.gitkeep
57
+ - lib/sfctl/toggl.rb
58
+ - lib/sfctl/version.rb
59
+ - scripts/test.sh
60
+ - sfctl.gemspec
61
+ homepage:
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 2.3.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.1.2
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: sfctl is a command line interface for the Starfish API.
84
+ test_files: []