forecasting 0.1.0
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 +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
|
+
[](https://badge.fury.io/rb/forecasting)
|
4
|
+
[](https://github.com/NeomindLabs/forecasting/actions/workflows/ruby.yml)
|
5
|
+
[](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: []
|