fin-infra 0.1.62__tar.gz → 0.1.64__tar.gz

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.
Files changed (179) hide show
  1. {fin_infra-0.1.62 → fin_infra-0.1.64}/PKG-INFO +1 -1
  2. {fin_infra-0.1.62 → fin_infra-0.1.64}/pyproject.toml +1 -1
  3. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/spending.py +8 -6
  4. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/banking/__init__.py +7 -4
  5. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/brokerage/__init__.py +4 -2
  6. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/categorization/__init__.py +1 -1
  7. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/categorization/add.py +2 -1
  8. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/categorization/ease.py +1 -1
  9. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/categorization/engine.py +1 -1
  10. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/categorization/llm_layer.py +2 -2
  11. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/compliance/__init__.py +4 -3
  12. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/credit/add.py +3 -2
  13. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/credit/experian/auth.py +3 -2
  14. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/credit/experian/client.py +2 -2
  15. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/credit/experian/provider.py +2 -2
  16. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/add.py +2 -2
  17. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/management.py +3 -3
  18. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/milestones.py +2 -2
  19. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/__init__.py +11 -4
  20. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/ease.py +11 -7
  21. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/models.py +1 -1
  22. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/providers/plaid.py +3 -3
  23. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/providers/snaptrade.py +2 -2
  24. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/markets/__init__.py +8 -3
  25. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/normalization/providers/exchangerate.py +3 -3
  26. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/banking/plaid_client.py +4 -3
  27. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/banking/teller_client.py +13 -7
  28. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/brokerage/alpaca.py +5 -5
  29. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/market/ccxt_crypto.py +5 -3
  30. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/tax/mock.py +3 -3
  31. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/detectors_llm.py +2 -2
  32. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/insights.py +2 -2
  33. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/normalizer.py +2 -1
  34. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/normalizers.py +2 -2
  35. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/security/encryption.py +2 -2
  36. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/security/pii_patterns.py +1 -1
  37. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/security/token_store.py +3 -1
  38. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/utils/http.py +3 -2
  39. {fin_infra-0.1.62 → fin_infra-0.1.64}/LICENSE +0 -0
  40. {fin_infra-0.1.62 → fin_infra-0.1.64}/README.md +0 -0
  41. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/__init__.py +0 -0
  42. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/__main__.py +0 -0
  43. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/__init__.py +0 -0
  44. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/add.py +0 -0
  45. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/cash_flow.py +0 -0
  46. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/ease.py +0 -0
  47. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/models.py +0 -0
  48. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/portfolio.py +0 -0
  49. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/projections.py +0 -0
  50. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/rebalancing.py +0 -0
  51. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/savings.py +0 -0
  52. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/analytics/scenarios.py +0 -0
  53. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/banking/history.py +0 -0
  54. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/banking/utils.py +0 -0
  55. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/__init__.py +0 -0
  56. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/add.py +0 -0
  57. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/alerts.py +0 -0
  58. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/ease.py +0 -0
  59. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/models.py +0 -0
  60. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/scaffold_templates/README.md +0 -0
  61. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/scaffold_templates/__init__.py +0 -0
  62. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/scaffold_templates/models.py.tmpl +0 -0
  63. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/scaffold_templates/repository.py.tmpl +0 -0
  64. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/scaffold_templates/schemas.py.tmpl +0 -0
  65. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/templates.py +0 -0
  66. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/budgets/tracker.py +0 -0
  67. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/cashflows/__init__.py +0 -0
  68. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/cashflows/core.py +0 -0
  69. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/categorization/models.py +0 -0
  70. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/categorization/rules.py +0 -0
  71. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/categorization/taxonomy.py +0 -0
  72. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/chat/__init__.py +0 -0
  73. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/chat/ease.py +0 -0
  74. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/chat/planning.py +0 -0
  75. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/cli/__init__.py +0 -0
  76. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/cli/cmds/__init__.py +0 -0
  77. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/cli/cmds/scaffold_cmds.py +0 -0
  78. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/clients/__init__.py +0 -0
  79. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/clients/base.py +0 -0
  80. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/clients/plaid.py +0 -0
  81. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/credit/__init__.py +0 -0
  82. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/credit/experian/__init__.py +0 -0
  83. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/credit/experian/parser.py +0 -0
  84. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/credit/mock.py +0 -0
  85. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/crypto/__init__.py +0 -0
  86. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/crypto/insights.py +0 -0
  87. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/documents/__init__.py +0 -0
  88. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/documents/add.py +0 -0
  89. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/documents/analysis.py +0 -0
  90. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/documents/ease.py +0 -0
  91. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/documents/models.py +0 -0
  92. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/documents/ocr.py +0 -0
  93. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/documents/storage.py +0 -0
  94. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/exceptions.py +0 -0
  95. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/__init__.py +0 -0
  96. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/funding.py +0 -0
  97. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/models.py +0 -0
  98. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/scaffold_templates/README.md +0 -0
  99. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/scaffold_templates/__init__.py +0 -0
  100. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/scaffold_templates/models.py.tmpl +0 -0
  101. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/scaffold_templates/repository.py.tmpl +0 -0
  102. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/goals/scaffold_templates/schemas.py.tmpl +0 -0
  103. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/insights/__init__.py +0 -0
  104. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/insights/aggregator.py +0 -0
  105. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/insights/models.py +0 -0
  106. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/add.py +0 -0
  107. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/providers/__init__.py +0 -0
  108. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/providers/base.py +0 -0
  109. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/scaffold_templates/README.md +0 -0
  110. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/scaffold_templates/__init__.py +0 -0
  111. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/scaffold_templates/models.py.tmpl +0 -0
  112. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/scaffold_templates/repository.py.tmpl +0 -0
  113. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/investments/scaffold_templates/schemas.py.tmpl +0 -0
  114. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/__init__.py +0 -0
  115. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/accounts.py +0 -0
  116. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/brokerage.py +0 -0
  117. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/candle.py +0 -0
  118. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/credit.py +0 -0
  119. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/money.py +0 -0
  120. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/quotes.py +0 -0
  121. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/tax.py +0 -0
  122. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/models/transactions.py +0 -0
  123. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/__init__.py +0 -0
  124. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/add.py +0 -0
  125. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/aggregator.py +0 -0
  126. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/calculator.py +0 -0
  127. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/ease.py +0 -0
  128. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/goals.py +0 -0
  129. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/insights.py +0 -0
  130. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/models.py +0 -0
  131. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/scaffold_templates/README.md +0 -0
  132. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/scaffold_templates/__init__.py +0 -0
  133. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/scaffold_templates/models.py.tmpl +0 -0
  134. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/scaffold_templates/repository.py.tmpl +0 -0
  135. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/net_worth/scaffold_templates/schemas.py.tmpl +0 -0
  136. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/normalization/__init__.py +0 -0
  137. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/normalization/currency_converter.py +0 -0
  138. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/normalization/models.py +0 -0
  139. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/normalization/providers/__init__.py +0 -0
  140. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/normalization/providers/static_mappings.py +0 -0
  141. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/normalization/symbol_resolver.py +0 -0
  142. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/obs/__init__.py +0 -0
  143. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/obs/classifier.py +0 -0
  144. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/__init__.py +0 -0
  145. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/banking/base.py +0 -0
  146. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/base.py +0 -0
  147. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/brokerage/base.py +0 -0
  148. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/credit/experian.py +0 -0
  149. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/identity/stripe_identity.py +0 -0
  150. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/market/alphavantage.py +0 -0
  151. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/market/base.py +0 -0
  152. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/market/coingecko.py +0 -0
  153. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/market/yahoo.py +0 -0
  154. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/registry.py +0 -0
  155. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/tax/__init__.py +0 -0
  156. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/tax/irs.py +0 -0
  157. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/providers/tax/taxbit.py +0 -0
  158. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/py.typed +0 -0
  159. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/__init__.py +0 -0
  160. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/add.py +0 -0
  161. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/detector.py +0 -0
  162. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/ease.py +0 -0
  163. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/models.py +0 -0
  164. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/recurring/summary.py +0 -0
  165. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/scaffold/__init__.py +0 -0
  166. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/scaffold/budgets.py +0 -0
  167. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/scaffold/goals.py +0 -0
  168. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/security/__init__.py +0 -0
  169. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/security/add.py +0 -0
  170. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/security/audit.py +0 -0
  171. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/security/models.py +0 -0
  172. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/security/pii_filter.py +0 -0
  173. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/settings.py +0 -0
  174. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/tax/__init__.py +0 -0
  175. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/tax/add.py +0 -0
  176. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/tax/tlh.py +0 -0
  177. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/utils/__init__.py +0 -0
  178. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/utils/retry.py +0 -0
  179. {fin_infra-0.1.62 → fin_infra-0.1.64}/src/fin_infra/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fin-infra
3
- Version: 0.1.62
3
+ Version: 0.1.64
4
4
  Summary: Financial infrastructure toolkit: banking connections, market data, credit, cashflows, and brokerage integrations
5
5
  License: MIT
6
6
  Keywords: finance,banking,plaid,brokerage,markets,credit,tax,cashflow,fintech,infra
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "fin-infra"
3
- version = "0.1.62"
3
+ version = "0.1.64"
4
4
  description = "Financial infrastructure toolkit: banking connections, market data, credit, cashflows, and brokerage integrations"
5
5
  authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
6
6
  license = "MIT"
@@ -162,12 +162,13 @@ async def analyze_spending(
162
162
  )
163
163
 
164
164
  return SpendingInsight(
165
- top_merchants=top_merchants,
166
- category_breakdown=dict(category_totals),
165
+ # Convert Decimal to float for model compatibility (intentional for Pydantic field types)
166
+ top_merchants=[(m, float(v)) for m, v in top_merchants],
167
+ category_breakdown={k: float(v) for k, v in category_totals.items()},
167
168
  spending_trends=spending_trends,
168
169
  anomalies=anomalies,
169
170
  period_days=days,
170
- total_spending=total_spending,
171
+ total_spending=float(total_spending) if total_spending else 0.0,
171
172
  )
