fin-infra 0.1.63__tar.gz → 0.1.65__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.63 → fin_infra-0.1.65}/PKG-INFO +1 -1
  2. {fin_infra-0.1.63 → fin_infra-0.1.65}/pyproject.toml +1 -1
  3. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/spending.py +9 -7
  4. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/banking/__init__.py +5 -2
  5. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/brokerage/__init__.py +4 -2
  6. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/categorization/__init__.py +1 -1
  7. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/categorization/add.py +15 -16
  8. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/categorization/ease.py +1 -1
  9. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/categorization/engine.py +1 -1
  10. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/categorization/llm_layer.py +2 -2
  11. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/chat/__init__.py +1 -10
  12. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/chat/ease.py +1 -1
  13. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/chat/planning.py +57 -0
  14. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/crypto/insights.py +1 -1
  15. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/__init__.py +11 -4
  16. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/ease.py +11 -7
  17. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/models.py +1 -1
  18. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/providers/plaid.py +1 -1
  19. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/markets/__init__.py +8 -3
  20. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/ease.py +34 -13
  21. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/normalization/__init__.py +6 -6
  22. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/banking/teller_client.py +7 -1
  23. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/base.py +98 -1
  24. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/credit/experian.py +5 -0
  25. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/tax/mock.py +3 -3
  26. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/add.py +10 -1
  27. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/detectors_llm.py +1 -1
  28. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/insights.py +1 -1
  29. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/normalizers.py +1 -1
  30. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/security/token_store.py +3 -1
  31. {fin_infra-0.1.63 → fin_infra-0.1.65}/LICENSE +0 -0
  32. {fin_infra-0.1.63 → fin_infra-0.1.65}/README.md +0 -0
  33. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/__init__.py +0 -0
  34. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/__main__.py +0 -0
  35. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/__init__.py +0 -0
  36. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/add.py +0 -0
  37. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/cash_flow.py +0 -0
  38. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/ease.py +0 -0
  39. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/models.py +0 -0
  40. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/portfolio.py +0 -0
  41. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/projections.py +0 -0
  42. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/rebalancing.py +0 -0
  43. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/savings.py +0 -0
  44. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/analytics/scenarios.py +0 -0
  45. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/banking/history.py +0 -0
  46. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/banking/utils.py +0 -0
  47. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/__init__.py +0 -0
  48. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/add.py +0 -0
  49. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/alerts.py +0 -0
  50. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/ease.py +0 -0
  51. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/models.py +0 -0
  52. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/scaffold_templates/README.md +0 -0
  53. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/scaffold_templates/__init__.py +0 -0
  54. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/scaffold_templates/models.py.tmpl +0 -0
  55. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/scaffold_templates/repository.py.tmpl +0 -0
  56. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/scaffold_templates/schemas.py.tmpl +0 -0
  57. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/templates.py +0 -0
  58. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/budgets/tracker.py +0 -0
  59. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/cashflows/__init__.py +0 -0
  60. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/cashflows/core.py +0 -0
  61. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/categorization/models.py +0 -0
  62. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/categorization/rules.py +0 -0
  63. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/categorization/taxonomy.py +0 -0
  64. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/cli/__init__.py +0 -0
  65. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/cli/cmds/__init__.py +0 -0
  66. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/cli/cmds/scaffold_cmds.py +0 -0
  67. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/clients/__init__.py +0 -0
  68. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/clients/base.py +0 -0
  69. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/clients/plaid.py +0 -0
  70. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/compliance/__init__.py +0 -0
  71. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/credit/__init__.py +0 -0
  72. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/credit/add.py +0 -0
  73. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/credit/experian/__init__.py +0 -0
  74. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/credit/experian/auth.py +0 -0
  75. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/credit/experian/client.py +0 -0
  76. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/credit/experian/parser.py +0 -0
  77. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/credit/experian/provider.py +0 -0
  78. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/credit/mock.py +0 -0
  79. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/crypto/__init__.py +0 -0
  80. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/documents/__init__.py +0 -0
  81. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/documents/add.py +0 -0
  82. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/documents/analysis.py +0 -0
  83. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/documents/ease.py +0 -0
  84. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/documents/models.py +0 -0
  85. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/documents/ocr.py +0 -0
  86. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/documents/storage.py +0 -0
  87. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/exceptions.py +0 -0
  88. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/__init__.py +0 -0
  89. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/add.py +0 -0
  90. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/funding.py +0 -0
  91. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/management.py +0 -0
  92. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/milestones.py +0 -0
  93. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/models.py +0 -0
  94. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/scaffold_templates/README.md +0 -0
  95. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/scaffold_templates/__init__.py +0 -0
  96. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/scaffold_templates/models.py.tmpl +0 -0
  97. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/scaffold_templates/repository.py.tmpl +0 -0
  98. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/goals/scaffold_templates/schemas.py.tmpl +0 -0
  99. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/insights/__init__.py +0 -0
  100. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/insights/aggregator.py +0 -0
  101. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/insights/models.py +0 -0
  102. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/add.py +0 -0
  103. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/providers/__init__.py +0 -0
  104. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/providers/base.py +0 -0
  105. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/providers/snaptrade.py +0 -0
  106. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/scaffold_templates/README.md +0 -0
  107. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/scaffold_templates/__init__.py +0 -0
  108. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/scaffold_templates/models.py.tmpl +0 -0
  109. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/scaffold_templates/repository.py.tmpl +0 -0
  110. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/investments/scaffold_templates/schemas.py.tmpl +0 -0
  111. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/__init__.py +0 -0
  112. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/accounts.py +0 -0
  113. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/brokerage.py +0 -0
  114. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/candle.py +0 -0
  115. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/credit.py +0 -0
  116. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/money.py +0 -0
  117. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/quotes.py +0 -0
  118. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/tax.py +0 -0
  119. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/models/transactions.py +0 -0
  120. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/__init__.py +0 -0
  121. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/add.py +0 -0
  122. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/aggregator.py +0 -0
  123. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/calculator.py +0 -0
  124. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/goals.py +0 -0
  125. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/insights.py +0 -0
  126. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/models.py +0 -0
  127. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/scaffold_templates/README.md +0 -0
  128. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/scaffold_templates/__init__.py +0 -0
  129. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/scaffold_templates/models.py.tmpl +0 -0
  130. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/scaffold_templates/repository.py.tmpl +0 -0
  131. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/net_worth/scaffold_templates/schemas.py.tmpl +0 -0
  132. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/normalization/currency_converter.py +0 -0
  133. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/normalization/models.py +0 -0
  134. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/normalization/providers/__init__.py +0 -0
  135. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/normalization/providers/exchangerate.py +0 -0
  136. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/normalization/providers/static_mappings.py +0 -0
  137. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/normalization/symbol_resolver.py +0 -0
  138. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/obs/__init__.py +0 -0
  139. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/obs/classifier.py +0 -0
  140. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/__init__.py +0 -0
  141. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/banking/base.py +0 -0
  142. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/banking/plaid_client.py +0 -0
  143. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/brokerage/alpaca.py +0 -0
  144. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/brokerage/base.py +0 -0
  145. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/identity/stripe_identity.py +0 -0
  146. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/market/alphavantage.py +0 -0
  147. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/market/base.py +0 -0
  148. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/market/ccxt_crypto.py +0 -0
  149. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/market/coingecko.py +0 -0
  150. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/market/yahoo.py +0 -0
  151. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/registry.py +0 -0
  152. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/tax/__init__.py +0 -0
  153. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/tax/irs.py +0 -0
  154. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/providers/tax/taxbit.py +0 -0
  155. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/py.typed +0 -0
  156. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/__init__.py +0 -0
  157. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/detector.py +0 -0
  158. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/ease.py +0 -0
  159. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/models.py +0 -0
  160. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/normalizer.py +0 -0
  161. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/recurring/summary.py +0 -0
  162. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/scaffold/__init__.py +0 -0
  163. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/scaffold/budgets.py +0 -0
  164. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/scaffold/goals.py +0 -0
  165. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/security/__init__.py +0 -0
  166. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/security/add.py +0 -0
  167. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/security/audit.py +0 -0
  168. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/security/encryption.py +0 -0
  169. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/security/models.py +0 -0
  170. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/security/pii_filter.py +0 -0
  171. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/security/pii_patterns.py +0 -0
  172. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/settings.py +0 -0
  173. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/tax/__init__.py +0 -0
  174. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/tax/add.py +0 -0
  175. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/tax/tlh.py +0 -0
  176. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/utils/__init__.py +0 -0
  177. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/utils/http.py +0 -0
  178. {fin_infra-0.1.63 → fin_infra-0.1.65}/src/fin_infra/utils/retry.py +0 -0
  179. {fin_infra-0.1.63 → fin_infra-0.1.65}/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.63
