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 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: []