172
173
 
173
174
 
@@ -359,9 +360,10 @@ async def _detect_spending_anomalies(
359
360
  anomalies.append(
360
361
  SpendingAnomaly(
361
362
  category=category,
362
- current_amount=current_amount,
363
- average_amount=average_amount,
364
- deviation_percent=deviation_percent,
363
+ # Convert Decimal to float for model compatibility (intentional for Pydantic field types)
364
+ current_amount=float(current_amount),
365
+ average_amount=float(average_amount),
366
+ deviation_percent=float(deviation_percent),
365
367
  severity=severity,
366
368
  )
367
369
  )
@@ -45,7 +45,7 @@ from __future__ import annotations
45
45
 
46
46
  import os
47
47
  from datetime import date
48
- from typing import TYPE_CHECKING, Optional
48
+ from typing import TYPE_CHECKING, Optional, cast
49
49
 
50
50
  from pydantic import BaseModel, Field
51
51
 
@@ -199,7 +199,7 @@ def easy_banking(provider: str = "teller", **config) -> BankingProvider:
199
199
  }
200
200
 
201
201
  # Use provider registry to dynamically load and configure provider
202
- return resolve("banking", provider, **config)
202
+ return cast(BankingProvider, resolve("banking", provider, **config))
203
203
 
204
204
 
