sfctl 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []