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.
- 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
|