harvesting 0.1.0 → 0.5.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.env.sample +10 -0
  3. data/.gitignore +13 -0
  4. data/.travis.yml +7 -2
  5. data/Dockerfile +6 -0
  6. data/README.md +173 -32
  7. data/RELEASE_NOTES.md +80 -0
  8. data/TODO.md +1 -1
  9. data/bin/console +1 -0
  10. data/bin/setup +5 -0
  11. data/docker-compose.yml +10 -0
  12. data/harvesting.gemspec +9 -6
  13. data/lib/harvesting.rb +20 -5
  14. data/lib/harvesting/client.rb +76 -15
  15. data/lib/harvesting/enumerable.rb +5 -2
  16. data/lib/harvesting/errors.rb +9 -0
  17. data/lib/harvesting/models/base.rb +93 -12
  18. data/lib/harvesting/models/client.rb +7 -1
  19. data/lib/harvesting/models/clients.rb +18 -0
  20. data/lib/harvesting/models/contact.rb +14 -1
  21. data/lib/harvesting/models/harvest_record.rb +18 -0
  22. data/lib/harvesting/models/harvest_record_collection.rb +43 -0
  23. data/lib/harvesting/models/invoice.rb +44 -0
  24. data/lib/harvesting/models/invoices.rb +17 -0
  25. data/lib/harvesting/models/line_item.rb +19 -0
  26. data/lib/harvesting/models/project.rb +28 -3
  27. data/lib/harvesting/models/project_task_assignment.rb +33 -0
  28. data/lib/harvesting/models/project_task_assignments.rb +18 -0
  29. data/lib/harvesting/models/project_user_assignment.rb +25 -0
  30. data/lib/harvesting/models/project_user_assignments.rb +19 -0
  31. data/lib/harvesting/models/projects.rb +6 -33
  32. data/lib/harvesting/models/task.rb +6 -3
  33. data/lib/harvesting/models/tasks.rb +6 -33
  34. data/lib/harvesting/models/time_entries.rb +6 -33
  35. data/lib/harvesting/models/time_entry.rb +22 -8
  36. data/lib/harvesting/models/user.rb +20 -6
  37. data/lib/harvesting/models/users.rb +18 -0
  38. data/lib/harvesting/version.rb +1 -1
  39. metadata +90 -21
  40. data/Gemfile.lock +0 -103
@@ -1,16 +1,31 @@
1
+ # framework
1
2
  require "harvesting/version"
2
3
  require "harvesting/enumerable"
3
4
  require "harvesting/errors"
4
5
  require "harvesting/models/base"
5
- require "harvesting/models/user"
6
- require "harvesting/models/contact"
6
+ require "harvesting/models/harvest_record"
7
+ require "harvesting/models/harvest_record_collection"
8
+ # harvest records
7
9
  require "harvesting/models/client"
8
- require "harvesting/models/projects"
10
+ require "harvesting/models/user"
9
11
  require "harvesting/models/project"
10
- require "harvesting/models/tasks"
11
12
  require "harvesting/models/task"
12
- require "harvesting/models/time_entries"
13
+ require "harvesting/models/project_user_assignment"
14
+ require "harvesting/models/project_task_assignment"
15
+ require "harvesting/models/invoice"
16
+ require "harvesting/models/line_item"
13
17
  require "harvesting/models/time_entry"
18
+ # harvest record collections
19
+ require "harvesting/models/clients"
20
+ require "harvesting/models/tasks"
21
+ require "harvesting/models/users"
22
+ require "harvesting/models/contact"
23
+ require "harvesting/models/time_entries"
24
+ require "harvesting/models/projects"
25
+ require "harvesting/models/project_user_assignments"
26
+ require "harvesting/models/project_task_assignments"
27
+ require "harvesting/models/invoices"
28
+ # API client
14
29
  require "harvesting/client"
15
30
 
16
31
  module Harvesting
@@ -1,18 +1,22 @@
1
1
  # frozen_string_literal: true
2
-
3
- require "byebug"
4
2
  require "http"
