harvesting 0.1.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.env.sample +10 -0
- data/.gitignore +13 -0
- data/.travis.yml +7 -2
- data/Dockerfile +6 -0
- data/README.md +173 -32
- data/RELEASE_NOTES.md +80 -0
- data/TODO.md +1 -1
- data/bin/console +1 -0
- data/bin/setup +5 -0
- data/docker-compose.yml +10 -0
- data/harvesting.gemspec +9 -6
- data/lib/harvesting.rb +20 -5
- data/lib/harvesting/client.rb +76 -15
- data/lib/harvesting/enumerable.rb +5 -2
- data/lib/harvesting/errors.rb +9 -0
- data/lib/harvesting/models/base.rb +93 -12
- data/lib/harvesting/models/client.rb +7 -1
- data/lib/harvesting/models/clients.rb +18 -0
- data/lib/harvesting/models/contact.rb +14 -1
- data/lib/harvesting/models/harvest_record.rb +18 -0
- data/lib/harvesting/models/harvest_record_collection.rb +43 -0
- data/lib/harvesting/models/invoice.rb +44 -0
- data/lib/harvesting/models/invoices.rb +17 -0
- data/lib/harvesting/models/line_item.rb +19 -0
- data/lib/harvesting/models/project.rb +28 -3
- data/lib/harvesting/models/project_task_assignment.rb +33 -0
- data/lib/harvesting/models/project_task_assignments.rb +18 -0
- data/lib/harvesting/models/project_user_assignment.rb +25 -0
- data/lib/harvesting/models/project_user_assignments.rb +19 -0
- data/lib/harvesting/models/projects.rb +6 -33
- data/lib/harvesting/models/task.rb +6 -3
- data/lib/harvesting/models/tasks.rb +6 -33
- data/lib/harvesting/models/time_entries.rb +6 -33
- data/lib/harvesting/models/time_entry.rb +22 -8
- data/lib/harvesting/models/user.rb +20 -6
- data/lib/harvesting/models/users.rb +18 -0
- data/lib/harvesting/version.rb +1 -1
- metadata +90 -21
- data/Gemfile.lock +0 -103
data/lib/harvesting.rb
CHANGED
@@ -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/
|
6
|
-
require "harvesting/models/
|
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/
|
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/
|
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
|
data/lib/harvesting/client.rb
CHANGED
@@ -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
|
-
#
|
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"),
|
31
|
+
Harvesting::Models::User.new(get("users/me"), harvest_client: self)
|
27
32
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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,
|
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),
|
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),
|
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),
|
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
|
-
|
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
|
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, &
|
21
|
+
each(start, &block)
|
19
22
|
end
|
20
23
|
self
|
21
24
|
end
|
data/lib/harvesting/errors.rb
CHANGED
@@ -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
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|