abt-cli 0.0.3 → 0.0.8

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +2 -0
  3. data/lib/abt/cli.rb +16 -7
  4. data/lib/abt/cli/dialogs.rb +18 -2
  5. data/lib/abt/cli/io.rb +8 -6
  6. data/lib/abt/docs.rb +16 -5
  7. data/lib/abt/docs/cli.rb +1 -1
  8. data/lib/abt/docs/markdown.rb +1 -1
  9. data/lib/abt/git_config.rb +55 -49
  10. data/lib/abt/providers/asana/api.rb +1 -1
  11. data/lib/abt/providers/asana/base_command.rb +9 -4
  12. data/lib/abt/providers/asana/commands/current.rb +10 -4
  13. data/lib/abt/providers/asana/commands/finalize.rb +71 -0
  14. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +2 -2
  15. data/lib/abt/providers/asana/commands/init.rb +10 -3
  16. data/lib/abt/providers/asana/commands/{pick_task.rb → pick.rb} +13 -6
  17. data/lib/abt/providers/asana/commands/projects.rb +9 -2
  18. data/lib/abt/providers/asana/commands/share.rb +29 -0
  19. data/lib/abt/providers/asana/commands/start.rb +51 -6
  20. data/lib/abt/providers/asana/commands/tasks.rb +4 -1
  21. data/lib/abt/providers/asana/configuration.rb +54 -34
  22. data/lib/abt/providers/harvest.rb +9 -51
  23. data/lib/abt/providers/harvest/api.rb +62 -0
  24. data/lib/abt/providers/harvest/base_command.rb +12 -16
  25. data/lib/abt/providers/harvest/commands/clear.rb +24 -0
  26. data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
  27. data/lib/abt/providers/harvest/commands/current.rb +83 -0
  28. data/lib/abt/providers/harvest/commands/init.rb +83 -0
  29. data/lib/abt/providers/harvest/commands/pick.rb +51 -0
  30. data/lib/abt/providers/harvest/commands/projects.rb +40 -0
  31. data/lib/abt/providers/harvest/commands/share.rb +29 -0
  32. data/lib/abt/providers/harvest/commands/start.rb +58 -0
  33. data/lib/abt/providers/harvest/commands/stop.rb +58 -0
  34. data/lib/abt/providers/harvest/commands/tasks.rb +45 -0
  35. data/lib/abt/providers/harvest/commands/track.rb +70 -0
  36. data/lib/abt/providers/harvest/configuration.rb +91 -0
  37. data/lib/abt/version.rb +1 -1
  38. metadata +18 -14
  39. data/lib/abt/harvest_client.rb +0 -58
  40. data/lib/abt/providers/asana/commands/move.rb +0 -56
  41. data/lib/abt/providers/harvest/clear.rb +0 -24
  42. data/lib/abt/providers/harvest/clear_global.rb +0 -24
  43. data/lib/abt/providers/harvest/current.rb +0 -79
  44. data/lib/abt/providers/harvest/init.rb +0 -61
  45. data/lib/abt/providers/harvest/pick_task.rb +0 -45
  46. data/lib/abt/providers/harvest/projects.rb +0 -29
  47. data/lib/abt/providers/harvest/start.rb +0 -58
  48. data/lib/abt/providers/harvest/stop.rb +0 -51
  49. data/lib/abt/providers/harvest/tasks.rb +0 -36
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ class Api
7
+ API_ENDPOINT = 'https://api.harvestapp.com/v2'
8
+ VERBS = %i[get post patch].freeze
9
+
10
+ attr_reader :access_token, :account_id
11
+
12
+ def initialize(access_token:, account_id:)
13
+ @access_token = access_token
14
+ @account_id = account_id
15
+ end
16
+
17
+ VERBS.each do |verb|
18
+ define_method(verb) do |*args|
19
+ request(verb, *args)
20
+ end
21
+ end
22
+
23
+ def get_paged(path, query = {})
24
+ result_key = path.split('?').first.split('/').last
25
+
26
+ page = 1
27
+ records = []
28
+
29
+ loop do
30
+ result = get(path, query.merge(page: page))
31
+ records += result[result_key]
32
+ break if result['total_pages'] == page
33
+
34
+ page += 1
35
+ end
36
+
37
+ records
38
+ end
39
+
40
+ def request(*args)
41
+ response = connection.public_send(*args)
42
+
43
+ if response.success?
44
+ Oj.load(response.body)
45
+ else
46
+ error_class = Abt::HttpError.error_class_for_status(response.status)
47
+ encoded_response_body = response.body.force_encoding('utf-8')
48
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
49
+ end
50
+ end
51
+
52
+ def connection
53
+ @connection ||= Faraday.new(API_ENDPOINT) do |connection|
54
+ connection.headers['Authorization'] = "Bearer #{access_token}"
55
+ connection.headers['Harvest-Account-Id'] = account_id
56
+ connection.headers['Content-Type'] = 'application/json'
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -2,12 +2,13 @@
2
2
 