3
+ Version: 0.1.65
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.63"
3
+ version = "0.1.65"
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
  )
@@ -468,7 +470,7 @@ async def generate_spending_insights(
468
470
 
469
471
  # Try to import ai-infra LLM (optional dependency)
470
472
  try:
471
- from ai_infra.llm import LLM
473
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
472
474
  except ImportError:
473
475
  # Graceful degradation: return rule-based insights
474
476
  return _generate_rule_based_insights(spending_insight, user_context)
@@ -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,
@@ -135,21 +136,19 @@ def add_categorization(
135
136
  categories = get_all_categories()
136
137
 
137
138
  # Return category metadata
138
- return [
139
- {
140
- "name": cat.value,
141
- "group": get_category_metadata(cat).group.value
142
- if get_category_metadata(cat)
143
- else None,
144
- "display_name": get_category_metadata(cat).display_name
145
- if get_category_metadata(cat)
146
- else cat.value,
147
- "description": get_category_metadata(cat).description
148
- if get_category_metadata(cat)
149
- else None,
150
- }
151
- for cat in categories
152
- ]
139
+ result = []
140
+ for cat in categories:
141
+ meta = get_category_metadata(cat)
142
+ result.append(
143
+ {
144
+ "name": cat.value,
145
+ "group": meta.group.value if meta else None,
146
+ "display_name": meta.display_name if meta else cat.value,
147
+ "description": meta.description if meta else None,
148
+ }
149
+ )
150
+
151
+ return result
153
152
 
154
153
  @router.get("/stats", response_model=CategoryStats)
155
154
  async def get_stats():
@@ -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
 
@@ -20,8 +20,8 @@ from pydantic import BaseModel, Field
20
20
 
21
21
  # ai-infra imports
22
22
  try:
23
- from ai_infra.llm import LLM
24
- from ai_infra.llm.providers import Providers
23
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
24
+ from ai_infra.llm.providers import Providers # type: ignore[attr-defined]
25
25
  except ImportError:
26
26
  raise ImportError("ai-infra not installed. Install with: pip install ai-infra")
27
27
 
@@ -149,15 +149,6 @@ def add_financial_conversation(
149
149
  # TODO: Get user_id from svc-infra auth context
150
150
  user_id = "demo_user" # Placeholder
151
151
 
152
- # Check for sensitive content
153
- if is_sensitive_question(request.question):
154
- return ConversationResponse(
155
- answer="I cannot process requests containing sensitive information like SSNs, passwords, or account numbers. Please rephrase your question without this information.",
156
- follow_up_questions=[],
157
- conversation_id=f"{user_id}_denied",
158
- disclaimer="This is an automated safety response.",
159
- )
160
-
161
152
  # Ask conversation
162
153
  response = await conversation.ask(
163
154
  user_id=user_id,
@@ -173,7 +164,7 @@ def add_financial_conversation(
173
164
  # TODO: Get user_id from svc-infra auth context
174
165
  user_id = "demo_user"
175
166
  context = await conversation._get_context(user_id)
176
- return context.exchanges if context else []
167
+ return context.previous_exchanges if context else []
177
168
 
178
169
  @router.delete("/history")
179
170
  async def clear_history():
@@ -69,7 +69,7 @@ def easy_financial_conversation(
69
69
  # Auto-create LLM if not provided
70
70
  if llm is None:
71
71
  try:
72
- from ai_infra.llm import LLM
72
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
73
73
 
74
74
  llm = LLM()
75
75
  except ImportError:
@@ -338,8 +338,65 @@ class FinancialPlanningConversation:
338
338
  # Save updated context (24h TTL)
339
339
  await self._save_context(context)
340
340
 
341
+ # Track latest session id for convenience endpoints (history/clear).
342
+ # Best-effort: failures here must not break the chat response.
343
+ try:
344
+ await self.cache.set(
345
+ self._latest_session_key(user_id),
346
+ context.session_id,
347
+ ttl=86400,
348
+ )
349
+ except Exception:
350
+ pass
351
+
341
352
  return response
342
353
 
354
+ # ---------------------------------------------------------------------
355
+ # Backward-compatible context helpers
356
+ # ---------------------------------------------------------------------
357
+
358
+ def _latest_session_key(self, user_id: str) -> str:
359
+ return f"fin_infra:conversation_latest_session:{user_id}"
360
+
361
+ async def _get_latest_session_id(self, user_id: str) -> str | None:
362
+ try:
363
+ value = await self.cache.get(self._latest_session_key(user_id))
364
+ except Exception:
365
+ return None
366
+
367
+ if value is None:
368
+ return None
369
+ if isinstance(value, bytes):
370
+ try:
371
+ return value.decode("utf-8")
372
+ except Exception:
373
+ return None
374
+ if isinstance(value, str):
375
+ return value
376
+ return str(value)
377
+
378
+ async def _get_context(
379
+ self, user_id: str, session_id: str | None = None
380
+ ) -> ConversationContext | None:
381
+ if session_id is None:
382
+ session_id = await self._get_latest_session_id(user_id)
383
+ if session_id is None:
384
+ return None
385
+
386
+ return await self._load_context(user_id=user_id, session_id=session_id)
387
+
388
+ async def _clear_context(self, user_id: str, session_id: str | None = None) -> None:
389
+ if session_id is None:
390
+ session_id = await self._get_latest_session_id(user_id)
391
+
392
+ if session_id is not None:
393
+ await self.clear_session(user_id=user_id, session_id=session_id)
394
+
395
+ try:
396
+ await self.cache.delete(self._latest_session_key(user_id))
397
+ except Exception:
398
+ pass
399
+
343
400
  async def _load_context(
344
401
  self,
345
402
  user_id: str,
@@ -15,7 +15,7 @@ from typing import TYPE_CHECKING
15
15
  from pydantic import BaseModel, Field
16
16
 
17
17
  if TYPE_CHECKING:
18
- from ai_infra.llm import LLM
18
+ from ai_infra.llm import LLM # type: ignore[attr-defined]
19
19
 
20
20
 
21
21
  class CryptoInsight(BaseModel):
@@ -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"
@@ -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,
@@ -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))
@@ -121,6 +121,15 @@ class NetWorthTracker:
121
121
  self.goal_tracker = goal_tracker
122
122
  self.conversation = conversation
123
123
 
124
+ # Configuration set by easy_net_worth(); declared here for type checkers.
125
+ self.snapshot_schedule: str = "daily"
126
+ self.change_threshold_percent: float = 5.0
127
+ self.change_threshold_amount: float = 10000.0
128
+ self.enable_llm: bool = False
129
+ self.llm_provider: str | None = None
130
+ self.llm_model: str | None = None
131
+ self.config: dict[str, Any] = {}
132
+
124
133
  async def calculate_net_worth(
125
134
  self,
126
135
  user_id: str,
@@ -368,12 +377,20 @@ def easy_net_worth(
368
377
 
369
378
  if enable_llm:
370
379
  try:
371
- from ai_infra.llm import LLM
380
+ from ai_infra.llm.llm import LLM # type: ignore[attr-defined]
372
381
  except ImportError:
373
382
  raise ImportError(
374
383
  "LLM features require ai-infra package. " "Install with: pip install ai-infra"
375
384
  )
376
385
 
386
+ cache = None
387
+ try:
388
+ from svc_infra.cache import get_cache
389
+
390
+ cache = get_cache()
391
+ except Exception:
392
+ cache = None
393
+
377
394
  # Determine default model
378
395
  default_models = {
379
396
  "google": "gemini-2.0-flash-exp",
@@ -416,18 +433,22 @@ def easy_net_worth(
416
433
  # goals.management not yet implemented, skip
417
434
  pass
418
435
 
419
- try:
420
- from fin_infra.conversation import FinancialPlanningConversation
421
-
422
- conversation = FinancialPlanningConversation(
423
- llm=llm,
424
- cache=cache, # Required for context storage
425
- provider=llm_provider,
426
- model_name=model_name,
427
- )
428
- except ImportError:
429
- # conversation module not yet implemented, skip
430
- pass
436
+ if cache is not None:
437
+ try:
438
+ from fin_infra.conversation import FinancialPlanningConversation
439
+
440
+ conversation = FinancialPlanningConversation(
441
+ llm=llm,
442
+ cache=cache, # Required for context storage
443
+ provider=llm_provider,
444
+ model_name=model_name,
445
+ )
446
+ except ImportError:
447
+ # conversation module not yet implemented, skip
448
+ pass
449
+ except Exception:
450
+ # Cache not configured or other runtime issue; skip optional conversation wiring.
451
+ pass
431
452
 
432
453
  # Create tracker
433
454
  tracker = NetWorthTracker(
@@ -150,14 +150,14 @@ def add_normalization(
150
150
  ):
151
151
  """Convert amount between currencies."""
152
152
  try:
153
- result = await converter.convert(amount, from_currency, to_currency)
153
+ result = await converter.convert_with_details(amount, from_currency, to_currency)
154
154
  return {
155
- "amount": amount,
156
- "from_currency": from_currency,
157
- "to_currency": to_currency,
158
- "result": result.amount,
155
+ "amount": result.amount,
156
+ "from_currency": result.from_currency,
157
+ "to_currency": result.to_currency,
158
+ "result": result.converted,
159
159
  "rate": result.rate,
160
- "timestamp": result.timestamp.isoformat(),
160
+ "timestamp": result.date.isoformat() if result.date else None,
161
161
  }
162
162
  except CurrencyNotSupportedError as e:
163
163
  raise HTTPException(status_code=400, detail=str(e))
@@ -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.