forecasting 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ ### Unreleased
2
+
3
+ **Notes**
4
+
5
+ **Bug Fixes**
6
+
7
+ ## Version 0.1.0 - Jan 29, 2023
8
+
9
+ **Notes**
10
+
11
+ - Initial release
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,12 @@
1
+ module Forecasting
2
+ module Models
3
+ class Address < Base
4
+ attributed :line_1,
5
+ :line_2,
6
+ :city,
7
+ :state,
8
+ :postal_code,
9
+ :country
10
+ end
11
+ end
12
+ 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,10 @@
1
+ module Forecasting
2
+ module Models
3
+ class Card < Base
4
+ attributed :brand,
5
+ :last_four,
6
+ :expiry_month,
7
+ :expiry_year
8
+ end
9
+ end
10
+ 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,7 @@
1
+ module Forecasting
2
+ module Models
3
+ class ColorLabel < Base
4
+ attributed :name, :label
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ module Forecasting
2
+ module Models
3
+ class Discount < Base
4
+ attributed :monthly_percentage,
5
+ :yearly_percentage
6
+ end
7
+ end
8
+ 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,7 @@
1
+ module Forecasting
2
+ module Models
3
+ class FtuxState < Base
4
+ attributed :account_creator, :step
5
+ end
6
+ end
7
+ 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
@@ -0,0 +1,7 @@
1
+ module Forecasting
2
+ module Models
3
+ class User < Base
4
+ attributed :id, :account_ids
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Forecasting
2
+ module Models
3
+ class UserConnection < Base
4
+ attributed :id,
5
+ :person_id,
6
+ :last_active_at
7
+
8
+ def person
9
+ forecast_client.person(id: person_id)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Forecasting
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -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: []