lunchmoney 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.github/dependabot.yml +18 -0
  4. data/.github/workflows/build_and_publish_yard_docs.yml +47 -0
  5. data/.github/workflows/ci.yml +58 -0
  6. data/.github/workflows/dependabot-rbi-updater.yml +43 -0
  7. data/.github/workflows/publish_gem.yml +31 -0
  8. data/.gitignore +62 -0
  9. data/.rubocop.yml +45 -0
  10. data/.ruby-version +1 -0
  11. data/.toys/.toys.rb +10 -0
  12. data/.toys/ci.rb +22 -0
  13. data/.toys/rbi.rb +60 -0
  14. data/.toys/rubocop.rb +10 -0
  15. data/.toys/spoom.rb +15 -0
  16. data/.toys/typecheck.rb +5 -0
  17. data/.yardopts +2 -0
  18. data/Appraisals +22 -0
  19. data/Gemfile +25 -0
  20. data/Gemfile.lock +174 -0
  21. data/LICENSE +21 -0
  22. data/README.md +57 -0
  23. data/bin/console +16 -0
  24. data/bin/rubocop +27 -0
  25. data/bin/setup +8 -0
  26. data/bin/spoom +27 -0
  27. data/bin/srb +27 -0
  28. data/bin/tapioca +27 -0
  29. data/bin/toys +27 -0
  30. data/bin/yard +27 -0
  31. data/lib/lunchmoney/api.rb +147 -0
  32. data/lib/lunchmoney/api_call.rb +109 -0
  33. data/lib/lunchmoney/assets/asset.rb +89 -0
  34. data/lib/lunchmoney/assets/asset_calls.rb +96 -0
  35. data/lib/lunchmoney/budget/budget.rb +74 -0
  36. data/lib/lunchmoney/budget/budget_calls.rb +82 -0
  37. data/lib/lunchmoney/budget/config.rb +38 -0
  38. data/lib/lunchmoney/budget/data.rb +42 -0
  39. data/lib/lunchmoney/categories/category/category.rb +52 -0
  40. data/lib/lunchmoney/categories/category/child_category.rb +42 -0
  41. data/lib/lunchmoney/categories/category_calls.rb +195 -0
  42. data/lib/lunchmoney/configuration.rb +26 -0
  43. data/lib/lunchmoney/crypto/crypto/crypto.rb +43 -0
  44. data/lib/lunchmoney/crypto/crypto/crypto_base.rb +65 -0
  45. data/lib/lunchmoney/crypto/crypto_calls.rb +49 -0
  46. data/lib/lunchmoney/data_object.rb +25 -0
  47. data/lib/lunchmoney/errors.rb +19 -0
  48. data/lib/lunchmoney/exceptions.rb +19 -0
  49. data/lib/lunchmoney/plaid_accounts/plaid_account.rb +73 -0
  50. data/lib/lunchmoney/plaid_accounts/plaid_account_calls.rb +38 -0
  51. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense.rb +65 -0
  52. data/lib/lunchmoney/recurring_expenses/recurring_expense/recurring_expense_base.rb +29 -0
  53. data/lib/lunchmoney/recurring_expenses/recurring_expense_calls.rb +28 -0
  54. data/lib/lunchmoney/tags/tag/tag.rb +20 -0
  55. data/lib/lunchmoney/tags/tag/tag_base.rb +21 -0
  56. data/lib/lunchmoney/tags/tag_calls.rb +20 -0
  57. data/lib/lunchmoney/transactions/transaction/child_transaction.rb +31 -0
  58. data/lib/lunchmoney/transactions/transaction/split.rb +24 -0
  59. data/lib/lunchmoney/transactions/transaction/transaction.rb +156 -0
  60. data/lib/lunchmoney/transactions/transaction/transaction_base.rb +52 -0
  61. data/lib/lunchmoney/transactions/transaction/transaction_modification_base.rb +30 -0
  62. data/lib/lunchmoney/transactions/transaction/update_transaction.rb +43 -0
  63. data/lib/lunchmoney/transactions/transaction_calls.rb +218 -0
  64. data/lib/lunchmoney/user/user.rb +36 -0
  65. data/lib/lunchmoney/user/user_calls.rb +19 -0
  66. data/lib/lunchmoney/validators.rb +43 -0
  67. data/lib/lunchmoney/version.rb +7 -0
  68. data/lib/lunchmoney.rb +54 -0
  69. data/lunchmoney.gemspec +34 -0
  70. data/sorbet/config +5 -0
  71. data/sorbet/rbi/annotations/.gitattributes +1 -0
  72. data/sorbet/rbi/annotations/activesupport.rbi +410 -0
  73. data/sorbet/rbi/annotations/faraday.rbi +17 -0
  74. data/sorbet/rbi/annotations/mocha.rbi +34 -0
  75. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  76. data/sorbet/rbi/annotations/webmock.rbi +9 -0
  77. data/sorbet/rbi/dsl/.gitattributes +1 -0
  78. data/sorbet/rbi/dsl/active_support/callbacks.rbi +22 -0
  79. data/sorbet/rbi/gems/.gitattributes +1 -0
  80. data/sorbet/rbi/gems/activesupport@7.1.3.rbi +18004 -0
  81. data/sorbet/rbi/gems/addressable@2.8.6.rbi +1993 -0
  82. data/sorbet/rbi/gems/appraisal@2.5.0.rbi +621 -0
  83. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  84. data/sorbet/rbi/gems/base64@0.2.0.rbi +508 -0
  85. data/sorbet/rbi/gems/bigdecimal@3.1.6.rbi +77 -0
  86. data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
  87. data/sorbet/rbi/gems/concurrent-ruby@1.2.3.rbi +11590 -0
  88. data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +8 -0
  89. data/sorbet/rbi/gems/crack@0.4.5.rbi +144 -0
  90. data/sorbet/rbi/gems/dotenv@2.8.1.rbi +234 -0
  91. data/sorbet/rbi/gems/drb@2.2.0.rbi +1346 -0
  92. data/sorbet/rbi/gems/erubi@1.12.0.rbi +145 -0
  93. data/sorbet/rbi/gems/faraday-net_http@3.1.0.rbi +146 -0
  94. data/sorbet/rbi/gems/faraday@2.9.0.rbi +2911 -0
  95. data/sorbet/rbi/gems/hashdiff@1.1.0.rbi +352 -0
  96. data/sorbet/rbi/gems/i18n@1.14.1.rbi +2325 -0
  97. data/sorbet/rbi/gems/json@2.7.1.rbi +1561 -0
  98. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
  99. data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
  100. data/sorbet/rbi/gems/minitest@5.21.2.rbi +2197 -0
  101. data/sorbet/rbi/gems/mocha@2.1.0.rbi +3934 -0
  102. data/sorbet/rbi/gems/mutex_m@0.2.0.rbi +93 -0
  103. data/sorbet/rbi/gems/net-http@0.4.1.rbi +4068 -0
  104. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  105. data/sorbet/rbi/gems/parallel@1.24.0.rbi +280 -0
  106. data/sorbet/rbi/gems/parser@3.3.0.5.rbi +5472 -0
  107. data/sorbet/rbi/gems/prettier_print@1.2.1.rbi +951 -0
  108. data/sorbet/rbi/gems/prism@0.19.0.rbi +29883 -0
  109. data/sorbet/rbi/gems/pry-sorbet@0.2.1.rbi +966 -0
  110. data/sorbet/rbi/gems/pry@0.14.2.rbi +10077 -0
  111. data/sorbet/rbi/gems/public_suffix@5.0.4.rbi +935 -0
  112. data/sorbet/rbi/gems/racc@1.7.3.rbi +161 -0
  113. data/sorbet/rbi/gems/rack@3.0.8.rbi +5183 -0
  114. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
  115. data/sorbet/rbi/gems/rake@13.1.0.rbi +3027 -0
  116. data/sorbet/rbi/gems/rbi@0.1.6.rbi +2922 -0
  117. data/sorbet/rbi/gems/regexp_parser@2.9.0.rbi +3771 -0
  118. data/sorbet/rbi/gems/rexml@3.2.6.rbi +4781 -0
  119. data/sorbet/rbi/gems/rubocop-ast@1.30.0.rbi +7117 -0
  120. data/sorbet/rbi/gems/rubocop-minitest@0.34.5.rbi +2576 -0
  121. data/sorbet/rbi/gems/rubocop-rails@2.23.1.rbi +9175 -0
  122. data/sorbet/rbi/gems/rubocop-shopify@2.14.0.rbi +8 -0
  123. data/sorbet/rbi/gems/rubocop-sorbet@0.7.6.rbi +1510 -0
  124. data/sorbet/rbi/gems/rubocop@1.60.1.rbi +57356 -0
  125. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
  126. data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +8 -0
  127. data/sorbet/rbi/gems/spoom@1.2.4.rbi +3777 -0
  128. data/sorbet/rbi/gems/syntax_tree@6.2.0.rbi +23136 -0
  129. data/sorbet/rbi/gems/tapioca@0.12.0.rbi +3506 -0
  130. data/sorbet/rbi/gems/thor@1.3.0.rbi +4312 -0
  131. data/sorbet/rbi/gems/toys-core@0.15.4.rbi +9462 -0
  132. data/sorbet/rbi/gems/toys@0.15.4.rbi +243 -0
  133. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5917 -0
  134. data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +65 -0
  135. data/sorbet/rbi/gems/uri@0.13.0.rbi +2327 -0
  136. data/sorbet/rbi/gems/vcr@6.2.0.rbi +3036 -0
  137. data/sorbet/rbi/gems/webmock@3.19.1.rbi +1768 -0
  138. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
  139. data/sorbet/rbi/gems/yard@0.9.34.rbi +18084 -0
  140. data/sorbet/shims/module.rbi +6 -0
  141. data/sorbet/tapioca/require.rb +10 -0
  142. metadata +228 -0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2021 halorrr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # lunchmoney