3
3
  module Abt
4
4
  module Providers
5
- class Harvest
5
+ module Harvest
6
6
  class BaseCommand
7
- attr_reader :arg_str, :project_id, :task_id, :cli
7
+ attr_reader :arg_str, :project_id, :task_id, :cli, :config
8
8
 
9
9
  def initialize(arg_str:, cli:)
10
10
  @arg_str = arg_str
11
+ @config = Configuration.new(cli: cli)
11
12
 
12
13
  if arg_str.nil?
13
14
  use_current_args
@@ -19,6 +20,10 @@ module Abt
19
20
 
20
21
  private
21
22
 
23
+ def same_args_as_config?
24
+ project_id == config.project_id && task_id == config.task_id
25
+ end
26
+
22
27
  def print_project(project)
23
28
  cli.print_provider_command(
24
29
  'harvest',
@@ -36,10 +41,8 @@ module Abt
36
41
  end
37
42
 
38
43
  def use_current_args
39
- @project_id = Abt::GitConfig.local('abt.harvest.projectId').to_s
40
- @project_id = nil if project_id.empty?
41
- @task_id = Abt::GitConfig.local('abt.harvest.taskId').to_s
42
- @task_id = nil if task_id.empty?
44
+ @project_id = config.project_id
45
+ @task_id = config.task_id
43
46
  end
44
47
 
45
48
  def use_arg_str(arg_str)
@@ -53,16 +56,9 @@ module Abt
53
56
  @task_id = nil if @task_id.empty?
54
57
  end
55
58
 
56
- def remember_project_id(project_id)
57
- Abt::GitConfig.local('abt.harvest.projectId', project_id)
58
- end
59
-
60
- def remember_task_id(task_id)
61
- if task_id.nil?
62
- Abt::GitConfig.unset_local('abt.harvest.taskId')
63
- else
64
- Abt::GitConfig.local('abt.harvest.taskId', task_id)
65
- end
59
+ def api
60
+ @api ||= Abt::Providers::Harvest::Api.new(access_token: config.access_token,
61
+ account_id: config.account_id)
66
62
  end
67
63
  end
68
64
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Clear < BaseCommand
8
+ def self.command
9
+ 'clear harvest'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear project/task for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing Harvest project configuration'
18
+ config.clear_local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class ClearGlobal < BaseCommand
8
+ def self.command
9
+ 'clear-global harvest'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear all global configuration'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing Harvest project configuration'
18
+ config.clear_global
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Current < BaseCommand
8
+ def self.command
9
+ 'current harvest[:<project-id>[/<task-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Get or set project and or task for current git repository'
14
+ end
15
+
16
+ def call
17
+ if same_args_as_config? || !config.local_available?
18
+ show_current_configuration
19
+ else
20
+ cli.warn 'Updating configuration'
21
+ update_configuration
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def show_current_configuration
28
+ if project_id.nil?
29
+ cli.warn 'No project selected'
30
+ elsif task_id.nil?
31
+ print_project(project)
32
+ else
33
+ print_task(project, task)
34
+ end
35
+ end
36
+
37
+ def update_configuration
38
+ ensure_project_is_valid!
39
+ config.project_id = project_id
40
+
41
+ if task_id.nil?
42
+ print_project(project)
43
+ config.task_id = nil
44
+ else
45
+ ensure_task_is_valid!
46
+ config.task_id = task_id
47
+
48
+ print_task(project, task)
49
+ end
50
+ end
51
+
52
+ def ensure_project_is_valid!
53
+ cli.abort "Invalid project: #{project_id}" if project.nil?
54
+ end
55
+
56
+ def ensure_task_is_valid!
57
+ cli.abort "Invalid task: #{task_id}" if task.nil?
58
+ end
59
+
60
+ def project
61
+ @project ||= project_assignment['project'].merge('client' => project_assignment['client'])
62
+ end
63
+
64
+ def task
65
+ @task ||= project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
66
+ task['id'].to_s == task_id
67
+ end
68
+ end
69
+
70
+ def project_assignment
71
+ @project_assignment ||= begin
72
+ project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
73
+ end
74
+ end
75
+
76
+ def project_assignments
77
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Init < BaseCommand
8
+ def self.command
9
+ 'init harvest'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick Harvest project for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+
19
+ projects # Load projects up front to make it obvious that searches are instant
20
+ project = find_search_result
21
+
22
+ config.project_id = project['id']
23
+ config.task_id = nil
24
+
25
+ print_project(project)
26
+ end
27
+
28
+ private
29
+
30
+ def find_search_result
31
+ cli.warn 'Select a project'
32
+
33
+ loop do
34
+ matches = matches_for_string cli.prompt('Enter search')
35
+ if matches.empty?
36
+ warn 'No matches'
37
+ next
38
+ end
39
+
40
+ cli.warn 'Showing the 10 first matches' if matches.size > 10
41
+ choice = cli.prompt_choice 'Select a project', matches[0...10], true
42
+ break choice['project'] unless choice.nil?
43
+ end
44
+ end
45
+
46
+ def matches_for_string(string)
47
+ search_string = sanitize_string(string)
48
+
49
+ searchable_projects.select do |project|
50
+ sanitize_string(project['name']).include?(search_string)
51
+ end
52
+ end
53
+
54
+ def sanitize_string(string)
55
+ string.downcase.gsub(/[^\w]/, '')
56
+ end
57
+
58
+ def searchable_projects
59
+ @searchable_projects ||= projects.map do |project|
60
+ {
61
+ 'name' => "#{project['client']['name']} > #{project['name']}",
62
+ 'project' => project
63
+ }
64
+ end
65
+ end
66
+
67
+ def projects
68
+ @projects ||= begin
69
+ cli.warn 'Fetching projects...'
70
+ project_assignments.map do |project_assignment|
71
+ project_assignment['project'].merge('client' => project_assignment['client'])
72
+ end
73
+ end
74
+ end
75
+
76
+ def project_assignments
77
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Pick < BaseCommand
8
+ def self.command
9
+ 'pick harvest[:<project-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick task for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+
19
+ cli.warn project['name']
20
+ task = cli.prompt_choice 'Select a task', tasks
21
+
22
+ config.project_id = project_id # We might have gotten the project ID as an argument
23
+ config.task_id = task['id']
24
+
25
+ print_task(project, task)
26
+ end
27
+
28
+ private
29
+
30
+ def project
31
+ project_assignment['project']
32
+ end
33
+
34
+ def tasks
35
+ @tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
36
+ end
37
+
38
+ def project_assignment
39
+ @project_assignment ||= begin
40
+ project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
41
+ end
42
+ end
43
+
44
+ def project_assignments
45
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Projects < BaseCommand
8
+ def self.command
9
+ 'projects harvest'
10
+ end
11
+
12
+ def self.description
13
+ 'List all available projects - useful for piping into grep etc.'
14
+ end
15
+
16
+ def call
17
+ projects.map do |project|
18
+ print_project(project)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def projects
25
+ @projects ||= begin
26
+ cli.warn 'Fetching projects...'
27
+ project_assignments.map do |project_assignment|
28
+ project_assignment['project'].merge('client' => project_assignment['client'])
29
+ end
30
+ end
31
+ end
32
+
33
+ def project_assignments
34
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Share < BaseCommand
8
+ def self.command
9
+ 'share harvest[:<project-id>[/<task-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print project/task config string'
14
+ end
15
+
16
+ def call
17
+ if project_id.nil?
18
+ cli.warn 'No project selected'
19
+ elsif task_id.nil?
20
+ cli.print_provider_command('harvest', project_id)
21
+ else
22
+ cli.print_provider_command('harvest', "#{project_id}/#{task_id}")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end