harvesting 0.1.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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