2
+
3
+ This gem and readme are very much a work in progress. More to come!
4
+
5
+ This gem is a library of the [LunchMoney API](https://lunchmoney.dev/) for the wonderful [LunchMoney](http://lunchmoney.app/) web app for personal finance & budgeting.
6
+
7
+ You can find the yard docs for this gem [here](https://halorrr.github.io/lunchmoney/)
8
+
9
+ ## Usage
10
+
11
+ ### Installation
12
+
13
+ Add this line to your application's `Gemfile`:
14
+
15
+ ```Ruby
16
+ gem "lunchmoney"
17
+ ```
18
+
19
+ ### Set your lunchmoney token
20
+
21
+ There are a few ways you can set your API token. You can set it manually using a configure block:
22
+
23
+ ```Ruby
24
+ LunchMoney.configure do |config|
25
+ config.api_key = "your_api_key"
26
+ end
27
+ ```
28
+
29
+ The config will also _automatically_ pull in the token if set via environment variable named `LUNCHMONEY_TOKEN`
30
+
31
+ You can also override the config and set your LunchMoney token for a specific API instance via kwarg:
32
+
33
+ ```Ruby
34
+ LunchMoney::Api.new(api_key: "your_api_key")
35
+ ```
36
+
37
+ ### Using the API
38
+
39
+ Create an instance of the api, then call the endpoint you need:
40
+
41
+ ```Ruby
42
+ api = LunchMoney::Api.new
43
+ api.categories
44
+ ```
45
+
46
+ ## Contributing to this repo
47
+
48
+ Feel free to contribute and submit PRs to improve this gem
49
+
50
+ ## Releasing a new gem version
51
+
52
+ 1. Bump the `VERSION` constant in `lib/lunchmoney/version.rb`
53
+ 2. Run `bundle install`
54
+ 3. Commit and push up the change in a PR
55
+ 4. Merge the PR
56
+ 5. Create a new tag and release with the name version as v0.0.0
57
+ 6. A Github action will kick off and publish the new gem version
data/bin/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "dotenv/load"
6
+ require "lunchmoney"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+ require "irb"
16
+ IRB.start(__FILE__)
data/bin/rubocop ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rubocop", "rubocop")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/spoom ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'spoom' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("spoom", "spoom")
data/bin/srb ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'srb' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("sorbet", "srb")
data/bin/tapioca ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'tapioca' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("tapioca", "tapioca")
data/bin/toys ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'toys' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("toys", "toys")
data/bin/yard ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'yard' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("yard", "yard")
@@ -0,0 +1,147 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "exceptions"
5
+ require_relative "configuration"
6
+
7
+ require_relative "api_call"
8
+ require_relative "data_object"
9
+ require_relative "user/user_calls"
10
+ require_relative "categories/category_calls"
11
+ require_relative "tags/tag_calls"
12
+ require_relative "transactions/transaction_calls"
13
+ require_relative "recurring_expenses/recurring_expense_calls"
14
+ require_relative "budget/budget_calls"
15
+ require_relative "assets/asset_calls"
16
+ require_relative "plaid_accounts/plaid_account_calls"
17
+ require_relative "crypto/crypto_calls"
18
+
19
+ module LunchMoney
20
+ # The main API class that a user should interface through the method of any individual call is delegated through here
21
+ # so that it is never necessary to go through things like `LunchMoney::UserCalls.new.user` instead you can directly
22
+ # call the endpoint with LunchMoney::Api.new.user and it will be delegated to the correct call.
23
+ class Api
24
+ sig { returns(T.nilable(String)) }
25
+ attr_reader :api_key
26
+
27
+ sig { params(api_key: T.nilable(String)).void }
28
+ def initialize(api_key: nil)
29
+ @api_key = T.let((api_key || LunchMoney.configuration.api_key), T.nilable(String))
30
+ end
31
+
32
+ delegate :me, to: :user_calls
33
+
34
+ sig { returns(LunchMoney::ApiCall) }
35
+ def user_calls
36
+ with_valid_api_key do
37
+ @user_calls ||= T.let(LunchMoney::UserCalls.new(api_key:), T.nilable(LunchMoney::UserCalls))
38
+ end
39
+ end
40
+
41
+ delegate :categories,
42
+ :category,
43
+ :create_category,
44
+ :create_category_group,
45
+ :update_category,
46
+ :add_to_category_group,
47
+ :delete_category,
48
+ :force_delete_category,
49
+ to: :category_calls
50
+
51
+ sig { returns(LunchMoney::ApiCall) }
52
+ def category_calls
53
+ with_valid_api_key do
54
+ @category_calls ||= T.let(LunchMoney::CategoryCalls.new(api_key:), T.nilable(LunchMoney::CategoryCalls))
55
+ end
56
+ end
57
+
58
+ delegate :tags, to: :tag_calls
59
+
60
+ sig { returns(LunchMoney::ApiCall) }
61
+ def tag_calls
62
+ with_valid_api_key do
63
+ @tag_calls ||= T.let(LunchMoney::TagCalls.new(api_key:), T.nilable(LunchMoney::TagCalls))
64
+ end
65
+ end
66
+
67
+ delegate :transactions,
68
+ :transaction,
69
+ :insert_transactions,
70
+ :update_transaction,
71
+ :unsplit_transaction,
72
+ :transaction_group,
73
+ :create_transaction_group,
74
+ :delete_transaction_group,
75
+ to: :transaction_calls
76
+
77
+ sig { returns(LunchMoney::ApiCall) }
78
+ def transaction_calls
79
+ with_valid_api_key do
80
+ @transaction_calls ||= T.let(
81
+ LunchMoney::TransactionCalls.new(api_key:),
82
+ T.nilable(LunchMoney::TransactionCalls),
83
+ )
84
+ end
85
+ end
86
+
87
+ delegate :recurring_expenses, to: :recurring_expense_calls
88
+
89
+ sig { returns(LunchMoney::ApiCall) }
90
+ def recurring_expense_calls
91
+ with_valid_api_key do
92
+ @recurring_expense_calls ||= T.let(
93
+ LunchMoney::RecurringExpenseCalls.new(api_key:),
94
+ T.nilable(LunchMoney::RecurringExpenseCalls),
95
+ )
96
+ end
97
+ end
98
+
99
+ delegate :budgets, :upsert_budget, :remove_budget, to: :budget_calls
100
+
101
+ sig { returns(LunchMoney::ApiCall) }
102
+ def budget_calls
103
+ with_valid_api_key do
104
+ @budget_calls ||= T.let(LunchMoney::BudgetCalls.new(api_key:), T.nilable(LunchMoney::BudgetCalls))
105
+ end
106
+ end
107
+
108
+ delegate :assets, :create_asset, :update_asset, to: :asset_calls
109
+
110
+ sig { returns(LunchMoney::ApiCall) }
111
+ def asset_calls
112
+ with_valid_api_key do
113
+ @asset_calls ||= T.let(LunchMoney::AssetCalls.new(api_key:), T.nilable(LunchMoney::AssetCalls))
114
+ end
115
+ end
116
+
117
+ delegate :plaid_accounts, :plaid_accounts_fetch, to: :plaid_account_calls
118
+
119
+ sig { returns(LunchMoney::ApiCall) }
120
+ def plaid_account_calls
121
+ with_valid_api_key do
122
+ @plaid_account_calls ||= T.let(
123
+ LunchMoney::PlaidAccountCalls.new(api_key:),
124
+ T.nilable(LunchMoney::PlaidAccountCalls),
125
+ )
126
+ end
127
+ end
128
+
129
+ delegate :crypto, :update_crypto, to: :crypto_calls
130
+
131
+ sig { returns(LunchMoney::ApiCall) }
132
+ def crypto_calls
133
+ with_valid_api_key do
134
+ @crypto_calls ||= T.let(LunchMoney::CryptoCalls.new(api_key:), T.nilable(LunchMoney::CryptoCalls))
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ sig { params(block: T.proc.returns(LunchMoney::ApiCall)).returns(LunchMoney::ApiCall) }
141
+ def with_valid_api_key(&block)
142
+ raise(InvalidApiKey, "API key is missing or invalid") if api_key.blank?
143
+
144
+ yield
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,109 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "errors"
5
+
6
+ module LunchMoney
7
+ # Base class for all API call types
8
+ class ApiCall
9
+ # Base URL used for API calls
10
+ BASE_URL = "https://dev.lunchmoney.app/v1/"
11
+
12
+ sig { returns(T.nilable(String)) }
13
+ attr_reader :api_key
14
+
15
+ sig { params(api_key: T.nilable(String)).void }
16
+ def initialize(api_key: nil)
17
+ @api_key = T.let((api_key || LunchMoney.configuration.api_key), T.nilable(String))
18
+ end
19
+
20
+ private
21
+
22
+ sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
23
+ def get(endpoint, query_params: nil)
24
+ connection = request(flat_params: true)
25
+
26
+ if query_params.present?
27
+ connection.get(BASE_URL + endpoint, query_params)
28
+ else
29
+ connection.get(BASE_URL + endpoint)
30
+ end
31
+ end
32
+
33
+ sig { params(endpoint: String, params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
34
+ def post(endpoint, params)
35
+ request(json_request: true).post(BASE_URL + endpoint, params)
36
+ end
37
+
38
+ sig { params(endpoint: String, body: T::Hash[Symbol, T.untyped]).returns(Faraday::Response) }
39
+ def put(endpoint, body)
40
+ request(json_request: true).put(BASE_URL + endpoint) do |req|
41
+ req.body = body
42
+ end
43
+ end
44
+
45
+ sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
46
+ def delete(endpoint, query_params: nil)
47
+ connection = request(flat_params: true)
48
+
49
+ if query_params.present?
50
+ connection.delete(BASE_URL + endpoint, query_params)
51
+ else
52
+ connection.delete(BASE_URL + endpoint)
53
+ end
54
+ end
55
+
56
+ sig { params(json_request: T::Boolean, flat_params: T::Boolean).returns(Faraday::Connection) }
57
+ def request(json_request: false, flat_params: false)
58
+ Faraday.new do |conn|
59
+ conn.request(:authorization, "Bearer", @api_key)
60
+ conn.request(:json) if json_request
61
+ # conn.options.params_encoder = Faraday::FlatParamsEncoder if flat_params
62
+ conn.response(:json, content_type: /json$/, parser_options: { symbolize_names: true })
63
+ end
64
+ end
65
+
66
+ sig { params(response: Faraday::Response).returns(LunchMoney::Errors) }
67
+ def errors(response)
68
+ body = response.body
69
+
70
+ return parse_errors(body) unless error_hash(body).nil?
71
+
72
+ LunchMoney::Errors.new
73
+ end
74
+
75
+ sig { params(body: T::Hash[Symbol, T.any(String, T::Array[String])]).returns(LunchMoney::Errors) }
76
+ def parse_errors(body)
77
+ errors = error_hash(body)
78
+ api_errors = LunchMoney::Errors.new
79
+ return api_errors if errors.blank?
80
+
81
+ case errors
82
+ when String
83
+ api_errors << errors
84
+ when Array
85
+ errors.each { |error| api_errors << error }
86
+ end
87
+
88
+ api_errors
89
+ end
90
+
91
+ sig { params(body: T.untyped).returns(T.untyped) }
92
+ def error_hash(body)
93
+ return unless body.is_a?(Hash)
94
+
95
+ if body[:error]
96
+ body[:error]
97
+ elsif body[:errors]
98
+ body[:errors]
99
+ elsif body[:name] == "Error"
100
+ body[:message]
101
+ end
102
+ end
103
+
104
+ sig { params(params: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
105
+ def clean_params(params)
106
+ params.reject! { |_key, value| value.nil? }
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,89 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LunchMoney
5
+ # https://lunchmoney.dev/#assets-object
6
+ class Asset < LunchMoney::DataObject
7
+ include LunchMoney::Validators
8
+
9
+ sig { returns(Integer) }
10
+ attr_accessor :id
11
+
12
+ sig { returns(String) }
13
+ attr_reader :type_name, :balance_as_of, :created_at
14
+
15
+ sig { returns(String) }
16
+ attr_accessor :name, :balance, :currency
17
+
18
+ sig { returns(T.nilable(String)) }
19
+ attr_accessor :display_name, :closed_on, :institution_name, :subtype_name
20
+
21
+ sig { returns(T::Boolean) }
22
+ attr_accessor :exclude_transactions
23
+
24
+ # Valid asset type names
25
+ VALID_TYPE_NAMES = T.let(
26
+ [
27
+ "cash",
28
+ "credit",
29
+ "investment",
30
+ "real estate",
31
+ "loan",
32
+ "vehicle",
33
+ "cryptocurrency",
34
+ "employee compensation",
35
+ "other liability",
36
+ "other asset",
37
+ ],
38
+ T::Array[String],
39
+ )
40
+
41
+ sig do
42
+ params(
43
+ created_at: String,
44
+ type_name: String,
45
+ name: String,
46
+ balance: String,
47
+ balance_as_of: String,
48
+ currency: String,
49
+ exclude_transactions: T::Boolean,
50
+ id: Integer,
51
+ subtype_name: T.nilable(String),
52
+ display_name: T.nilable(String),
53
+ closed_on: T.nilable(String),
54
+ institution_name: T.nilable(String),
55
+ ).void
56
+ end
57
+ def initialize(created_at:, type_name:, name:, balance:, balance_as_of:, currency:, exclude_transactions:, id:,
58
+ subtype_name: nil, display_name: nil, closed_on: nil, institution_name: nil)
59
+ super()
60
+ @created_at = T.let(validate_iso8601!(created_at), String)
61
+ @type_name = T.let(validate_one_of!(type_name, VALID_TYPE_NAMES), String)
62
+ @name = name
63
+ @balance = balance
64
+ @balance_as_of = T.let(validate_iso8601!(balance_as_of), String)
65
+ @currency = currency
66
+ @exclude_transactions = exclude_transactions
67
+ @id = id
68
+ @subtype_name = subtype_name
69
+ @display_name = display_name
70
+ @closed_on = closed_on
71
+ @institution_name = institution_name
72
+ end
73
+
74
+ sig { params(name: String).void }
75
+ def type_name=(name)
76
+ @type_name = validate_one_of!(name, VALID_TYPE_NAMES)
77
+ end
78
+
79
+ sig { params(time: String).void }
80
+ def balance_as_of=(time)
81
+ @balance_as_of = validate_iso8601!(time)
82
+ end
83
+
84
+ sig { params(time: String).void }
85
+ def created_at=(time)
86
+ @created_at = validate_iso8601!(time)
87
+ end
88
+ end
89
+ end