forecasting 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +22 -0
- data/README.md +69 -0
- data/lib/forecasting/client.rb +238 -0
- data/lib/forecasting/errors.rb +16 -0
- data/lib/forecasting/models/account.rb +20 -0
- data/lib/forecasting/models/address.rb +12 -0
- data/lib/forecasting/models/assignment.rb +34 -0
- data/lib/forecasting/models/base.rb +64 -0
- data/lib/forecasting/models/card.rb +10 -0
- data/lib/forecasting/models/client.rb +17 -0
- data/lib/forecasting/models/color_label.rb +7 -0
- data/lib/forecasting/models/discount.rb +8 -0
- data/lib/forecasting/models/forecast_record.rb +74 -0
- data/lib/forecasting/models/ftux_state.rb +7 -0
- data/lib/forecasting/models/future_scheduled_hours.rb +30 -0
- data/lib/forecasting/models/milestone.rb +20 -0
- data/lib/forecasting/models/person.rb +27 -0
- data/lib/forecasting/models/placeholder.rb +16 -0
- data/lib/forecasting/models/project.rb +28 -0
- data/lib/forecasting/models/remaining_budgeted_hours.rb +23 -0
- data/lib/forecasting/models/repeated_assignment_set.rb +20 -0
- data/lib/forecasting/models/role.rb +27 -0
- data/lib/forecasting/models/subscription.rb +24 -0
- data/lib/forecasting/models/user.rb +7 -0
- data/lib/forecasting/models/user_connection.rb +13 -0
- data/lib/forecasting/version.rb +3 -0
- data/lib/forecasting.rb +29 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: edb8ec26fd106cae648d103e0d55a3eac43a84950932903c964263597641dae1
|
4
|
+
data.tar.gz: cc0ff4974048deca834309b5c652bd776ba6ef78454f7cf5cbe1afeeb85dc4cf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bf01d135878f0a0fe617787f0b4f73404b5cbc71b0fe6ea7c460b47bb8e097f25265925d9a50e78dc5bd17386d32773d8e86428ed2ad414e293d2b1975296f5a
|
7
|
+
data.tar.gz: f58e4883da565c2388e6baa5b6b09498574e5e214cbc21d1e0cff0b9d696462db230c07b3406500ee6583faa5f680960bc78670a66570805d688fb4fd3b080f1
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2023 Jordan Owens
|
2
|
+
Copyright (c) 2018 Ernesto Tagwerker
|
3
|
+
|
4
|
+
The MIT License (MIT)
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
14
|
+
all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Forecasting
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/forecasting.svg)](https://badge.fury.io/rb/forecasting)
|
4
|
+
[![Tests](https://github.com/NeomindLabs/forecasting/actions/workflows/ruby.yml/badge.svg)](https://github.com/NeomindLabs/forecasting/actions/workflows/ruby.yml)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/NeomindLabs/forecasting/badges/gpa.svg)](https://codeclimate.com/github/NeomindLabs/forecasting)
|
6
|
+
|
7
|
+
A Ruby gem to interact with the Harvest Forecast API. Please note that there is currently [no official public API](https://help.getharvest.com/forecast/faqs/faq-list/api). This API client has been made by inspecting network requests using the Forecast website. This library is based on the excellent [Harvesting](https://github.com/fastruby/harvesting) gem.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'forecasting'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install forecasting
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
In order to start using this gem you will need your personal token and an account id:
|
28
|
+
|
29
|
+
You can find these details over here: https://id.getharvest.com/developers
|
30
|
+
|
31
|
+
If you don't specify values for `access_token` or `account_id`, it will default to these environment variables:
|
32
|
+
|
33
|
+
* `ENV['FORECAST_ACCESS_TOKEN']`
|
34
|
+
* `ENV['FORECAST_ACCOUNT_ID']`
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
# $ export FORECST_ACCESS_TOKEN=xxx
|
38
|
+
# $ export FORECAST_ACCOUNT_ID=12345678
|
39
|
+
client = Forecasting::Client.new
|
40
|
+
client.whoami
|
41
|
+
=> #<Forecasting::Models::User:0x000000010488e370 @attributes={"id"=>212737, ... }>
|
42
|
+
```
|
43
|
+
|
44
|
+
If the access token or account id is invalid a `Forecasting::AuthenticationError` will be raised:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
client = Forcasting::Client.new(access_token: "foo", account_id: "bar")
|
48
|
+
client.whoami
|
49
|
+
=> #<Forecasting::AuthenticationError: {"reason":"non-existent-token","account_id":"bar"}>
|
50
|
+
```
|
51
|
+
|
52
|
+
With a valid access token and account id:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
client = Forecasting::Client.new(access_token: "<your token here>", account_id: "<your account id here>")
|
56
|
+
user = client.whoami
|
57
|
+
#<Forecasting::Models::User:0x000000010e238ac0 @attributes={"id"=>212737, ... }>
|
58
|
+
|
59
|
+
user.id
|
60
|
+
# => 212737
|
61
|
+
```
|
62
|
+
|
63
|
+
## Releases
|
64
|
+
|
65
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
66
|
+
|
67
|
+
## License
|
68
|
+
|
69
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "http"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
class Forecasting::Client
|
6
|
+
DEFAULT_HOST = "https://api.forecastapp.com"
|
7
|
+
|
8
|
+
attr_accessor :access_token, :account_id, :host
|
9
|
+
|
10
|
+
def initialize(access_token: ENV['FORECAST_ACCESS_TOKEN'],
|
11
|
+
account_id: ENV['FORECAST_ACCOUNT_ID'],
|
12
|
+
host: DEFAULT_HOST)
|
13
|
+
@access_token = access_token
|
14
|
+
@account_id = account_id
|
15
|
+
@host = host
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Forecasting::Models::Account]
|
19
|
+
def account(id:)
|
20
|
+
Forecasting::Models::Account.new(get("accounts/#{id}")["account"])
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Forecasting::Models::Assignment]
|
24
|
+
def assignment(id:)
|
25
|
+
Forecasting::Models::Assignment.new(get("assignments/#{id}")["assignment"], forecast_client: self)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Array<Forecasting::Models::Assignment>]
|
29
|
+
def assignments(opts = {})
|
30
|
+
get("assignments", opts)["assignments"].map do |entry|
|
31
|
+
Forecasting::Models::Assignment.new(entry, forecast_client: self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Forecasting::Models::Client]
|
36
|
+
def client(id:)
|
37
|
+
Forecasting::Models::Client.new(get("clients/#{id}")["client"], forecast_client: self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Array<Forecasting::Models::Client>]
|
41
|
+
def clients(opts = {})
|
42
|
+
get("clients", opts)["clients"].map do |entry|
|
43
|
+
Forecasting::Models::Client.new(entry, forecast_client: self)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Forecasting::Models::FtuxState]
|
48
|
+
def ftux_state
|
49
|
+
Forecasting::Models::FtuxState.new(get("ftux_state")["ftux_state"])
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Array<Forecasting::Models::FutureScheduledHours>]
|
53
|
+
def future_scheduled_hours(opts = {})
|
54
|
+
start_date = opts.delete(:start_date) || Time.now.strftime("%Y-%m-%d")
|
55
|
+
get("aggregate/future_scheduled_hours/#{start_date}", opts)["future_scheduled_hours"].map do |entry|
|
56
|
+
Forecasting::Models::FutureScheduledHours.new(entry, forecast_client: self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Forecasting::Models::Milestone]
|
61
|
+
def milestone(id:)
|
62
|
+
Forecasting::Models::Milestone.new(get("milestones/#{id}")["milestone"], forecast_client: self)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Array<Forecasting::Models::Milestone>]
|
66
|
+
def milestones(opts = {})
|
67
|
+
get("milestones", opts)["milestones"].map do |entry|
|
68
|
+
Forecasting::Models::Milestone.new(entry, forecast_client: self)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [Forecasting::Models::Person]
|
73
|
+
def person(id:)
|
74
|
+
Forecasting::Models::Person.new(get("people/#{id}")["person"], forecast_client: self)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Array<Forecasting::Models::Person>]
|
78
|
+
def people(opts = {})
|
79
|
+
get("people", opts)["people"].map do |entry|
|
80
|
+
Forecasting::Models::Person.new(entry, forecast_client: self)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Forecasting::Models::Placeholder]
|
85
|
+
def placeholder(id:)
|
86
|
+
Forecasting::Models::Placeholder.new(get("placeholders/#{id}")["placeholder"], forecast_client: self)
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Array<Forecasting::Models::Placeholder>]
|
90
|
+
def placeholders(opts = {})
|
91
|
+
get("placeholders", opts)["placeholders"].map do |entry|
|
92
|
+
Forecasting::Models::Placeholder.new(entry, forecast_client: self)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [Forecasting::Models::Projects]
|
97
|
+
def project(id:)
|
98
|
+
Forecasting::Models::Project.new(get("projects/#{id}")["project"], forecast_client: self)
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [Array<Forecasting::Models::Project>]
|
102
|
+
def projects(opts = {})
|
103
|
+
get("projects", opts)["projects"].map do |entry|
|
104
|
+
Forecasting::Models::Project.new(entry, forecast_client: self)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Array<Forecasting::Models::RemainingBudgetedHours>]
|
109
|
+
def remaining_budgeted_hours(opts = {})
|
110
|
+
get("aggregate/remaining_budgeted_hours", opts)["remaining_budgeted_hours"].map do |entry|
|
111
|
+
Forecasting::Models::RemainingBudgetedHours.new(entry, forecast_client: self)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [Forecasting::Models::RepeatedAssignmentSet]
|
116
|
+
def repeated_assignment_set(id:)
|
117
|
+
Forecasting::Models::RepeatedAssignmentSet.new(get("repeated_assignment_sets/#{id}")["repeated_assignment_set"], forecast_client: self)
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [Array<Forecasting::Models::RepeatedAssignmentSet>]
|
121
|
+
def repeated_assignment_sets(opts = {})
|
122
|
+
get("repeated_assignment_sets", opts)["repeated_assignment_sets"].map do |entry|
|
123
|
+
Forecasting::Models::RepeatedAssignmentSet.new(entry, forecast_client: self)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# @return [Forecasting::Models::Role]
|
128
|
+
def role(id:)
|
129
|
+
Forecasting::Models::Role.new(get("roles/#{id}")["role"], forecast_client: self)
|
130
|
+
end
|
131
|
+
|
132
|
+
# @return [Array<Forecasting::Models::Role>]
|
133
|
+
def roles(opts = {})
|
134
|
+
get("roles", opts)["roles"].map do |entry|
|
135
|
+
Forecasting::Models::Role.new(entry, forecast_client: self)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return [Forecasting::Models::Subscription]
|
140
|
+
def subscription
|
141
|
+
Forecasting::Models::Subscription.new(get("billing/subscription")["subscription"])
|
142
|
+
end
|
143
|
+
|
144
|
+
# @return [Forecasting::Models::UserConnection]
|
145
|
+
def user_connections(opts = {})
|
146
|
+
get("user_connections", opts)["user_connections"].map do |entry|
|
147
|
+
Forecasting::Models::UserConnection.new(entry)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [Forecasting::Models::User]
|
152
|
+
def whoami
|
153
|
+
Forecasting::Models::User.new(get("whoami")["current_user"])
|
154
|
+
end
|
155
|
+
|
156
|
+
# Creates an `entity` in your Harvest account.
|
157
|
+
#
|
158
|
+
# @param entity [Harvesting::Models::Base] A new record in your Harvest account
|
159
|
+
# @return [Harvesting::Models::Base] A subclass of `Harvesting::Models::Base` updated with the response from Harvest
|
160
|
+
def create(entity)
|
161
|
+
url = "#{host}/#{entity.path}"
|
162
|
+
uri = URI(url)
|
163
|
+
response = http_response(:post, uri, body: payload(entity))
|
164
|
+
entity.attributes = JSON.parse(response.body)[entity.type]
|
165
|
+
entity
|
166
|
+
end
|
167
|
+
|
168
|
+
# Updates an `entity` in your Harvest account.
|
169
|
+
#
|
170
|
+
# @param entity [Harvesting::Models::Base] An existing record in your Harvest account
|
171
|
+
# @return [Harvesting::Models::Base] A subclass of `Harvesting::Models::Base` updated with the response from Harvest
|
172
|
+
def update(entity)
|
173
|
+
url = "#{host}/#{entity.path}"
|
174
|
+
uri = URI(url)
|
175
|
+
response = http_response(:patch, uri, body: payload(entity))
|
176
|
+
entity.attributes = JSON.parse(response.body)[entity.type]
|
177
|
+
entity
|
178
|
+
end
|
179
|
+
|
180
|
+
# It removes an `entity` from your Harvest account.
|
181
|
+
#
|
182
|
+
# @param entity [Harvesting::Models::Base] A record to be removed from your Harvest account
|
183
|
+
# @return [Hash]
|
184
|
+
# @raise [UnprocessableRequest] When HTTP response is not 200 OK
|
185
|
+
def delete(entity)
|
186
|
+
url = "#{host}/#{entity.path}"
|
187
|
+
uri = URI(url)
|
188
|
+
response = http_response(:delete, uri)
|
189
|
+
raise Forecasting::Errors::UnprocessableRequest.new(response.to_s) unless response.code.to_i == 200
|
190
|
+
|
191
|
+
JSON.parse(response.body)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Performs a GET request and returned the parsed JSON as a Hash.
|
195
|
+
#
|
196
|
+
# @param path [String] path to be combined with `host`
|
197
|
+
# @param opts [Hash] key/values will get passed as HTTP (GET) parameters
|
198
|
+
# @return [Hash]
|
199
|
+
def get(path, opts = {})
|
200
|
+
url = "#{host}/#{path}"
|
201
|
+
url += "?#{opts.map {|k, v| "#{k}=#{v}"}.join("&")}" if opts.any?
|
202
|
+
uri = URI(url)
|
203
|
+
response = http_response(:get, uri)
|
204
|
+
JSON.parse(response.body)
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def payload(entity)
|
210
|
+
{
|
211
|
+
entity.type => entity.to_hash
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
def http_response(method, uri, opts = {})
|
216
|
+
response = nil
|
217
|
+
|
218
|
+
http = HTTP["User-Agent" => "Forecasting Ruby Gem",
|
219
|
+
"Authorization" => "Bearer #{access_token}",
|
220
|
+
"Forecast-Account-ID" => account_id]
|
221
|
+
params = {}
|
222
|
+
if opts[:body]
|
223
|
+
params[:json] = opts[:body]
|
224
|
+
end
|
225
|
+
response = http.send(method, uri, params)
|
226
|
+
|
227
|
+
raise Forecasting::AuthenticationError.new(response.to_s) if auth_error?(response)
|
228
|
+
raise Forecasting::UnprocessableRequest.new(response.to_s) if response.code.to_i == 422
|
229
|
+
raise Forecasting::RequestNotFound.new(uri) if response.code.to_i == 404
|
230
|
+
raise Forecasting::RateLimitExceeded.new(response.to_s) if response.code.to_i == 429
|
231
|
+
|
232
|
+
response
|
233
|
+
end
|
234
|
+
|
235
|
+
def auth_error?(response)
|
236
|
+
response.code.to_i == 403 || response.code.to_i == 401
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Forecasting
|
2
|
+
class AuthenticationError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class UnprocessableRequest < StandardError
|
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
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class Account < Base
|
4
|
+
attributed :id,
|
5
|
+
:name,
|
6
|
+
:weekly_capacity,
|
7
|
+
:harvest_subdomain,
|
8
|
+
:harvest_link,
|
9
|
+
:harvest_name,
|
10
|
+
:weekends_enabled,
|
11
|
+
:created_at
|
12
|
+
|
13
|
+
def color_labels
|
14
|
+
attributes["color_labels"].map do |entry|
|
15
|
+
ColorLabel.new(entry)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class Assignment < ForecastRecord
|
4
|
+
attributed :id,
|
5
|
+
:start_date,
|
6
|
+
:end_date,
|
7
|
+
:allocation,
|
8
|
+
:notes,
|
9
|
+
:project_id,
|
10
|
+
:person_id,
|
11
|
+
:placeholder_id,
|
12
|
+
:repeated_assignment_set_id,
|
13
|
+
:active_on_days_off,
|
14
|
+
:updated_at,
|
15
|
+
:updated_by_id
|
16
|
+
|
17
|
+
def path
|
18
|
+
@attributes['id'].nil? ? "milestones" : "milestones/#{@attributes['id']}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def project
|
22
|
+
forecast_client.project(id: project_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def person
|
26
|
+
forecast_client.person(id: person_id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def placeholder
|
30
|
+
forecast_client.placeholder(id: placeholder_id)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class Base
|
4
|
+
# @return [Hash]
|
5
|
+
attr_accessor :attributes
|
6
|
+
|
7
|
+
def initialize(attrs)
|
8
|
+
@attributes = attrs.dup
|
9
|
+
@models = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# It returns keys and values for all the attributes of this record.
|
13
|
+
#
|
14
|
+
# @return [Hash]
|
15
|
+
def to_hash
|
16
|
+
@attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# Class method to define attribute methods for accessing attributes for
|
21
|
+
# a record
|
22
|
+
#
|
23
|
+
# It needs to be used like this:
|
24
|
+
#
|
25
|
+
# class User < ForecastRecord
|
26
|
+
# attributed :id,
|
27
|
+
# :title,
|
28
|
+
# :first_name
|
29
|
+
# ...
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @param attribute_names [Array] A list of attributes
|
33
|
+
def self.attributed(*attribute_names)
|
34
|
+
attribute_names.each do |attribute_name|
|
35
|
+
define_method(attribute_name) do
|
36
|
+
@attributes[__method__.to_s]
|
37
|
+
end
|
38
|
+
define_method("#{attribute_name}=") do |value|
|
39
|
+
@attributes[__method__.to_s.chop] = value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Class method to define nested resources for a record.
|
45
|
+
#
|
46
|
+
# It needs to be used like this:
|
47
|
+
#
|
48
|
+
# class Project < Base
|
49
|
+
# modeled client: Client
|
50
|
+
# ...
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# @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.
|
54
|
+
def self.modeled(opts = {})
|
55
|
+
opts.each do |attribute_name, model|
|
56
|
+
attribute_name_string = attribute_name.to_s
|
57
|
+
define_method(attribute_name_string) do
|
58
|
+
@models[attribute_name_string] ||= model.new(@attributes[attribute_name_string] || {})
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
# A client record from your Harvest account.
|
4
|
+
class Client < ForecastRecord
|
5
|
+
attributed :id,
|
6
|
+
:name,
|
7
|
+
:harvest_id,
|
8
|
+
:archived,
|
9
|
+
:updated_at,
|
10
|
+
:updated_by_id
|
11
|
+
|
12
|
+
def path
|
13
|
+
@attributes['id'].nil? ? "clients" : "clients/#{@attributes['id']}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class ForecastRecord < Base
|
4
|
+
# @return [Forecasting::Model::Client]
|
5
|
+
attr_reader :forecast_client
|
6
|
+
|
7
|
+
def initialize(attrs, opts = {})
|
8
|
+
@forecast_client = opts[:forecast_client] || Forecasting::Client.new(**opts)
|
9
|
+
super(attrs)
|
10
|
+
end
|
11
|
+
|
12
|
+
def save
|
13
|
+
id.nil? ? create : update
|
14
|
+
end
|
15
|
+
|
16
|
+
def create
|
17
|
+
forecast_client.create(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def update
|
21
|
+
forecast_client.update(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete
|
25
|
+
forecast_client.delete(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
# It loads a new record from your Harvest account.
|
29
|
+
#
|
30
|
+
# @return [Forecasting::Models::Base]
|
31
|
+
def fetch
|
32
|
+
self.class.new(@forecast_client.get(path), forecast_client: @forecast_client)
|
33
|
+
end
|
34
|
+
|
35
|
+
# It returns the model type
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
def type
|
39
|
+
self.class.name.split("::").last.downcase
|
40
|
+
end
|
41
|
+
|
42
|
+
# Retrieves an instance of the object by ID
|
43
|
+
#
|
44
|
+
# @param id [Integer] the id of the object to retrieve
|
45
|
+
# @param opts [Hash] options to pass along to the `Forecasting::Client`
|
46
|
+
# instance
|
47
|
+
def self.get(id, opts = {})
|
48
|
+
client = opts[:forecast_client] || Forecasting::Client.new(**opts)
|
49
|
+
self.new({ 'id' => id }, opts).fetch
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
# Class method to define nested resources for a record.
|
55
|
+
#
|
56
|
+
# It needs to be used like this:
|
57
|
+
#
|
58
|
+
# class Project < ForecastRecord
|
59
|
+
# modeled client: Client
|
60
|
+
# ...
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# @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.
|
64
|
+
def self.modeled(opts = {})
|
65
|
+
opts.each do |attribute_name, model|
|
66
|
+
attribute_name_string = attribute_name.to_s
|
67
|
+
define_method(attribute_name_string) do
|
68
|
+
@models[attribute_name_string] ||= model.new(@attributes[attribute_name_string] || {}, forecast_client: forecast_client)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class FutureScheduledHours < Base
|
4
|
+
# @return [Forecasting::Model::Client]
|
5
|
+
attr_reader :forecast_client
|
6
|
+
|
7
|
+
attributed :project_id,
|
8
|
+
:person_id,
|
9
|
+
:placeholder_id,
|
10
|
+
:allocation
|
11
|
+
|
12
|
+
def initialize(attrs, opts = {})
|
13
|
+
@forecast_client = opts[:forecast_client] || Forecasting::Client.new(**opts)
|
14
|
+
super(attrs)
|
15
|
+
end
|
16
|
+
|
17
|
+
def project
|
18
|
+
forecast_client.project(id: project_id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def person
|
22
|
+
forecast_client.person(id: person_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def placeholder
|
26
|
+
forecast_client.placeholder(id: placeholder_id)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class Milestone < ForecastRecord
|
4
|
+
attributed :id,
|
5
|
+
:name,
|
6
|
+
:date,
|
7
|
+
:project_id,
|
8
|
+
:updated_at,
|
9
|
+
:updated_by_id
|
10
|
+
|
11
|
+
def path
|
12
|
+
@attributes['id'].nil? ? "milestones" : "milestones/#{@attributes['id']}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def project
|
16
|
+
forecast_client.project(id: project_id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class Person < ForecastRecord
|
4
|
+
attributed :id,
|
5
|
+
:first_name,
|
6
|
+
:last_name,
|
7
|
+
:email,
|
8
|
+
:login,
|
9
|
+
:admin,
|
10
|
+
:archived,
|
11
|
+
:subscribed,
|
12
|
+
:avatar_url,
|
13
|
+
:roles,
|
14
|
+
:updated_at,
|
15
|
+
:updated_by_id,
|
16
|
+
:harvest_user_id,
|
17
|
+
:weekly_capacity,
|
18
|
+
:working_days,
|
19
|
+
:color_blind,
|
20
|
+
:personal_feed_token_id
|
21
|
+
|
22
|
+
def path
|
23
|
+
@attributes['id'].nil? ? "people" : "people/#{@attributes['id']}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class Placeholder < ForecastRecord
|
4
|
+
attributed :id,
|
5
|
+
:name,
|
6
|
+
:archived,
|
7
|
+
:roles,
|
8
|
+
:updated_at,
|
9
|
+
:updated_by_id
|
10
|
+
|
11
|
+
def path
|
12
|
+
@attributes['id'].nil? ? "placeholders" : "placeholders/#{@attributes['id']}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
# A project record from your Harvest account.
|
4
|
+
class Project < ForecastRecord
|
5
|
+
attributed :id,
|
6
|
+
:name,
|
7
|
+
:code,
|
8
|
+
:color,
|
9
|
+
:notes,
|
10
|
+
:start_date,
|
11
|
+
:end_date,
|
12
|
+
:harvest_id,
|
13
|
+
:archived,
|
14
|
+
:updated_at,
|
15
|
+
:updated_by_id,
|
16
|
+
:client_id,
|
17
|
+
:tags
|
18
|
+
|
19
|
+
def path
|
20
|
+
@attributes['id'].nil? ? "projects" : "projects/#{@attributes['id']}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def client
|
24
|
+
forecast_client.client(id: client_id)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class RemainingBudgetedHours < Base
|
4
|
+
# @return [Forecasting::Model::Client]
|
5
|
+
attr_reader :forecast_client
|
6
|
+
|
7
|
+
attributed :project_id,
|
8
|
+
:budget_by,
|
9
|
+
:budget_is_monthly,
|
10
|
+
:hours,
|
11
|
+
:response_code
|
12
|
+
|
13
|
+
def initialize(attrs, opts = {})
|
14
|
+
@forecast_client = opts[:forecast_client] || Forecasting::Client.new(**opts)
|
15
|
+
super(attrs)
|
16
|
+
end
|
17
|
+
|
18
|
+
def project
|
19
|
+
forecast_client.project(id: project_id)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class RepeatedAssignmentSet < ForecastRecord
|
4
|
+
attributed :id,
|
5
|
+
:first_start_date,
|
6
|
+
:last_end_date,
|
7
|
+
:assignment_ids
|
8
|
+
|
9
|
+
def path
|
10
|
+
@attributes['id'].nil? ? "repeated_assignment_sets" : "repeated_assignment_sets/#{@attributes['id']}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def assignments
|
14
|
+
assignment_ids.map do |assignment_id|
|
15
|
+
forecast_client.assignment(id: assignment_id)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class Role < ForecastRecord
|
4
|
+
attributed :id,
|
5
|
+
:name,
|
6
|
+
:placeholder_ids,
|
7
|
+
:person_ids,
|
8
|
+
:harvest_role_id
|
9
|
+
|
10
|
+
def path
|
11
|
+
@attributes['id'].nil? ? "roles" : "roles/#{@attributes['id']}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def placeholders
|
15
|
+
placeholder_ids.map do |placeholder_id|
|
16
|
+
forecast_client.placeholder(id: placeholder_id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def people
|
21
|
+
person_ids.map do |person_id|
|
22
|
+
forecast_client.person(id: person_id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Forecasting
|
2
|
+
module Models
|
3
|
+
class Subscription < Base
|
4
|
+
attributed :id,
|
5
|
+
:next_billing_date,
|
6
|
+
:days_until_next_billing_date,
|
7
|
+
:amount,
|
8
|
+
:amount_per_person,
|
9
|
+
:receipt_recipient,
|
10
|
+
:status,
|
11
|
+
:purchase_people,
|
12
|
+
:interval,
|
13
|
+
:paid_by_invoice,
|
14
|
+
:placeholder_limit,
|
15
|
+
:invoiced,
|
16
|
+
:days_until_due,
|
17
|
+
:balance,
|
18
|
+
:sales_tax_exempt,
|
19
|
+
:sales_tax_percentage
|
20
|
+
|
21
|
+
modeled address: Address, card: Card, discounts: Discount
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/forecasting.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# framework
|
2
|
+
require_relative "forecasting/version"
|
3
|
+
require_relative "forecasting/errors"
|
4
|
+
require_relative "forecasting/models/base"
|
5
|
+
require_relative "forecasting/models/forecast_record"
|
6
|
+
# forecast records
|
7
|
+
require_relative "forecasting/models/account"
|
8
|
+
require_relative "forecasting/models/address"
|
9
|
+
require_relative "forecasting/models/assignment"
|
10
|
+
require_relative "forecasting/models/card"
|
11
|
+
require_relative "forecasting/models/client"
|
12
|
+
require_relative "forecasting/models/discount"
|
13
|
+
require_relative "forecasting/models/ftux_state"
|
14
|
+
require_relative "forecasting/models/future_scheduled_hours"
|
15
|
+
require_relative "forecasting/models/milestone"
|
16
|
+
require_relative "forecasting/models/person"
|
17
|
+
require_relative "forecasting/models/placeholder"
|
18
|
+
require_relative "forecasting/models/project"
|
19
|
+
require_relative "forecasting/models/remaining_budgeted_hours"
|
20
|
+
require_relative "forecasting/models/repeated_assignment_set"
|
21
|
+
require_relative "forecasting/models/role"
|
22
|
+
require_relative "forecasting/models/subscription"
|
23
|
+
require_relative "forecasting/models/user_connection"
|
24
|
+
require_relative "forecasting/models/user"
|
25
|
+
# API client
|
26
|
+
require_relative "forecasting/client"
|
27
|
+
|
28
|
+
module Forecasting
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: forecasting
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jordan Owens
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-01-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: http
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description: Interact with the Forecast API from your Ruby application
|
56
|
+
email:
|
57
|
+
- jordan@neomindlabs.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- CHANGELOG.md
|
63
|
+
- LICENSE
|
64
|
+
- README.md
|
65
|
+
- lib/forecasting.rb
|
66
|
+
- lib/forecasting/client.rb
|
67
|
+
- lib/forecasting/errors.rb
|
68
|
+
- lib/forecasting/models/account.rb
|
69
|
+
- lib/forecasting/models/address.rb
|
70
|
+
- lib/forecasting/models/assignment.rb
|
71
|
+
- lib/forecasting/models/base.rb
|
72
|
+
- lib/forecasting/models/card.rb
|
73
|
+
- lib/forecasting/models/client.rb
|
74
|
+
- lib/forecasting/models/color_label.rb
|
75
|
+
- lib/forecasting/models/discount.rb
|
76
|
+
- lib/forecasting/models/forecast_record.rb
|
77
|
+
- lib/forecasting/models/ftux_state.rb
|
78
|
+
- lib/forecasting/models/future_scheduled_hours.rb
|
79
|
+
- lib/forecasting/models/milestone.rb
|
80
|
+
- lib/forecasting/models/person.rb
|
81
|
+
- lib/forecasting/models/placeholder.rb
|
82
|
+
- lib/forecasting/models/project.rb
|
83
|
+
- lib/forecasting/models/remaining_budgeted_hours.rb
|
84
|
+
- lib/forecasting/models/repeated_assignment_set.rb
|
85
|
+
- lib/forecasting/models/role.rb
|
86
|
+
- lib/forecasting/models/subscription.rb
|
87
|
+
- lib/forecasting/models/user.rb
|
88
|
+
- lib/forecasting/models/user_connection.rb
|
89
|
+
- lib/forecasting/version.rb
|
90
|
+
homepage: https://github.com/NeomindLabs/forecasting
|
91
|
+
licenses:
|
92
|
+
- MIT
|
93
|
+
metadata: {}
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '2.6'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubygems_version: 3.3.20
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Ruby wrapper for the Forecast API
|
113
|
+
test_files: []
|