205
205
  def add_banking(
@@ -397,10 +397,13 @@ def add_banking(
397
397
  }
398
398
  """
399
399
  # Get all transactions from provider
400
+ # Convert date to ISO string format as expected by BankingProvider.transactions()
401
+ start_date_str: str | None = start_date.isoformat() if start_date else None
402
+ end_date_str: str | None = end_date.isoformat() if end_date else None
400
403
  transactions = banking.transactions(
401
404
  access_token=access_token,
402
- start_date=start_date,
403
- end_date=end_date,
405
+ start_date=start_date_str,
406
+ end_date=end_date_str,
404
407
  )
405
408
 
406
409
  # Apply filters
@@ -212,7 +212,9 @@ def add_brokerage(
212
212
 
213
213
  # Initialize provider if string or None
214
214
  if isinstance(provider, str):
215
- brokerage_provider = easy_brokerage(provider=provider, mode=mode, **config)
215
+ # Cast provider string to Literal type for type checker
216
+ provider_literal: Literal["alpaca"] | None = provider if provider == "alpaca" else None # type: ignore[assignment]
217
+ brokerage_provider = easy_brokerage(provider=provider_literal, mode=mode, **config)
216
218
  elif provider is None:
217
219
  brokerage_provider = easy_brokerage(mode=mode, **config)
218
220
  else:
@@ -241,7 +243,7 @@ def add_brokerage(
241
243
  Returns list of positions with symbol, quantity, P/L, etc.
242
244
  """
243
245
  try:
244
- positions = brokerage_provider.positions()
246
+ positions = list(brokerage_provider.positions()) # Convert Iterable to list for len()
245
247
  return {"positions": positions, "count": len(positions)}
246
248
  except Exception as e:
247
249
  raise HTTPException(status_code=500, detail=f"Error fetching positions: {str(e)}")
@@ -44,7 +44,7 @@ from .taxonomy import Category, CategoryGroup, get_all_categories, get_category_
44
44
  try:
45
45
  from .llm_layer import LLMCategorizer
46
46
  except ImportError:
47
- LLMCategorizer = None
47
+ LLMCategorizer = None # type: ignore[assignment,misc]
48
48
 
49
49
  __all__ = [
50
50
  # Easy setup
@@ -96,7 +96,8 @@ def add_categorization(
96
96
  start_time = time.perf_counter()
97
97
 
98
98
  try:
99
- prediction = engine.categorize(
99
+ # Await the async categorize method
100
+ prediction = await engine.categorize(
100
101
  merchant_name=request.merchant_name,
101
102
  user_id=request.user_id,
102
103
  include_alternatives=request.include_alternatives,
@@ -13,7 +13,7 @@ from .engine import CategorizationEngine
13
13
  try:
14
14
  from .llm_layer import LLMCategorizer
15
15
  except ImportError:
16
- LLMCategorizer = None
16
+ LLMCategorizer = None # type: ignore[assignment,misc]
17
17
 
18
18
 
19
19
  def easy_categorization(
@@ -23,7 +23,7 @@ from .taxonomy import Category
23
23
  try:
24
24
  from .llm_layer import LLMCategorizer
25
25
  except ImportError:
26
- LLMCategorizer = None
26
+ LLMCategorizer = None # type: ignore[assignment,misc]
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
@@ -15,7 +15,7 @@ Expected performance:
15
15
 
16
16
  import hashlib
17
17
  import logging
18
- from typing import Optional, List, Tuple
18
+ from typing import Optional, List, Tuple, cast
19
19
  from pydantic import BaseModel, Field
20
20
 
21
21
  # ai-infra imports
@@ -245,7 +245,7 @@ class LLMCategorizer:
245
245
  f"Must be one of {len(valid_categories)} valid categories."
246
246
  )
247
247
 
248
- return response
248
+ return cast(CategoryPrediction, response)
249
249
 
250
250
  def _build_system_prompt(self) -> str:
251
251
  """Build system prompt with few-shot examples (reused across all requests)."""
@@ -21,7 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  import logging
23
23
  from datetime import datetime
24
- from typing import Any, Callable, TYPE_CHECKING
24
+ from typing import Any, Callable, TYPE_CHECKING, cast
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from fastapi import FastAPI, Request, Response
@@ -118,7 +118,8 @@ def add_compliance_tracking(
118
118
 
119
119
  # Track only GET requests (data access)
120
120
  if method != "GET":
121
- return await call_next(request)
121
+ from starlette.responses import Response as StarletteResponse
122
+ return cast("Response", await call_next(request))
122
123
 
123
124
  # Determine if path is a compliance-tracked endpoint
124
125
  event = None
@@ -148,7 +149,7 @@ def add_compliance_tracking(
148
149
  if on_event:
149
150
  on_event(event, context)
150
151
 
151
- return response
152
+ return cast("Response", response)
152
153
 
153
154
  logger.info(
154
155
  "Compliance tracking enabled",
@@ -23,6 +23,7 @@ Example:
23
23
  """
24
24
 
25
25
  import logging
26
+ from typing import cast
26
27
 
27
28
  from fastapi import FastAPI, Depends, HTTPException, status
28
29
 
@@ -175,7 +176,7 @@ def add_credit(
175
176
  # Don't fail request if webhook publishing fails
176
177
  logger.warning(f"Failed to publish credit.score_changed webhook: {e}")
177
178
 
178
- return score
179
+ return cast(CreditScore, score)
179
180
 
180
181
  @router.post("/report", response_model=CreditReport)
181
182
  @credit_resource.cache_read(ttl=cache_ttl, suffix="report")
@@ -219,7 +220,7 @@ def add_credit(
219
220
  detail="Credit bureau service unavailable",
220
221
  )
221
222
 
222
- return report
223
+ return cast(CreditReport, report)
223
224
 
224
225
  # Mount router with dual routes (with/without trailing slash)
225
226
  app.include_router(router, include_in_schema=True)
@@ -24,6 +24,7 @@ Example:
24
24
  """
25
25
 
26
26
  import base64
27
+ from typing import cast
27
28
 
28
29
  import httpx
29
30
  from svc_infra.cache import cache_read
@@ -85,7 +86,7 @@ class ExperianAuthManager:
85
86
  >>> headers = {"Authorization": f"Bearer {token}"}
86
87
  """
87
88
  # Call the cached implementation with client_id for cache key
88
- return await self._get_token_cached(client_id=self.client_id)
89
+ return cast(str, await self._get_token_cached(client_id=self.client_id))
89
90
 
90
91
  @cache_read(
91
92
  key="oauth_token:experian:{client_id}", # Use client_id for uniqueness
@@ -140,7 +141,7 @@ class ExperianAuthManager:
140
141
 
141
142
  # Parse and return token
142
143
  data = response.json()
143
- return data["access_token"]
144
+ return cast(str, data["access_token"])
144
145
 
145
146
  async def invalidate(self) -> None:
146
147
  """Invalidate cached token for THIS client (force refresh on next get_token call).
@@ -14,7 +14,7 @@ Example:
14
14
  >>> data = await client.get_credit_score("user123")
15
15
  """
16
16
 
17
- from typing import Any
17
+ from typing import Any, cast
18
18
 
19
19
  import httpx
20
20
  from tenacity import (
@@ -155,7 +155,7 @@ class ExperianClient:
155
155
  **kwargs,
156
156
  )
157
157
  response.raise_for_status()
158
- return response.json()
158
+ return cast(dict[str, Any], response.json())
159
159
 
160
160
  except httpx.HTTPStatusError as e:
161
161
  # Parse error response
@@ -31,7 +31,7 @@ Example:
31
31
 
32
32
  import logging
33
33
  from datetime import datetime, timezone
34
- from typing import Literal
34
+ from typing import Literal, cast
35
35
 
36
36
  from fin_infra.credit.experian.auth import ExperianAuthManager
37
37
  from fin_infra.credit.experian.client import ExperianClient
@@ -360,4 +360,4 @@ class ExperianProvider(CreditProvider):
360
360
  signature_key=signature_key,
361
361
  )
362
362
 
363
- return data.get("subscriptionId", "unknown")
363
+ return cast(str, data.get("subscriptionId", "unknown"))
@@ -29,7 +29,7 @@ add_goals(app)
29
29
 
30
30
  import logging
31
31
  from datetime import datetime
32
- from typing import List, Optional
32
+ from typing import Any, List, Optional, cast
33
33
 
34
34
  from fastapi import FastAPI, HTTPException, status, Query, Body
35
35
  from pydantic import BaseModel, Field
@@ -469,7 +469,7 @@ def add_goals(
469
469
  # Get all milestones from the goal (check_milestones only returns newly reached ones)
470
470
  goal = get_goal(goal_id)
471
471
  milestones = goal.get("milestones", [])
472
- return milestones
472
+ return cast(list[dict[Any, Any]], milestones)
473
473
  except KeyError:
474
474
  raise HTTPException(
475
475
  status_code=status.HTTP_404_NOT_FOUND, detail=f"Goal {goal_id} not found"
@@ -41,7 +41,7 @@ Example:
41
41
  """
42
42
 
43
43
  from datetime import datetime
44
- from typing import Any
44
+ from typing import Any, cast
45
45
 
46
46
  from pydantic import BaseModel, Field
47
47
 
@@ -839,7 +839,7 @@ def get_goal(goal_id: str) -> dict[str, Any]:
839
839
  if goal_id not in _GOALS_STORE:
840
840
  raise KeyError(f"Goal not found: {goal_id}")
841
841
 
842
- return _GOALS_STORE[goal_id]
842
+ return cast(dict[str, Any], _GOALS_STORE[goal_id])
843
843
 
844
844
 
845
845
  def update_goal(
@@ -885,7 +885,7 @@ def update_goal(
885
885
 
886
886
  Goal(**goal) # Will raise ValidationError if invalid
887
887
 
888
- return goal
888
+ return cast(dict[str, Any], goal)
889
889
 
890
890
 
891
891
  def delete_goal(goal_id: str) -> None:
@@ -26,7 +26,7 @@ Example:
26
26
  """
27
27
 
28
28
  from datetime import datetime
29
- from typing import Any
29
+ from typing import Any, cast
30
30
 
31
31
  from fin_infra.goals.management import get_goal, update_goal
32
32
  from fin_infra.goals.models import Milestone
@@ -229,7 +229,7 @@ def get_next_milestone(goal_id: str) -> dict[str, Any] | None:
229
229
  # Find first unreached milestone (sorted by amount)
230
230
  for milestone in milestones:
231
231
  if not milestone.get("reached", False):
232
- return milestone
232
+ return cast(dict[str, Any], milestone)
233
233
 
234
234
  return None
235
235
 
@@ -51,7 +51,8 @@ from typing import TYPE_CHECKING, Literal
51
51
  if TYPE_CHECKING:
52
52
  from fastapi import FastAPI
53
53
 
54
- from ..providers.base import InvestmentProvider
54
+ # Use the local InvestmentProvider base class (same as providers use)
55
+ from .providers.base import InvestmentProvider
55
56
 
56
57
  # Lazy imports to avoid loading provider SDKs unless needed
57
58
  _provider_cache: dict[str, InvestmentProvider] = {}
@@ -114,6 +115,7 @@ def easy_investments(
114
115
  return _provider_cache[cache_key]
115
116
 
116
117
  # Lazy import and initialize provider
118
+ instance: InvestmentProvider
117
119
  if provider == "plaid":
118
120
  from .providers.plaid import PlaidInvestmentProvider
119
121
 
@@ -172,14 +174,19 @@ def add_investments(
172
174
  >>> # GET /investments/transactions
173
175
  >>> # etc.
174
176
  """
175
- from .add import add_investments_impl
177
+ from .add import add_investments as add_investments_impl
178
+ from .providers.base import InvestmentProvider as InvestmentProviderBase
179
+
180
+ # Resolve provider from string Literal to actual InvestmentProvider instance
181
+ resolved_provider: InvestmentProviderBase | None = None
182
+ if provider is not None:
183
+ resolved_provider = easy_investments(provider=provider, **provider_config) # type: ignore[assignment]
176
184
 
177
185
  return add_investments_impl(
178
186
  app,
179
- provider=provider,
187
+ provider=resolved_provider,
180
188
  prefix=prefix,
181
189
  tags=tags or ["Investments"],
182
- **provider_config,
183
190
  )
184
191
 
185
192
 
@@ -112,19 +112,20 @@ def easy_investments(
112
112
  - Most other SnapTrade brokerages support trading operations
113
113
  """
114
114
  # Auto-detect provider from environment if not specified
115
- if provider is None:
116
- provider = _detect_provider()
115
+ detected_provider: str | None = provider
116
+ if detected_provider is None:
117
+ detected_provider = _detect_provider()
117
118
 
118
119
  # Validate provider
119
- if provider not in ("plaid", "snaptrade"):
120
+ if detected_provider not in ("plaid", "snaptrade"):
120
121
  raise ValueError(
121
- f"Invalid provider: {provider}. Must be 'plaid' or 'snaptrade'."
122
+ f"Invalid provider: {detected_provider}. Must be 'plaid' or 'snaptrade'."
122
123
  )
123
124
 
124
125
  # Instantiate provider
125
- if provider == "plaid":
126
+ if detected_provider == "plaid":
126
127
  return _create_plaid_provider(**config)
127
- elif provider == "snaptrade":
128
+ elif detected_provider == "snaptrade":
128
129
  return _create_snaptrade_provider(**config)
129
130
 
130
131
  # Should never reach here
@@ -233,10 +234,13 @@ def _create_snaptrade_provider(**config: Any) -> InvestmentProvider:
233
234
  "Example: easy_investments(provider='snaptrade', client_id='...', consumer_key='...')"
234
235
  )
235
236
 
237
+ # Ensure base_url is a string (default is set in SnapTradeInvestmentProvider)
238
+ resolved_base_url: str = base_url if isinstance(base_url, str) else "https://api.snaptrade.com/api/v1"
239
+
236
240
  return SnapTradeInvestmentProvider(
237
241
  client_id=client_id,
238
242
  consumer_key=consumer_key,
239
- base_url=base_url,
243
+ base_url=resolved_base_url,
240
244
  )
241
245
 
242
246
 
@@ -71,7 +71,7 @@ class TransactionType(str, Enum):
71
71
  fee = "fee"
72
72
  tax = "tax"
73
73
  transfer = "transfer"
74
- split = "split"
74
+ split = "split" # type: ignore[assignment] # str.split() name conflict
75
75
  merger = "merger"
76
76
  cancel = "cancel"
77
77
  other = "other"
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from datetime import date
12
12
  from decimal import Decimal
13
- from typing import Any, Dict, List, Optional
13
+ from typing import Any, Dict, List, Optional, cast
14
14
 
15
15
  from plaid.api import plaid_api
16
16
  from plaid.model.investments_holdings_get_request import InvestmentsHoldingsGetRequest
@@ -103,7 +103,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
103
103
  "development": plaid.Environment.Sandbox, # Map development to sandbox
104
104
  "production": plaid.Environment.Production,
105
105
  }
106
- return hosts.get(environment.lower(), plaid.Environment.Sandbox)
106
+ return cast(str, hosts.get(environment.lower(), plaid.Environment.Sandbox))
107
107
 
108
108
  async def get_holdings(
109
109
  self, access_token: str, account_ids: Optional[List[str]] = None
@@ -370,7 +370,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
370
370
  isin=plaid_security.get("isin"),
371
371
  sedol=plaid_security.get("sedol"),
372
372
  ticker_symbol=plaid_security.get("ticker_symbol"),
373
- name=plaid_security.get("name"),
373
+ name=plaid_security.get("name") or "Unknown Security",
374
374
  type=self._normalize_security_type(plaid_security.get("type", "other")),
375
375
  sector=plaid_security.get("sector"),
376
376
  close_price=close_price,
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  from datetime import date
13
13
  from decimal import Decimal
14
- from typing import Any, Dict, List, Optional
14
+ from typing import Any, Dict, List, Optional, cast
15
15
 
16
16
  import httpx
17
17
 
@@ -393,7 +393,7 @@ class SnapTradeInvestmentProvider(InvestmentProvider):
393
393
  url = f"{self.base_url}/connections"
394
394
  response = await self.client.get(url, headers=auth_headers)
395
395
  response.raise_for_status()
396
- return await response.json()
396
+ return cast(list[dict[str, Any]], await response.json())
397
397
 
398
398
  except httpx.HTTPStatusError as e:
399
399
  raise self._transform_error(e)
@@ -193,7 +193,11 @@ def add_market_data(
193
193
  if isinstance(provider, MarketDataProvider):
194
194
  market = provider
195
195
  else:
196
- market = easy_market(provider=provider, **config)
196
+ # Cast provider to Literal type for type checker
197
+ provider_literal: Literal["alphavantage", "yahoo"] | None = (
198
+ provider if provider in ("alphavantage", "yahoo", None) else None # type: ignore[assignment]
199
+ )
200
+ market = easy_market(provider=provider_literal, **config)
197
201
 
198
202
  # Create router (public - no auth required)
199
203
  router = public_router(prefix=prefix, tags=["Market Data"])
@@ -223,14 +227,15 @@ def add_market_data(
223
227
  try:
224
228
  candles = market.history(symbol, period=period, interval=interval)
225
229
  # Convert to dicts if they're Pydantic models
226
- candles_list = []
230
+ candles_list: list[dict] = []
227
231
  for candle in candles:
228
232
  if hasattr(candle, "model_dump"):
229
233
  candles_list.append(candle.model_dump())
230
234
  elif hasattr(candle, "dict"):
231
235
  candles_list.append(candle.dict())
232
236
  else:
233
- candles_list.append(candle)
237
+ # Cast to dict for type compatibility
238
+ candles_list.append(dict(candle) if hasattr(candle, "__iter__") else {"data": candle})
234
239
  return {"candles": candles_list}
235
240
  except Exception as e:
236
241
  raise HTTPException(status_code=400, detail=str(e))
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from datetime import date as DateType
5
- from typing import Optional
5
+ from typing import Optional, cast
6
6
 
7
7
  import httpx
8
8
 
@@ -66,10 +66,10 @@ class ExchangeRateClient:
66
66
  raise ExchangeRateAPIError(
67
67
  f"API returned error: {data.get('error-type', 'unknown')}"
68
68
  )
69
- return data["conversion_rates"]
69
+ return cast(dict[str, float], data["conversion_rates"])
70
70
  else:
71
71
  # Free tier response format
72
- return data["rates"]
72
+ return cast(dict[str, float], data["rates"])
73
73
 
74
74
  except httpx.HTTPError as e:
75
75
  raise ExchangeRateAPIError(f"HTTP error fetching rates: {e}")
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import date, datetime, timedelta
4
+ from typing import Any, cast
4
5
 
5
6
  # Plaid SDK v25+ uses new API structure
6
7
  try:
@@ -96,7 +97,7 @@ class PlaidClient(BankingProvider):
96
97
  language="en",
97
98
  )
98
99
  response = self.client.link_token_create(request)
99
- return response["link_token"]
100
+ return cast(str, response["link_token"])
100
101
 
101
102
  def exchange_public_token(self, public_token: str) -> dict:
102
103
  request = ItemPublicTokenExchangeRequest(public_token=public_token)
@@ -146,8 +147,8 @@ class PlaidClient(BankingProvider):
146
147
  # Return all balances
147
148
  return {"balances": [acc.get("balances", {}) for acc in accounts]}
148
149
 
149
- def identity(self, access_token: str) -> dict:
150
+ def identity(self, access_token: str) -> dict[Any, Any]:
150
151
  """Fetch identity/account holder information."""
151
152
  request = IdentityGetRequest(access_token=access_token)
152
153
  response = self.client.identity_get(request)
153
- return response.to_dict()
154
+ return cast(dict[Any, Any], response.to_dict())
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import ssl
26
26
  import httpx
27
- from typing import Any
27
+ from typing import Any, cast
28
28
 
29
29
  from ..base import BankingProvider
30
30
 
@@ -93,7 +93,13 @@ class TellerClient(BankingProvider):
93
93
  ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
94
94
  client_kwargs["verify"] = ssl_context
95
95
 
96
- self.client = httpx.Client(**client_kwargs)
96
+ # Create client with explicit parameters to satisfy type checker
97
+ self.client = httpx.Client(
98
+ base_url=str(client_kwargs["base_url"]),
99
+ timeout=float(client_kwargs["timeout"]), # type: ignore[arg-type]
100
+ headers=client_kwargs["headers"], # type: ignore[arg-type]
101
+ verify=client_kwargs.get("verify", True), # type: ignore[arg-type]
102
+ )
97
103
 
98
104
  def _request(self, method: str, path: str, **kwargs: Any) -> Any:
99
105
  """Make HTTP request to Teller API with error handling.
@@ -139,7 +145,7 @@ class TellerClient(BankingProvider):
139
145
  "products": ["accounts", "transactions", "balances", "identity"],
140
146
  },
141
147
  )
142
- return response.get("enrollment_id", "")
148
+ return cast(str, response.get("enrollment_id", ""))
143
149
 
144
150
  def exchange_public_token(self, public_token: str) -> dict:
145
151
  """Exchange public token for access token.
@@ -186,7 +192,7 @@ class TellerClient(BankingProvider):
186
192
  auth=(access_token, ""),
187
193
  )
188
194
  response.raise_for_status()
189
- return response.json()
195
+ return cast(list[dict[Any, Any]], response.json())
190
196
 
191
197
  def transactions(
192
198
  self,
@@ -229,7 +235,7 @@ class TellerClient(BankingProvider):
229
235
  params=params,
230
236
  )
231
237
  response.raise_for_status()
232
- return response.json()
238
+ return cast(list[dict[Any, Any]], response.json())
233
239
 
234
240
  def balances(self, access_token: str, account_id: str | None = None) -> dict:
235
241
  """Fetch current balances.
@@ -261,7 +267,7 @@ class TellerClient(BankingProvider):
261
267
  )
262
268
 
263
269
  response.raise_for_status()
264
- return response.json()
270
+ return cast(dict[Any, Any], response.json())
265
271
 
266
272
  def identity(self, access_token: str) -> dict:
267
273
  """Fetch identity/account holder information.
@@ -285,7 +291,7 @@ class TellerClient(BankingProvider):
285
291
  auth=(access_token, ""),
286
292
  )
287
293
  response.raise_for_status()
288
- return response.json()
294
+ return cast(dict[Any, Any], response.json())
289
295
 
290
296
  def __del__(self) -> None:
291
297
  """Close HTTP client on cleanup."""