5
3
  require "json"
6
4
 
7
5
  module Harvesting
6
+
7
+ # A client for the Harvest API (version 2.0)
8
8
  class Client
9
9
  DEFAULT_HOST = "https://api.harvestapp.com/v2"
10
10
 
11
11
  attr_accessor :access_token, :account_id
12
12
 
13
+ # Returns a new instance of `Client`
13
14
  #
14
- # @param opts
15
+ # client = Client.new(access_token: "12345678", account_id: "98764")
15
16
  #
17
+ # @param [Hash] opts the options to create an API client
18
+ # @option opts [String] :access_token Harvest access token
19
+ # @option opts [String] :account_id Harvest account id
16
20
  def initialize(access_token: ENV['HARVEST_ACCESS_TOKEN'], account_id: ENV['HARVEST_ACCOUNT_ID'])
17
21
  @access_token = access_token.to_s
18
22
  @account_id = account_id.to_s
@@ -22,34 +26,66 @@ module Harvesting
22
26
  end
23
27
  end
24
28
 
29
+ # @return [Harvesting::Models::User]
25
30
  def me
26
- Harvesting::Models::User.new(get("users/me"), client: self)
31
+ Harvesting::Models::User.new(get("users/me"), harvest_client: self)
27
32
  end
28
-
29
- def clients
30
- get("clients")["clients"].map do |result|
31
- Harvesting::Models::Client.new(result, client: self)
32
- end
33
+
34
+ # @return [Harvesting::Models::Clients]
35
+ def clients(opts = {})
36
+ Harvesting::Models::Clients.new(get("clients", opts), opts, harvest_client: self)
33
37
  end
34
38
 
39
+ # @return [Array<Harvesting::Models::Contact>]
35
40
  def contacts
36
41
  get("contacts")["contacts"].map do |result|
37
- Harvesting::Models::Contact.new(result, client: self)
42
+ Harvesting::Models::Contact.new(result, harvest_client: self)
38
43
  end
39
44
  end
40
45
 
46
+ # @return [Harvesting::Models::TimeEntries]
41
47
  def time_entries(opts = {})
42
- Harvesting::Models::TimeEntries.new(get("time_entries", opts), client: self)
48
+ Harvesting::Models::TimeEntries.new(get("time_entries", opts), opts, harvest_client: self)
43
49
  end
44
50
 
51
+ # @return [Harvesting::Models::Projects]
45
52
  def projects(opts = {})
46
- Harvesting::Models::Projects.new(get("projects", opts), client: self)
53
+ Harvesting::Models::Projects.new(get("projects", opts), opts, harvest_client: self)
47
54
  end
48
55
 
56
+ # @return [Harvesting::Models::Tasks]
49
57
  def tasks(opts = {})
50
- Harvesting::Models::Tasks.new(get("tasks", opts), client: self)
58
+ Harvesting::Models::Tasks.new(get("tasks", opts), opts, harvest_client: self)
59
+ end
60
+
61
+ # @return [Harvesting::Models::Users]
62
+ def users(opts = {})
63
+ Harvesting::Models::Users.new(get("users", opts), opts, harvest_client: self)
64
+ end
65
+
66
+ # @return [Array<Harvesting::Models::Invoice>]
67
+ def invoices(opts = {})
68
+ Harvesting::Models::Invoices.new(get("invoices", opts), opts, harvest_client: self)
69
+ end
70
+
71
+ # @return [Harvesting::Models::ProjectUserAssignments]
72
+ def user_assignments(opts = {})
73
+ project_id = opts.delete(:project_id)
74
+ path = project_id.nil? ? "user_assignments" : "projects/#{project_id}/user_assignments"
75
+ Harvesting::Models::ProjectUserAssignments.new(get(path, opts), opts, harvest_client: self)
76
+ end
77
+
78
+ # @return [Harvesting::Models::ProjectTaskAssignments]
79
+ def task_assignments(opts = {})
80
+ project_id = opts.delete(:project_id)
81
+ path = project_id.nil? ? "task_assignments" : "projects/#{project_id}/task_assignments"
82
+ Harvesting::Models::ProjectTaskAssignments.new(get(path, opts), opts, harvest_client: self)
51
83
  end
