lunchmoney 1.4.0 → 1.5.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 +4 -4
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/build_and_publish_yard_docs.yml +4 -4
- data/.github/workflows/ci.yml +9 -10
- data/.github/workflows/rbi-updater.yml +1 -1
- data/.github/workflows/release_pipeline.yml +1 -1
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/.simplecov +1 -0
- data/.toys/.toys.rb +8 -0
- data/Gemfile +3 -3
- data/Gemfile.lock +102 -78
- data/README.md +0 -2
- data/SECURITY.md +151 -0
- data/bin/check_vcr_version +94 -0
- data/lib/lunchmoney/api.rb +26 -38
- data/lib/lunchmoney/calls/assets.rb +10 -13
- data/lib/lunchmoney/calls/base.rb +59 -7
- data/lib/lunchmoney/calls/budgets.rb +22 -25
- data/lib/lunchmoney/calls/categories.rb +28 -38
- data/lib/lunchmoney/calls/crypto.rb +7 -9
- data/lib/lunchmoney/calls/plaid_accounts.rb +7 -9
- data/lib/lunchmoney/calls/recurring_expenses.rb +4 -5
- data/lib/lunchmoney/calls/tags.rb +3 -4
- data/lib/lunchmoney/calls/transactions.rb +28 -37
- data/lib/lunchmoney/calls/users.rb +3 -4
- data/lib/lunchmoney/configuration.rb +20 -0
- data/lib/lunchmoney/deprecate.rb +35 -0
- data/lib/lunchmoney/objects/asset.rb +6 -1
- data/lib/lunchmoney/objects/object.rb +4 -9
- data/lib/lunchmoney/objects/plaid_account.rb +6 -1
- data/lib/lunchmoney/validators.rb +8 -6
- data/lib/lunchmoney/version.rb +1 -1
- data/lib/lunchmoney.rb +3 -3
- data/lunchmoney.gemspec +1 -1
- data/sorbet/rbi/annotations/activesupport.rbi +40 -0
- data/sorbet/rbi/dsl/active_support/callbacks.rbi +0 -2
- data/sorbet/rbi/gems/{activesupport@7.2.1.rbi → activesupport@8.0.2.1.rbi} +1431 -1028
- data/sorbet/rbi/gems/{ast@2.4.2.rbi → ast@2.4.3.rbi} +4 -3
- data/sorbet/rbi/gems/{base64@0.2.0.rbi → base64@0.3.0.rbi} +76 -39
- data/sorbet/rbi/gems/benchmark@0.4.1.rbi +619 -0
- data/sorbet/rbi/gems/bigdecimal@3.2.2.rbi +275 -0
- data/sorbet/rbi/gems/{concurrent-ruby@1.3.4.rbi → concurrent-ruby@1.3.5.rbi} +44 -32
- data/sorbet/rbi/gems/{connection_pool@2.4.1.rbi → connection_pool@2.5.3.rbi} +1 -0
- data/sorbet/rbi/gems/{dotenv@3.1.2.rbi → dotenv@3.1.8.rbi} +21 -29
- data/sorbet/rbi/gems/{drb@2.2.1.rbi → drb@2.2.3.rbi} +503 -188
- data/sorbet/rbi/gems/{erubi@1.13.0.rbi → erubi@1.13.1.rbi} +14 -9
- data/sorbet/rbi/gems/{faraday-net_http@3.1.1.rbi → faraday-net_http@3.4.1.rbi} +34 -34
- data/sorbet/rbi/gems/{faraday@2.10.1.rbi → faraday@2.13.4.rbi} +507 -171
- data/sorbet/rbi/gems/{hashdiff@1.1.1.rbi → hashdiff@1.2.0.rbi} +5 -3
- data/sorbet/rbi/gems/{i18n@1.14.5.rbi → i18n@1.14.7.rbi} +80 -80
- data/sorbet/rbi/gems/{json@2.7.2.rbi → json@2.13.2.rbi} +988 -226
- data/sorbet/rbi/gems/{kramdown@2.4.0.rbi → kramdown@2.5.1.rbi} +316 -234
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.5.rbi +9 -0
- data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +240 -0
- data/sorbet/rbi/gems/{logger@1.6.0.rbi → logger@1.7.0.rbi} +136 -76
- data/sorbet/rbi/gems/{minitest@5.25.1.rbi → minitest@5.25.5.rbi} +227 -220
- data/sorbet/rbi/gems/{mocha@2.4.5.rbi → mocha@2.7.1.rbi} +154 -118
- data/sorbet/rbi/gems/{net-http@0.4.1.rbi → net-http@0.6.0.rbi} +360 -181
- data/sorbet/rbi/gems/{parser@3.3.4.2.rbi → parser@3.3.9.0.rbi} +326 -308
- data/sorbet/rbi/gems/{prism@0.30.0.rbi → prism@1.4.0.rbi} +12440 -9920
- data/sorbet/rbi/gems/{rack@3.1.7.rbi → rack@3.2.1.rbi} +752 -579
- data/sorbet/rbi/gems/{rake@13.2.1.rbi → rake@13.3.0.rbi} +238 -227
- data/sorbet/rbi/gems/rbi@0.3.6.rbi +5162 -0
- data/sorbet/rbi/gems/rbs@4.0.0.dev.4.rbi +7895 -0
- data/sorbet/rbi/gems/{regexp_parser@2.9.2.rbi → regexp_parser@2.11.2.rbi} +1124 -1013
- data/sorbet/rbi/gems/require-hooks@0.2.2.rbi +110 -0
- data/sorbet/rbi/gems/{rexml@3.3.6.rbi → rexml@3.4.2.rbi} +755 -318
- data/sorbet/rbi/gems/{rubocop-ast@1.32.1.rbi → rubocop-ast@1.46.0.rbi} +1287 -899
- data/sorbet/rbi/gems/{rubocop-minitest@0.35.1.rbi → rubocop-minitest@0.38.2.rbi} +133 -97
- data/sorbet/rbi/gems/{rubocop-rails@2.26.0.rbi → rubocop-rails@2.33.3.rbi} +9874 -6597
- data/sorbet/rbi/gems/{rubocop-shopify@2.15.1.rbi → rubocop-shopify@2.17.1.rbi} +1 -0
- data/sorbet/rbi/gems/{rubocop-sorbet@0.8.5.rbi → rubocop-sorbet@0.10.5.rbi} +804 -83
- data/sorbet/rbi/gems/{rubocop@1.65.1.rbi → rubocop@1.80.1.rbi} +10688 -5103
- data/sorbet/rbi/gems/{securerandom@0.3.1.rbi → securerandom@0.4.1.rbi} +7 -5
- data/sorbet/rbi/gems/{spoom@1.4.2.rbi → spoom@1.7.6.rbi} +1939 -1039
- data/sorbet/rbi/gems/{tapioca@0.16.1.rbi → tapioca@0.17.7.rbi} +765 -821
- data/sorbet/rbi/gems/{thor@1.3.1.rbi → thor@1.4.0.rbi} +139 -91
- data/sorbet/rbi/gems/unicode-display_width@3.1.5.rbi +132 -0
- data/sorbet/rbi/gems/unicode-emoji@4.0.4.rbi +251 -0
- data/sorbet/rbi/gems/{uri@0.13.0.rbi → uri@1.0.3.rbi} +278 -256
- data/sorbet/rbi/gems/{vcr@6.3.1.rbi → vcr@6.3.1-ce35c236fe48899f02ddf780973b44cdb756c0ee.rbi} +140 -123
- data/sorbet/rbi/gems/{webmock@3.23.1.rbi → webmock@3.25.1.rbi} +101 -78
- data/sorbet/rbi/gems/{yard@0.9.36.rbi → yard@0.9.37.rbi} +394 -235
- metadata +55 -53
- data/sorbet/rbi/gems/bigdecimal@3.1.8.rbi +0 -78
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +0 -14237
- data/sorbet/rbi/gems/rbi@0.1.14.rbi +0 -3305
- data/sorbet/rbi/gems/strscan@3.1.0.rbi +0 -9
- data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +0 -65
- /data/sorbet/rbi/gems/{parallel@1.26.3.rbi → parallel@1.27.0.rbi} +0 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "net/http"
|
5
|
+
require "json"
|
6
|
+
require "uri"
|
7
|
+
|
8
|
+
# This script checks if a newer version of VCR has been released beyond 6.3.1
|
9
|
+
# If a newer version is found, it raises an error indicating that we should
|
10
|
+
# switch back to using the released gem instead of the git commit.
|
11
|
+
|
12
|
+
CURRENT_VCR_VERSION = "6.3.1"
|
13
|
+
VCR_COMMIT_SHA = "ce35c236fe48899f02ddf780973b44cdb756c0ee"
|
14
|
+
|
15
|
+
def fetch_latest_vcr_version
|
16
|
+
uri = URI("https://rubygems.org/api/v1/gems/vcr.json")
|
17
|
+
response = Net::HTTP.get_response(uri)
|
18
|
+
|
19
|
+
unless response.is_a?(Net::HTTPSuccess)
|
20
|
+
puts "Warning: Could not fetch VCR version information from RubyGems API"
|
21
|
+
puts "Response: #{response.code} #{response.message}"
|
22
|
+
return nil
|
23
|
+
end
|
24
|
+
|
25
|
+
gem_info = JSON.parse(response.body)
|
26
|
+
gem_info["version"]
|
27
|
+
rescue StandardError => e
|
28
|
+
puts "Warning: Error fetching VCR version information: #{e.message}"
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def version_greater?(version1, version2)
|
33
|
+
# Simple version comparison - splits by dots and compares numerically
|
34
|
+
v1_parts = version1.split(".").map(&:to_i)
|
35
|
+
v2_parts = version2.split(".").map(&:to_i)
|
36
|
+
|
37
|
+
# Pad shorter version with zeros
|
38
|
+
max_length = [v1_parts.length, v2_parts.length].max
|
39
|
+
v1_parts += [0] * (max_length - v1_parts.length)
|
40
|
+
v2_parts += [0] * (max_length - v2_parts.length)
|
41
|
+
|
42
|
+
v1_parts.zip(v2_parts).each do |v1, v2|
|
43
|
+
return true if v1 > v2
|
44
|
+
return false if v1 < v2
|
45
|
+
end
|
46
|
+
|
47
|
+
false # versions are equal
|
48
|
+
end
|
49
|
+
|
50
|
+
def main
|
51
|
+
puts "Checking for newer VCR releases..."
|
52
|
+
puts "Current pinned version: #{CURRENT_VCR_VERSION}"
|
53
|
+
puts "Using commit: #{VCR_COMMIT_SHA}"
|
54
|
+
puts ""
|
55
|
+
|
56
|
+
latest_version = fetch_latest_vcr_version
|
57
|
+
|
58
|
+
if latest_version.nil?
|
59
|
+
puts "Could not determine latest VCR version. Skipping check."
|
60
|
+
exit 0
|
61
|
+
end
|
62
|
+
|
63
|
+
puts "Latest released version: #{latest_version}"
|
64
|
+
|
65
|
+
if version_greater?(latest_version, CURRENT_VCR_VERSION)
|
66
|
+
puts ""
|
67
|
+
puts "🚨 NEWER VCR VERSION AVAILABLE! 🚨"
|
68
|
+
puts ""
|
69
|
+
puts "A newer version of VCR (#{latest_version}) has been released!"
|
70
|
+
puts "This is likely to include the Ruby 3.5+ compatibility fix from commit #{VCR_COMMIT_SHA}."
|
71
|
+
puts ""
|
72
|
+
puts "ACTION REQUIRED:"
|
73
|
+
puts "1. Update Gemfile to use the released version:"
|
74
|
+
puts " gem \"vcr\", \"~> #{latest_version}\", require: false"
|
75
|
+
puts ""
|
76
|
+
puts "2. Remove the git reference and commit SHA"
|
77
|
+
puts ""
|
78
|
+
puts "3. Run 'bundle update vcr' to update to the new version"
|
79
|
+
puts ""
|
80
|
+
puts "4. Test to ensure everything works with the released version"
|
81
|
+
puts ""
|
82
|
+
puts "5. Consider removing this version check script once updated"
|
83
|
+
puts ""
|
84
|
+
|
85
|
+
exit 1
|
86
|
+
else
|
87
|
+
puts "No newer version available. Continuing to use commit #{VCR_COMMIT_SHA}."
|
88
|
+
exit 0
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if __FILE__ == $0
|
93
|
+
main
|
94
|
+
end
|
data/lib/lunchmoney/api.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require_relative "exceptions"
|
5
5
|
require_relative "configuration"
|
6
|
+
require_relative "deprecate"
|
6
7
|
|
7
8
|
require_relative "calls/base"
|
8
9
|
require_relative "objects/object"
|
@@ -29,7 +30,7 @@ module LunchMoney
|
|
29
30
|
|
30
31
|
sig { params(api_key: T.nilable(String)).void }
|
31
32
|
def initialize(api_key: nil)
|
32
|
-
@api_key = T.let(
|
33
|
+
@api_key = T.let(api_key || LunchMoney.configuration.api_key, T.nilable(String))
|
33
34
|
end
|
34
35
|
|
35
36
|
delegate :me, to: :user_calls
|
@@ -40,9 +41,7 @@ module LunchMoney
|
|
40
41
|
# api.me
|
41
42
|
sig { returns(LunchMoney::Calls::Base) }
|
42
43
|
def user_calls
|
43
|
-
|
44
|
-
@user_calls ||= T.let(LunchMoney::Calls::Users.new(api_key:), T.nilable(LunchMoney::Calls::Users))
|
45
|
-
end
|
44
|
+
memoized_call_instance(:@user_calls, LunchMoney::Calls::Users)
|
46
45
|
end
|
47
46
|
|
48
47
|
delegate :categories,
|
@@ -83,9 +82,7 @@ module LunchMoney
|
|
83
82
|
# api.force_delete_category(1234567)
|
84
83
|
sig { returns(LunchMoney::Calls::Base) }
|
85
84
|
def category_calls
|
86
|
-
|
87
|
-
@category_calls ||= T.let(LunchMoney::Calls::Categories.new(api_key:), T.nilable(LunchMoney::Calls::Categories))
|
88
|
-
end
|
85
|
+
memoized_call_instance(:@category_calls, LunchMoney::Calls::Categories)
|
89
86
|
end
|
90
87
|
|
91
88
|
delegate :tags, to: :tag_calls
|
@@ -96,9 +93,7 @@ module LunchMoney
|
|
96
93
|
# api.tags
|
97
94
|
sig { returns(LunchMoney::Calls::Base) }
|
98
95
|
def tag_calls
|
99
|
-
|
100
|
-
@tag_calls ||= T.let(LunchMoney::Calls::Tags.new(api_key:), T.nilable(LunchMoney::Calls::Tags))
|
101
|
-
end
|
96
|
+
memoized_call_instance(:@tag_calls, LunchMoney::Calls::Tags)
|
102
97
|
end
|
103
98
|
|
104
99
|
delegate :transactions,
|
@@ -159,12 +154,7 @@ module LunchMoney
|
|
159
154
|
# api.delete_transaction_group(905483362)
|
160
155
|
sig { returns(LunchMoney::Calls::Base) }
|
161
156
|
def transaction_calls
|
162
|
-
|
163
|
-
@transaction_calls ||= T.let(
|
164
|
-
LunchMoney::Calls::Transactions.new(api_key:),
|
165
|
-
T.nilable(LunchMoney::Calls::Transactions),
|
166
|
-
)
|
167
|
-
end
|
157
|
+
memoized_call_instance(:@transaction_calls, LunchMoney::Calls::Transactions)
|
168
158
|
end
|
169
159
|
|
170
160
|
delegate :recurring_expenses, to: :recurring_expense_calls
|
@@ -175,12 +165,7 @@ module LunchMoney
|
|
175
165
|
# api.recurring_expenses
|
176
166
|
sig { returns(LunchMoney::Calls::Base) }
|
177
167
|
def recurring_expense_calls
|
178
|
-
|
179
|
-
@recurring_expense_calls ||= T.let(
|
180
|
-
LunchMoney::Calls::RecurringExpenses.new(api_key:),
|
181
|
-
T.nilable(LunchMoney::Calls::RecurringExpenses),
|
182
|
-
)
|
183
|
-
end
|
168
|
+
memoized_call_instance(:@recurring_expense_calls, LunchMoney::Calls::RecurringExpenses)
|
184
169
|
end
|
185
170
|
|
186
171
|
delegate :budgets, :upsert_budget, :remove_budget, to: :budget_calls
|
@@ -192,14 +177,12 @@ module LunchMoney
|
|
192
177
|
# @example [Upsert Budget](https://lunchmoney.dev/#upsert-budget)
|
193
178
|
# api = LunchMoney::Api.new
|
194
179
|
# api.upsert_budget(start_date: "2023-01-01", category_id: 777052, amount: 400.99)
|
195
|
-
# @example [Remove Budget(https://lunchmoney.dev/#remove-budget)
|
180
|
+
# @example [Remove Budget](https://lunchmoney.dev/#remove-budget)
|
196
181
|
# api = LunchMoney::Api.new
|
197
182
|
# api.remove_budget(start_date: "2023-01-01", category_id: 777052)
|
198
183
|
sig { returns(LunchMoney::Calls::Base) }
|
199
184
|
def budget_calls
|
200
|
-
|
201
|
-
@budget_calls ||= T.let(LunchMoney::Calls::Budgets.new(api_key:), T.nilable(LunchMoney::Calls::Budgets))
|
202
|
-
end
|
185
|
+
memoized_call_instance(:@budget_calls, LunchMoney::Calls::Budgets)
|
203
186
|
end
|
204
187
|
|
205
188
|
delegate :assets, :create_asset, :update_asset, to: :asset_calls
|
@@ -220,9 +203,7 @@ module LunchMoney
|
|
220
203
|
# api.update_asset(93746, balance: "99.99")
|
221
204
|
sig { returns(LunchMoney::Calls::Base) }
|
222
205
|
def asset_calls
|
223
|
-
|
224
|
-
@asset_calls ||= T.let(LunchMoney::Calls::Assets.new(api_key:), T.nilable(LunchMoney::Calls::Assets))
|
225
|
-
end
|
206
|
+
memoized_call_instance(:@asset_calls, LunchMoney::Calls::Assets)
|
226
207
|
end
|
227
208
|
|
228
209
|
delegate :plaid_accounts, :plaid_accounts_fetch, to: :plaid_account_calls
|
@@ -236,12 +217,7 @@ module LunchMoney
|
|
236
217
|
# api.plaid_accounts_fetch
|
237
218
|
sig { returns(LunchMoney::Calls::Base) }
|
238
219
|
def plaid_account_calls
|
239
|
-
|
240
|
-
@plaid_account_calls ||= T.let(
|
241
|
-
LunchMoney::Calls::PlaidAccounts.new(api_key:),
|
242
|
-
T.nilable(LunchMoney::Calls::PlaidAccounts),
|
243
|
-
)
|
244
|
-
end
|
220
|
+
memoized_call_instance(:@plaid_account_calls, LunchMoney::Calls::PlaidAccounts)
|
245
221
|
end
|
246
222
|
|
247
223
|
delegate :crypto, :update_crypto, to: :crypto_calls
|
@@ -255,9 +231,7 @@ module LunchMoney
|
|
255
231
|
# api.update_crypto(1234567, name: "New Crypto Name")
|
256
232
|
sig { returns(LunchMoney::Calls::Base) }
|
257
233
|
def crypto_calls
|
258
|
-
|
259
|
-
@crypto_calls ||= T.let(LunchMoney::Calls::Crypto.new(api_key:), T.nilable(LunchMoney::Calls::Crypto))
|
260
|
-
end
|
234
|
+
memoized_call_instance(:@crypto_calls, LunchMoney::Calls::Crypto)
|
261
235
|
end
|
262
236
|
|
263
237
|
private
|
@@ -268,5 +242,19 @@ module LunchMoney
|
|
268
242
|
|
269
243
|
yield
|
270
244
|
end
|
245
|
+
|
246
|
+
sig do
|
247
|
+
type_parameters(:T)
|
248
|
+
.params(
|
249
|
+
ivar_name: Symbol,
|
250
|
+
klass: T.class_of(LunchMoney::Calls::Base),
|
251
|
+
)
|
252
|
+
.returns(LunchMoney::Calls::Base)
|
253
|
+
end
|
254
|
+
def memoized_call_instance(ivar_name, klass)
|
255
|
+
with_valid_api_key do
|
256
|
+
instance_variable_get(ivar_name) || instance_variable_set(ivar_name, klass.new(api_key:))
|
257
|
+
end
|
258
|
+
end
|
271
259
|
end
|
272
260
|
end
|
@@ -11,11 +11,10 @@ module LunchMoney
|
|
11
11
|
def assets
|
12
12
|
response = get("assets")
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
LunchMoney::Objects::Asset.new(**asset)
|
14
|
+
handle_api_response(response) do |body|
|
15
|
+
body[:assets].map do |asset|
|
16
|
+
LunchMoney::Objects::Asset.new(**asset)
|
17
|
+
end
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
@@ -50,10 +49,9 @@ module LunchMoney
|
|
50
49
|
|
51
50
|
response = post("assets", params)
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
LunchMoney::Objects::Asset.new(**response.body)
|
52
|
+
handle_api_response(response) do |body|
|
53
|
+
LunchMoney::Objects::Asset.new(**body)
|
54
|
+
end
|
57
55
|
end
|
58
56
|
|
59
57
|
sig do
|
@@ -88,10 +86,9 @@ module LunchMoney
|
|
88
86
|
|
89
87
|
response = put("assets/#{asset_id}", params)
|
90
88
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
LunchMoney::Objects::Asset.new(**response.body)
|
89
|
+
handle_api_response(response) do |body|
|
90
|
+
LunchMoney::Objects::Asset.new(**body)
|
91
|
+
end
|
95
92
|
end
|
96
93
|
end
|
97
94
|
end
|
@@ -22,14 +22,15 @@ module LunchMoney
|
|
22
22
|
|
23
23
|
sig { params(api_key: T.nilable(String)).void }
|
24
24
|
def initialize(api_key: nil)
|
25
|
-
@api_key = T.let(
|
25
|
+
@api_key = T.let(api_key || LunchMoney.configuration.api_key, T.nilable(String))
|
26
|
+
@connections = T.let({}, T::Hash[Symbol, Faraday::Connection])
|
26
27
|
end
|
27
28
|
|
28
29
|
private
|
29
30
|
|
30
31
|
sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
|
31
32
|
def get(endpoint, query_params: nil)
|
32
|
-
connection =
|
33
|
+
connection = connection_for(:flat_params)
|
33
34
|
|
34
35
|
if query_params.present?
|
35
36
|
connection.get(BASE_URL + endpoint, query_params)
|
@@ -40,19 +41,19 @@ module LunchMoney
|
|
40
41
|
|
41
42
|
sig { params(endpoint: String, params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
|
42
43
|
def post(endpoint, params)
|
43
|
-
|
44
|
+
connection_for(:json).post(BASE_URL + endpoint, params)
|
44
45
|
end
|
45
46
|
|
46
47
|
sig { params(endpoint: String, body: T::Hash[Symbol, T.untyped]).returns(Faraday::Response) }
|
47
48
|
def put(endpoint, body)
|
48
|
-
|
49
|
+
connection_for(:json).put(BASE_URL + endpoint) do |req|
|
49
50
|
req.body = body
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
53
54
|
sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
|
54
55
|
def delete(endpoint, query_params: nil)
|
55
|
-
connection =
|
56
|
+
connection = connection_for(:flat_params)
|
56
57
|
|
57
58
|
if query_params.present?
|
58
59
|
connection.delete(BASE_URL + endpoint, query_params)
|
@@ -61,12 +62,24 @@ module LunchMoney
|
|
61
62
|
end
|
62
63
|
end
|
63
64
|
|
65
|
+
sig { params(connection_type: Symbol).returns(Faraday::Connection) }
|
66
|
+
def connection_for(connection_type)
|
67
|
+
@connections[connection_type] ||= case connection_type
|
68
|
+
when :json
|
69
|
+
build_connection(json_request: true)
|
70
|
+
when :flat_params
|
71
|
+
build_connection(flat_params: true)
|
72
|
+
else
|
73
|
+
build_connection
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
64
77
|
sig { params(json_request: T::Boolean, flat_params: T::Boolean).returns(Faraday::Connection) }
|
65
|
-
def
|
78
|
+
def build_connection(json_request: false, flat_params: false)
|
66
79
|
Faraday.new do |conn|
|
67
80
|
conn.request(:authorization, "Bearer", @api_key)
|
68
81
|
conn.request(:json) if json_request
|
69
|
-
|
82
|
+
conn.options.params_encoder = Faraday::FlatParamsEncoder if flat_params
|
70
83
|
conn.response(:json, content_type: /json$/, parser_options: { symbolize_names: true })
|
71
84
|
end
|
72
85
|
end
|
@@ -113,6 +126,45 @@ module LunchMoney
|
|
113
126
|
def clean_params(params)
|
114
127
|
params.reject! { |_key, value| value.nil? }
|
115
128
|
end
|
129
|
+
|
130
|
+
sig do
|
131
|
+
type_parameters(:T)
|
132
|
+
.params(
|
133
|
+
response: Faraday::Response,
|
134
|
+
block: T.proc.params(body: T.untyped).returns(T.type_parameter(:T)),
|
135
|
+
)
|
136
|
+
.returns(T.any(T.type_parameter(:T), LunchMoney::Errors))
|
137
|
+
end
|
138
|
+
def handle_api_response(response, &block)
|
139
|
+
api_errors = errors(response)
|
140
|
+
return api_errors if api_errors.present?
|
141
|
+
|
142
|
+
yield(response.body)
|
143
|
+
end
|
144
|
+
|
145
|
+
sig do
|
146
|
+
type_parameters(:T)
|
147
|
+
.params(
|
148
|
+
response: Faraday::Response,
|
149
|
+
collection_key: Symbol,
|
150
|
+
lazy: T::Boolean,
|
151
|
+
block: T.proc.params(item: T.untyped).returns(T.type_parameter(:T)),
|
152
|
+
)
|
153
|
+
.returns(T.any(T::Enumerable[T.type_parameter(:T)], T::Array[T.type_parameter(:T)], LunchMoney::Errors))
|
154
|
+
end
|
155
|
+
def handle_collection_response(response, collection_key, lazy: false, &block)
|
156
|
+
api_errors = errors(response)
|
157
|
+
return api_errors if api_errors.present?
|
158
|
+
|
159
|
+
collection = response.body[collection_key]
|
160
|
+
return [] unless collection
|
161
|
+
|
162
|
+
if lazy
|
163
|
+
collection.lazy.map(&block)
|
164
|
+
else
|
165
|
+
collection.map(&block)
|
166
|
+
end
|
167
|
+
end
|
116
168
|
end
|
117
169
|
end
|
118
170
|
end
|
@@ -18,29 +18,28 @@ module LunchMoney
|
|
18
18
|
params = clean_params({ start_date:, end_date:, currency: })
|
19
19
|
response = get("budgets", query_params: params)
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
handle_api_response(response) do |body|
|
22
|
+
body.map do |budget|
|
23
|
+
if budget[:data]
|
24
|
+
data_keys = budget[:data].keys
|
25
|
+
data_keys.each do |data_key|
|
26
|
+
budget[:data][data_key] = LunchMoney::Objects::Data.new(**budget[:data][data_key])
|
27
|
+
end
|
28
|
+
end
|
23
29
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
if budget[:config]
|
31
|
+
config_keys = budget[:config].keys
|
32
|
+
config_keys.each do |config_key|
|
33
|
+
budget[:config][config_key] = LunchMoney::Objects::Data.new(**budget[:config][config_key])
|
34
|
+
end
|
29
35
|
end
|
30
|
-
end
|
31
36
|
|
32
|
-
|
33
|
-
|
34
|
-
config_keys.each do |config_key|
|
35
|
-
budget[:config][config_key] = LunchMoney::Objects::Data.new(**budget[:config][config_key])
|
37
|
+
if budget[:recurring]
|
38
|
+
budget[:recurring][:list]&.map! { |recurring| LunchMoney::Objects::RecurringExpenseBase.new(**recurring) }
|
36
39
|
end
|
37
|
-
end
|
38
40
|
|
39
|
-
|
40
|
-
budget[:recurring][:list]&.map! { |recurring| LunchMoney::Objects::RecurringExpenseBase.new(**recurring) }
|
41
|
+
LunchMoney::Objects::Budget.new(**budget)
|
41
42
|
end
|
42
|
-
|
43
|
-
LunchMoney::Objects::Budget.new(**budget)
|
44
43
|
end
|
45
44
|
end
|
46
45
|
|
@@ -59,10 +58,9 @@ module LunchMoney
|
|
59
58
|
params = clean_params({ start_date:, category_id:, amount:, currency: })
|
60
59
|
response = put("budgets", params)
|
61
60
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
response.body
|
61
|
+
handle_api_response(response) do |body|
|
62
|
+
body
|
63
|
+
end
|
66
64
|
end
|
67
65
|
|
68
66
|
sig { params(start_date: String, category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
|
@@ -74,10 +72,9 @@ module LunchMoney
|
|
74
72
|
|
75
73
|
response = delete("budgets", query_params: params)
|
76
74
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
response.body
|
75
|
+
handle_api_response(response) do |body|
|
76
|
+
body
|
77
|
+
end
|
81
78
|
end
|
82
79
|
end
|
83
80
|
end
|
@@ -7,7 +7,7 @@ module LunchMoney
|
|
7
7
|
module Calls
|
8
8
|
# https://lunchmoney.dev/#categories
|
9
9
|
class Categories < LunchMoney::Calls::Base
|
10
|
-
# Valid query parameter
|
10
|
+
# Valid query parameter formats for categories
|
11
11
|
VALID_FORMATS = T.let(
|
12
12
|
[
|
13
13
|
"flattened",
|
@@ -24,13 +24,12 @@ module LunchMoney
|
|
24
24
|
def categories(format: nil)
|
25
25
|
response = get("categories", query_params: categories_params(format:))
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
handle_api_response(response) do |body|
|
28
|
+
body[:categories].map do |category|
|
29
|
+
category[:children]&.map! { |child_category| LunchMoney::Objects::Category.new(**child_category) }
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
LunchMoney::Objects::Category.new(**category)
|
31
|
+
LunchMoney::Objects::Category.new(**category)
|
32
|
+
end
|
34
33
|
end
|
35
34
|
end
|
36
35
|
|
@@ -38,12 +37,11 @@ module LunchMoney
|
|
38
37
|
def category(category_id)
|
39
38
|
response = get("categories/#{category_id}")
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
response.body[:children]&.map! { |child_category| LunchMoney::Objects::ChildCategory.new(**child_category) }
|
40
|
+
handle_api_response(response) do |body|
|
41
|
+
body[:children]&.map! { |child_category| LunchMoney::Objects::ChildCategory.new(**child_category) }
|
45
42
|
|
46
|
-
|
43
|
+
LunchMoney::Objects::Category.new(**body)
|
44
|
+
end
|
47
45
|
end
|
48
46
|
|
49
47
|
sig do
|
@@ -70,10 +68,9 @@ module LunchMoney
|
|
70
68
|
})
|
71
69
|
response = post("categories", params)
|
72
70
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
response.body
|
71
|
+
handle_api_response(response) do |body|
|
72
|
+
body
|
73
|
+
end
|
77
74
|
end
|
78
75
|
|
79
76
|
sig do
|
@@ -89,7 +86,6 @@ module LunchMoney
|
|
89
86
|
end
|
90
87
|
def create_category_group(name:, description: nil, is_income: false, exclude_from_budget: false,
|
91
88
|
exclude_from_totals: false, category_ids: [], new_categories: [])
|
92
|
-
|
93
89
|
params = {
|
94
90
|
name:,
|
95
91
|
description:,
|
@@ -102,10 +98,9 @@ module LunchMoney
|
|
102
98
|
|
103
99
|
response = post("categories/group", params)
|
104
100
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
response.body
|
101
|
+
handle_api_response(response) do |body|
|
102
|
+
body
|
103
|
+
end
|
109
104
|
end
|
110
105
|
|
111
106
|
sig do
|
@@ -122,7 +117,6 @@ module LunchMoney
|
|
122
117
|
end
|
123
118
|
def update_category(category_id, name: nil, description: nil, is_income: nil, exclude_from_budget: nil,
|
124
119
|
exclude_from_totals: nil, archived: nil, group_id: nil)
|
125
|
-
|
126
120
|
params = clean_params({
|
127
121
|
name:,
|
128
122
|
description:,
|
@@ -134,10 +128,9 @@ module LunchMoney
|
|
134
128
|
})
|
135
129
|
response = put("categories/#{category_id}", params)
|
136
130
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
response.body
|
131
|
+
handle_api_response(response) do |body|
|
132
|
+
body
|
133
|
+
end
|
141
134
|
end
|
142
135
|
|
143
136
|
sig do
|
@@ -155,30 +148,27 @@ module LunchMoney
|
|
155
148
|
|
156
149
|
response = post("categories/group/#{group_id}/add", params)
|
157
150
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
LunchMoney::Objects::Category.new(**response.body)
|
151
|
+
handle_api_response(response) do |body|
|
152
|
+
LunchMoney::Objects::Category.new(**body)
|
153
|
+
end
|
162
154
|
end
|
163
155
|
|
164
156
|
sig { params(category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
|
165
157
|
def delete_category(category_id)
|
166
158
|
response = delete("categories/#{category_id}")
|
167
159
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
response.body
|
160
|
+
handle_api_response(response) do |body|
|
161
|
+
body
|
162
|
+
end
|
172
163
|
end
|
173
164
|
|
174
165
|
sig { params(category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
|
175
166
|
def force_delete_category(category_id)
|
176
167
|
response = delete("categories/#{category_id}/force")
|
177
168
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
response.body
|
169
|
+
handle_api_response(response) do |body|
|
170
|
+
body
|
171
|
+
end
|
182
172
|
end
|
183
173
|
|
184
174
|
private
|
@@ -11,11 +11,10 @@ module LunchMoney
|
|
11
11
|
def crypto
|
12
12
|
response = get("crypto")
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
LunchMoney::Objects::Crypto.new(**crypto)
|
14
|
+
handle_api_response(response) do |body|
|
15
|
+
body[:crypto].map do |crypto|
|
16
|
+
LunchMoney::Objects::Crypto.new(**crypto)
|
17
|
+
end
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
@@ -40,10 +39,9 @@ module LunchMoney
|
|
40
39
|
|
41
40
|
response = put("crypto/manual/#{crypto_id}", params)
|
42
41
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
LunchMoney::Objects::CryptoBase.new(**response.body)
|
42
|
+
handle_api_response(response) do |body|
|
43
|
+
LunchMoney::Objects::CryptoBase.new(**body)
|
44
|
+
end
|
47
45
|
end
|
48
46
|
end
|
49
47
|
end
|
@@ -11,11 +11,10 @@ module LunchMoney
|
|
11
11
|
def plaid_accounts
|
12
12
|
response = get("plaid_accounts")
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
LunchMoney::Objects::PlaidAccount.new(**plaid_account)
|
14
|
+
handle_api_response(response) do |body|
|
15
|
+
body[:plaid_accounts].map do |plaid_account|
|
16
|
+
LunchMoney::Objects::PlaidAccount.new(**plaid_account)
|
17
|
+
end
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
@@ -30,10 +29,9 @@ module LunchMoney
|
|
30
29
|
params = clean_params({ start_date:, end_date:, plaid_account_id: })
|
31
30
|
response = post("plaid_accounts/fetch", params)
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
response.body
|
32
|
+
handle_api_response(response) do |body|
|
33
|
+
body
|
34
|
+
end
|
37
35
|
end
|
38
36
|
end
|
39
37
|
end
|