52
84
 
85
+ # Creates an `entity` in your Harvest account.
86
+ #
87
+ # @param entity [Harvesting::Models::Base] A new record in your Harvest account
88
+ # @return [Harvesting::Models::Base] A subclass of `Harvesting::Models::Base` updated with the response from Harvest
53
89
  def create(entity)
54
90
  url = "#{DEFAULT_HOST}/#{entity.path}"
55
91
  uri = URI(url)
@@ -58,6 +94,10 @@ module Harvesting
58
94
  entity
59
95
  end
60
96
 
97
+ # Updates an `entity` in your Harvest account.
98
+ #
99
+ # @param entity [Harvesting::Models::Base] An existing record in your Harvest account
100
+ # @return [Harvesting::Models::Base] A subclass of `Harvesting::Models::Base` updated with the response from Harvest
61
101
  def update(entity)
62
102
  url = "#{DEFAULT_HOST}/#{entity.path}"
63
103
  uri = URI(url)
@@ -66,8 +106,25 @@ module Harvesting
66
106
  entity
67
107
  end
68
108
 
69
- private
109
+ # It removes an `entity` from your Harvest account.
110
+ #
111
+ # @param entity [Harvesting::Models::Base] A record to be removed from your Harvest account
112
+ # @return [Hash]
113
+ # @raise [UnprocessableRequest] When HTTP response is not 200 OK
114
+ def delete(entity)
115
+ url = "#{DEFAULT_HOST}/#{entity.path}"
116
+ uri = URI(url)
117
+ response = http_response(:delete, uri)
118
+ raise UnprocessableRequest.new(response.to_s) unless response.code.to_i == 200
119
+
120
+ JSON.parse(response.body)
121
+ end
70
122
 
123
+ # Performs a GET request and returned the parsed JSON as a Hash.
124
+ #
125
+ # @param path [String] path to be combined with `DEFAULT_HOST`
126
+ # @param opts [Hash] key/values will get passed as HTTP (GET) parameters
127
+ # @return [Hash]
71
128
  def get(path, opts = {})
72
129
  url = "#{DEFAULT_HOST}/#{path}"
73
130
  url += "?#{opts.map {|k, v| "#{k}=#{v}"}.join("&")}" if opts.any?
@@ -76,10 +133,12 @@ module Harvesting
76
133
  JSON.parse(response.body)
77
134
  end
78
135
 
136
+ private
137
+
79
138
  def http_response(method, uri, opts = {})
80
139
  response = nil
81
140
 
82
- http = HTTP["User-Agent" => "Ruby Harvest API Sample",
141
+ http = HTTP["User-Agent" => "Harvesting Ruby Gem",
83
142
  "Authorization" => "Bearer #{@access_token}",
84
143
  "Harvest-Account-ID" => @account_id]
85
144
  params = {}
@@ -90,6 +149,8 @@ module Harvesting
90
149
 
91
150
  raise Harvesting::AuthenticationError.new(response.to_s) if auth_error?(response)
92
151
  raise Harvesting::UnprocessableRequest.new(response.to_s) if response.code.to_i == 422
152
+ raise Harvesting::RequestNotFound.new(uri) if response.code.to_i == 404
153
+ raise Harvesting::RateLimitExceeded.new(response.to_s) if response.code.to_i == 429
93
154
 
94
155
  response
95
156
  end
@@ -1,10 +1,13 @@
1
+ # `Enumerable` extends the stdlib `Enumerable` to provide pagination for paged
2
+ # API requests.
3
+ #
1
4
  # @see https://github.com/sferik/twitter/blob/aa909b3b7733ca619d80f1c8cba961033d1fc7e6/lib/twitter/enumerable.rb
2
5
  module Harvesting
3
6
  module Enumerable
4
7
  include ::Enumerable
5
8
 
6
9
  # @return [Enumerator]
7
- def each(start = 0)
10
+ def each(start = 0, &block)
8
11
  @cursor = start
9
12
  return to_enum(:each, start) unless block_given?
10
13
  Array(@entries[start..-1]).each_with_index do |element, index|
@@ -15,7 +18,7 @@ module Harvesting
15
18
  unless last?
16
19
  start = [@entries.size, start].max
17
20
  fetch_next_page
18
- each(start, &Proc.new)
21
+ each(start, &block)
19
22
  end
20
23
  self
21
24
  end
@@ -4,4 +4,13 @@ module Harvesting
4
4
 
5
5
  class UnprocessableRequest < StandardError
6
6
  end
7
+
8
+ class RequestNotFound < StandardError
9
+ def initialize(uri)
10
+ super("The page you were looking for may have been moved or the address misspelled: #{uri}")
11
+ end
12
+ end
13
+
14
+ class RateLimitExceeded < StandardError
15
+ end
7
16
  end
@@ -1,37 +1,118 @@
1
1
  module Harvesting
2
2
  module Models
3
3
  class Base
4
+ # @return [Hash]
4
5
  attr_accessor :attributes
5
- attr_reader :client
6
+ # @return [Harvesting::Model::Client]
7
+ attr_reader :harvest_client
6
8
 
7
9
  def initialize(attrs, opts = {})
10
+ @models = {}
8
11
  @attributes = attrs.dup
9
- @client = opts[:client] || Harvesting::Client.new(opts)
10
- end
11
-
12
- def self.attributed(*attribute_names)
13
- attribute_names.each do |attribute_name|
14
- Harvesting::Models::Base.send :define_method, attribute_name.to_s do
15
- @attributes[__method__.to_s]
16
- end
17
- end
12
+ @harvest_client = opts[:harvest_client] || Harvesting::Client.new(opts)
18
13
  end
19
14
 
15
+ # It calls `create` or `update` depending on the record's ID. If the ID
16
+ # is present, then it calls `update`. Otherwise it calls `create`
17
+ #
18
+ # @see Client#create
19
+ # @see Client#update
20
20
  def save
21
21
  id.nil? ? create : update
22
22
  end
23
23
 
24
+ # It creates the record.
25
+ #
26
+ # @see Client#create
27
+ # @return [Harvesting::Models::Base]
24
28
  def create
25
- @client.create(self)
29
+ @harvest_client.create(self)
26
30
  end
27
31
 
32
+ # It updates the record.
33
+ #
34
+ # @see Client#update
35
+ # @return [Harvesting::Models::Base]
28
36
  def update
29
- @client.update(self)
37
+ @harvest_client.update(self)
30
38
  end
31
39
 
40
+ # It removes the record.
41
+ #
42
+ # @see Client#delete
43
+ # @return [Harvesting::Models::Base]
44
+ def delete
45
+ @harvest_client.delete(self)
46
+ end
47
+
48
+ # It returns keys and values for all the attributes of this record.
49
+ #
50
+ # @return [Hash]
32
51
  def to_hash
33
52
  @attributes
34
53
  end
54
+
55
+ # It loads a new record from your Harvest account.
56
+ #
57
+ # @return [Harvesting::Models::Base]
58
+ def fetch
59
+ self.class.new(@harvest_client.get(path), harvest_client: @harvest_client)
60
+ end
61
+
62
+ # Retrieves an instance of the object by ID
63
+ #
64
+ # @param id [Integer] the id of the object to retrieve
65
+ # @param opts [Hash] options to pass along to the `Harvesting::Client`
66
+ # instance
67
+ def self.get(id, opts = {})
68
+ client = opts[:harvest_client] || Harvesting::Client.new(opts)
69
+ self.new({ 'id' => id }, opts).fetch
70
+ end
71
+
72
+ protected
73
+
74
+ # Class method to define attribute methods for accessing attributes for
75
+ # a record
76
+ #
77
+ # It needs to be used like this:
78
+ #
79
+ # class Contact < HarvestRecord
80
+ # attributed :id,
81
+ # :title,
82
+ # :first_name
83
+ # ...
84
+ # end
85
+ #
86
+ # @param attribute_names [Array] A list of attributes
87
+ def self.attributed(*attribute_names)
88
+ attribute_names.each do |attribute_name|
89
+ define_method(attribute_name) do
90
+ @attributes[__method__.to_s]
91
+ end
92
+ define_method("#{attribute_name}=") do |value|
93
+ @attributes[__method__.to_s.chop] = value
94
+ end
95
+ end
96
+ end
97
+
98
+ # Class method to define nested resources for a record.
99
+ #
100
+ # It needs to be used like this:
101
+ #
102
+ # class Contact < HarvestRecord
103
+ # modeled client: Client
104
+ # ...
105
+ # end
106
+ #
107
+ # @param opts [Hash] key = symbol that needs to be the same as the one returned by the Harvest API. value = model class for the nested resource.
108
+ def self.modeled(opts = {})
109
+ opts.each do |attribute_name, model|
110
+ attribute_name_string = attribute_name.to_s
111
+ Harvesting::Models::Base.send :define_method, attribute_name_string do
112
+ @models[attribute_name_string] ||= model.new(@attributes[attribute_name_string] || {}, harvest_client: harvest_client)
113
+ end
114
+ end
115
+ end
35
116
  end
36
117
  end
37
118
  end
@@ -1,6 +1,9 @@
1
1
  module Harvesting
2
2
  module Models
3
- class Client < Base
3
+ # A client record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/clients-api/clients/clients/
6
+ class Client < HarvestRecord
4
7
  attributed :id,
5
8
  :name,
6
9
  :is_active,
@@ -9,6 +12,9 @@ module Harvesting
9
12
  :updated_at,
10
13
  :currency
11
14
 
15
+ def path
16
+ @attributes['id'].nil? ? "clients" : "clients/#{@attributes['id']}"
17
+ end
12
18
  end
13
19
  end
14
20
  end
@@ -0,0 +1,18 @@
1
+ module Harvesting
2
+ module Models
3
+ class Clients < HarvestRecordCollection
4
+
5
+ def initialize(attrs, query_opts = {}, opts = {})
6
+ super(attrs.reject {|k,v| k == "clients" }, query_opts, opts)
7
+ @entries = attrs["clients"].map do |entry|
8
+ Harvesting::Models::Client.new(entry, harvest_client: opts[:harvest_client])
9
+ end
10
+ end
11
+
12
+ def fetch_next_page
13
+ @entries += harvest_client.clients(next_page_query_opts).entries
14
+ @attributes['page'] = page + 1
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,6 +1,9 @@
1
1
  module Harvesting
2
2
  module Models
3
- class Contact < Base
3
+ # A contact record from your Harvest account.
4
+ #
5
+ # For more information: https://help.getharvest.com/api-v2/clients-api/clients/contacts/
6
+ class Contact < HarvestRecord
4
7
  attributed :id,
5
8
  :title,
6
9
  :first_name,
@@ -11,6 +14,16 @@ module Harvesting
11
14
  :fax,
12
15
  :created_at,
13
16
  :updated_at
17
+
18
+ modeled client: Client
19
+
20
+ def path
21
+ @attributes['id'].nil? ? "contacts" : "contacts/#{@attributes['id']}"
22
+ end
23
+
24
+ def to_hash
25
+ { client_id: client.id }.merge(super)
26
+ end
14
27
  end
15
28
  end
16
29
  end
@@ -0,0 +1,18 @@
1
+ module Harvesting
2
+ module Models
3
+ class HarvestRecord < Base
4
+
5
+ def save
6
+ id.nil? ? create : update
7
+ end
8
+
9
+ def create
10
+ harvest_client.create(self)
11
+ end
12
+
13
+ def update
14
+ harvest_client.update(self)
15
+ end
16
+ end
17
+ end
18
+ end