fin-infra 0.1.79__tar.gz → 0.1.81__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.
- {fin_infra-0.1.79 → fin_infra-0.1.81}/PKG-INFO +1 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/pyproject.toml +5 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/__init__.py +10 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/banking/__init__.py +0 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/banking/history.py +5 -5
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/banking/utils.py +9 -9
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/alerts.py +3 -3
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/tracker.py +2 -2
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/cashflows/core.py +1 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/categorization/llm_layer.py +2 -2
- fin_infra-0.1.81/src/fin_infra/clients/__init__.py +25 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/clients/base.py +1 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/clients/plaid.py +1 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/compliance/__init__.py +2 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/documents/analysis.py +2 -2
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/documents/models.py +4 -4
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/documents/ocr.py +2 -2
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/documents/storage.py +2 -2
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/add.py +9 -9
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/funding.py +3 -5
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/models.py +5 -5
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/providers/base.py +9 -9
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/providers/plaid.py +11 -11
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/providers/snaptrade.py +12 -12
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/markets/__init__.py +4 -2
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/__init__.py +7 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/obs/classifier.py +1 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/base.py +27 -2
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/market/alphavantage.py +1 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/market/yahoo.py +1 -1
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/summary.py +11 -11
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/scaffold/budgets.py +4 -4
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/scaffold/goals.py +4 -4
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/security/encryption.py +4 -4
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/utils/retry.py +2 -1
- fin_infra-0.1.79/src/fin_infra/clients/__init__.py +0 -3
- {fin_infra-0.1.79 → fin_infra-0.1.81}/LICENSE +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/README.md +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/__main__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/cash_flow.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/ease.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/portfolio.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/projections.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/rebalancing.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/savings.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/scenarios.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/analytics/spending.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/brokerage/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/ease.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/scaffold_templates/README.md +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/scaffold_templates/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/scaffold_templates/models.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/scaffold_templates/repository.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/scaffold_templates/schemas.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/budgets/templates.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/cashflows/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/categorization/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/categorization/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/categorization/ease.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/categorization/engine.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/categorization/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/categorization/rules.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/categorization/taxonomy.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/chat/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/chat/ease.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/chat/planning.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/cli/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/cli/cmds/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/cli/cmds/scaffold_cmds.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/credit/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/credit/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/credit/experian/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/credit/experian/auth.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/credit/experian/client.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/credit/experian/parser.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/credit/experian/provider.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/credit/mock.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/crypto/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/crypto/insights.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/documents/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/documents/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/documents/ease.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/exceptions.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/management.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/milestones.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/scaffold_templates/README.md +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/scaffold_templates/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/scaffold_templates/models.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/scaffold_templates/repository.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/goals/scaffold_templates/schemas.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/insights/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/insights/aggregator.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/insights/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/ease.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/providers/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/scaffold_templates/README.md +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/scaffold_templates/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/scaffold_templates/models.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/scaffold_templates/repository.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/investments/scaffold_templates/schemas.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/accounts.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/brokerage.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/candle.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/credit.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/money.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/quotes.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/tax.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/models/transactions.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/aggregator.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/calculator.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/ease.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/goals.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/insights.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/scaffold_templates/README.md +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/scaffold_templates/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/scaffold_templates/models.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/scaffold_templates/repository.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/net_worth/scaffold_templates/schemas.py.tmpl +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/normalization/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/normalization/currency_converter.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/normalization/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/normalization/providers/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/normalization/providers/exchangerate.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/normalization/providers/static_mappings.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/normalization/symbol_resolver.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/obs/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/banking/base.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/banking/plaid_client.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/banking/teller_client.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/brokerage/alpaca.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/brokerage/base.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/credit/experian.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/identity/stripe_identity.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/market/base.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/market/ccxt_crypto.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/market/coingecko.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/registry.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/tax/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/tax/irs.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/tax/mock.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/providers/tax/taxbit.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/py.typed +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/detector.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/detectors_llm.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/ease.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/insights.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/normalizer.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/recurring/normalizers.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/scaffold/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/security/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/security/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/security/audit.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/security/models.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/security/pii_filter.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/security/pii_patterns.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/security/token_store.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/settings.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/tax/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/tax/add.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/tax/tlh.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/utils/__init__.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/src/fin_infra/utils/http.py +0 -0
- {fin_infra-0.1.79 → fin_infra-0.1.81}/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.
|
|
3
|
+
Version: 0.1.81
|
|
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.
|
|
3
|
+
version = "0.1.81"
|
|
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"
|
|
@@ -81,6 +81,10 @@ pre-commit = ">=3.0.0"
|
|
|
81
81
|
types-requests = ">=2.31.0"
|
|
82
82
|
pytest-cov = ">=4.0.0"
|
|
83
83
|
aiosqlite = ">=0.20.0"
|
|
84
|
+
# Documentation
|
|
85
|
+
mkdocs = ">=1.6.0"
|
|
86
|
+
mkdocs-material = ">=9.5.0"
|
|
87
|
+
mkdocstrings = {extras = ["python"], version = ">=0.25.0"}
|
|
84
88
|
|
|
85
89
|
[tool.pytest.ini_options]
|
|
86
90
|
testpaths = ["tests"]
|
|
@@ -7,6 +7,16 @@ This module provides comprehensive financial analytics capabilities including:
|
|
|
7
7
|
- Portfolio analytics (returns, allocation, benchmarking)
|
|
8
8
|
- Growth projections (net worth forecasting with scenarios)
|
|
9
9
|
|
|
10
|
+
Feature Status:
|
|
11
|
+
✅ STABLE: Core calculation functions (all analytics work with provided data)
|
|
12
|
+
⚠️ INTEGRATION: Auto-fetching from providers requires setup:
|
|
13
|
+
- Banking provider for transaction data
|
|
14
|
+
- Brokerage provider for investment data
|
|
15
|
+
- Categorization for expense categorization
|
|
16
|
+
|
|
17
|
+
When providers aren't configured, functions accept data directly or return
|
|
18
|
+
sensible placeholder values for testing/development.
|
|
19
|
+
|
|
10
20
|
Serves multiple use cases:
|
|
11
21
|
- Personal finance apps (cash flow, savings tracking)
|
|
12
22
|
- Wealth management platforms (portfolio analytics, projections)
|
|
@@ -174,7 +174,6 @@ def easy_banking(provider: str = "teller", **config) -> BankingProvider:
|
|
|
174
174
|
See Also:
|
|
175
175
|
- add_banking(): For FastAPI integration with routes
|
|
176
176
|
- docs/banking.md: Comprehensive banking integration guide
|
|
177
|
-
- docs/adr/0003-banking-integration.md: Architecture decisions
|
|
178
177
|
"""
|
|
179
178
|
# Auto-detect provider config from environment if not explicitly provided
|
|
180
179
|
# Only auto-detect if no config params were passed
|
|
@@ -42,7 +42,7 @@ from __future__ import annotations
|
|
|
42
42
|
import logging
|
|
43
43
|
import os
|
|
44
44
|
from datetime import date, datetime, timedelta
|
|
45
|
-
from typing import
|
|
45
|
+
from typing import Optional
|
|
46
46
|
from pydantic import BaseModel, Field, ConfigDict
|
|
47
47
|
|
|
48
48
|
|
|
@@ -59,7 +59,7 @@ _logger = logging.getLogger(__name__)
|
|
|
59
59
|
|
|
60
60
|
# In-memory storage for testing (will be replaced with SQL database in production)
|
|
61
61
|
# ⚠️ WARNING: All data is LOST on restart when using in-memory storage!
|
|
62
|
-
_balance_snapshots:
|
|
62
|
+
_balance_snapshots: list[BalanceSnapshot] = []
|
|
63
63
|
_production_warning_logged = False
|
|
64
64
|
|
|
65
65
|
|
|
@@ -157,7 +157,7 @@ def get_balance_history(
|
|
|
157
157
|
days: int = 90,
|
|
158
158
|
start_date: Optional[date] = None,
|
|
159
159
|
end_date: Optional[date] = None,
|
|
160
|
-
) ->
|
|
160
|
+
) -> list[BalanceSnapshot]:
|
|
161
161
|
"""Get balance history for an account.
|
|
162
162
|
|
|
163
163
|
Retrieves balance snapshots for the specified account within a date range.
|
|
@@ -216,8 +216,8 @@ def get_balance_history(
|
|
|
216
216
|
|
|
217
217
|
def get_balance_snapshots(
|
|
218
218
|
account_id: str,
|
|
219
|
-
dates:
|
|
220
|
-
) ->
|
|
219
|
+
dates: list[date],
|
|
220
|
+
) -> list[BalanceSnapshot]:
|
|
221
221
|
"""Get balance snapshots for specific dates.
|
|
222
222
|
|
|
223
223
|
Args:
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import re
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, Optional, Literal
|
|
13
13
|
from pydantic import BaseModel, ConfigDict, Field
|
|
14
14
|
|
|
15
15
|
from ..providers.base import BankingProvider
|
|
@@ -179,7 +179,7 @@ def validate_provider_token(provider: str, access_token: str) -> bool:
|
|
|
179
179
|
return validator(access_token)
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def parse_banking_providers(banking_providers:
|
|
182
|
+
def parse_banking_providers(banking_providers: dict[str, Any]) -> BankingConnectionStatus:
|
|
183
183
|
"""
|
|
184
184
|
Parse banking_providers JSON field into structured status.
|
|
185
185
|
|
|
@@ -257,7 +257,7 @@ def parse_banking_providers(banking_providers: Dict[str, Any]) -> BankingConnect
|
|
|
257
257
|
return status
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def sanitize_connection_status(status: BankingConnectionStatus) ->
|
|
260
|
+
def sanitize_connection_status(status: BankingConnectionStatus) -> dict[str, Any]:
|
|
261
261
|
"""
|
|
262
262
|
Sanitize connection status for API responses (removes access tokens).
|
|
263
263
|
|
|
@@ -298,10 +298,10 @@ def sanitize_connection_status(status: BankingConnectionStatus) -> Dict[str, Any
|
|
|
298
298
|
|
|
299
299
|
|
|
300
300
|
def mark_connection_unhealthy(
|
|
301
|
-
banking_providers:
|
|
301
|
+
banking_providers: dict[str, Any],
|
|
302
302
|
provider: str,
|
|
303
303
|
error_message: str,
|
|
304
|
-
) ->
|
|
304
|
+
) -> dict[str, Any]:
|
|
305
305
|
"""
|
|
306
306
|
Mark a provider connection as unhealthy (for error handling).
|
|
307
307
|
|
|
@@ -335,9 +335,9 @@ def mark_connection_unhealthy(
|
|
|
335
335
|
|
|
336
336
|
|
|
337
337
|
def mark_connection_healthy(
|
|
338
|
-
banking_providers:
|
|
338
|
+
banking_providers: dict[str, Any],
|
|
339
339
|
provider: str,
|
|
340
|
-
) ->
|
|
340
|
+
) -> dict[str, Any]:
|
|
341
341
|
"""
|
|
342
342
|
Mark a provider connection as healthy (after successful sync).
|
|
343
343
|
|
|
@@ -368,7 +368,7 @@ def mark_connection_healthy(
|
|
|
368
368
|
|
|
369
369
|
|
|
370
370
|
def get_primary_access_token(
|
|
371
|
-
banking_providers:
|
|
371
|
+
banking_providers: dict[str, Any],
|
|
372
372
|
) -> tuple[Optional[str], Optional[str]]:
|
|
373
373
|
"""
|
|
374
374
|
Get the primary access token and provider name.
|
|
@@ -437,7 +437,7 @@ async def test_connection_health(
|
|
|
437
437
|
return False, error_msg
|
|
438
438
|
|
|
439
439
|
|
|
440
|
-
def should_refresh_token(banking_providers:
|
|
440
|
+
def should_refresh_token(banking_providers: dict[str, Any], provider: str) -> bool:
|
|
441
441
|
"""
|
|
442
442
|
Check if a provider token should be refreshed.
|
|
443
443
|
|
|
@@ -35,7 +35,7 @@ Example:
|
|
|
35
35
|
from __future__ import annotations
|
|
36
36
|
|
|
37
37
|
from datetime import datetime
|
|
38
|
-
from typing import TYPE_CHECKING,
|
|
38
|
+
from typing import TYPE_CHECKING, Optional
|
|
39
39
|
|
|
40
40
|
from fin_infra.budgets.models import (
|
|
41
41
|
AlertSeverity,
|
|
@@ -52,7 +52,7 @@ async def check_budget_alerts(
|
|
|
52
52
|
budget_id: str,
|
|
53
53
|
tracker: BudgetTracker,
|
|
54
54
|
thresholds: Optional[dict[str, float]] = None,
|
|
55
|
-
) ->
|
|
55
|
+
) -> list[BudgetAlert]:
|
|
56
56
|
"""
|
|
57
57
|
Check budget for alerts (overspending, approaching limits, unusual patterns).
|
|
58
58
|
|
|
@@ -111,7 +111,7 @@ async def check_budget_alerts(
|
|
|
111
111
|
# Get budget progress
|
|
112
112
|
progress = await tracker.get_budget_progress(budget_id)
|
|
113
113
|
|
|
114
|
-
alerts:
|
|
114
|
+
alerts: list[BudgetAlert] = []
|
|
115
115
|
|
|
116
116
|
# Check each category for alerts
|
|
117
117
|
for category in progress.categories:
|
|
@@ -36,7 +36,7 @@ from __future__ import annotations
|
|
|
36
36
|
|
|
37
37
|
import uuid
|
|
38
38
|
from datetime import datetime, timedelta
|
|
39
|
-
from typing import TYPE_CHECKING,
|
|
39
|
+
from typing import TYPE_CHECKING, Optional
|
|
40
40
|
|
|
41
41
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
42
42
|
|
|
@@ -206,7 +206,7 @@ class BudgetTracker:
|
|
|
206
206
|
self,
|
|
207
207
|
user_id: str,
|
|
208
208
|
type: Optional[str] = None,
|
|
209
|
-
) ->
|
|
209
|
+
) -> list[Budget]:
|
|
210
210
|
"""
|
|
211
211
|
Get all budgets for a user.
|
|
212
212
|
|
|
@@ -15,7 +15,7 @@ Expected performance:
|
|
|
15
15
|
|
|
16
16
|
import hashlib
|
|
17
17
|
import logging
|
|
18
|
-
from typing import Any,
|
|
18
|
+
from typing import Any, Optional, cast
|
|
19
19
|
from pydantic import BaseModel, Field
|
|
20
20
|
|
|
21
21
|
# ai-infra imports
|
|
@@ -40,7 +40,7 @@ class CategoryPrediction(BaseModel):
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
# Few-shot examples (20 diverse merchants covering all major categories)
|
|
43
|
-
FEW_SHOT_EXAMPLES:
|
|
43
|
+
FEW_SHOT_EXAMPLES: list[tuple[str, str, str]] = [
|
|
44
44
|
# Food & Dining (5 examples)
|
|
45
45
|
("STARBUCKS #1234", "Coffee Shops", "Popular coffee shop chain"),
|
|
46
46
|
("MCDONALD'S", "Fast Food", "Fast food restaurant"),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""DEPRECATED: Use fin_infra.providers instead.
|
|
2
|
+
|
|
3
|
+
This module is deprecated and will be removed in a future version.
|
|
4
|
+
All ABCs have been consolidated into fin_infra.providers.base.
|
|
5
|
+
|
|
6
|
+
Migration:
|
|
7
|
+
# Old (deprecated)
|
|
8
|
+
from fin_infra.clients import BankingClient, MarketDataClient
|
|
9
|
+
|
|
10
|
+
# New
|
|
11
|
+
from fin_infra.providers.base import BankingProvider, MarketDataProvider
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
from .base import BankingClient, CreditClient, MarketDataClient
|
|
17
|
+
|
|
18
|
+
warnings.warn(
|
|
19
|
+
"fin_infra.clients is deprecated. Use fin_infra.providers instead. "
|
|
20
|
+
"This module will be removed in a future version.",
|
|
21
|
+
DeprecationWarning,
|
|
22
|
+
stacklevel=2,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = ["BankingClient", "MarketDataClient", "CreditClient"]
|
|
@@ -21,7 +21,8 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
23
|
from datetime import datetime
|
|
24
|
-
from typing import Any,
|
|
24
|
+
from typing import Any, TYPE_CHECKING, cast
|
|
25
|
+
from collections.abc import Callable
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
27
28
|
from fastapi import FastAPI, Request, Response
|
|
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import re
|
|
26
26
|
from datetime import datetime
|
|
27
|
-
from typing import TYPE_CHECKING
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from svc_infra.storage.base import StorageBackend
|
|
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
|
|
|
32
32
|
from .models import DocumentAnalysis
|
|
33
33
|
|
|
34
34
|
# In-memory analysis cache (production: use svc-infra cache)
|
|
35
|
-
_analysis_cache:
|
|
35
|
+
_analysis_cache: dict[str, "DocumentAnalysis"] = {}
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
async def analyze_document(
|
|
@@ -31,7 +31,7 @@ from __future__ import annotations
|
|
|
31
31
|
|
|
32
32
|
from datetime import datetime
|
|
33
33
|
from enum import Enum
|
|
34
|
-
from typing import
|
|
34
|
+
from typing import Optional
|
|
35
35
|
|
|
36
36
|
from pydantic import BaseModel, ConfigDict, Field
|
|
37
37
|
from svc_infra.documents import Document as BaseDocument
|
|
@@ -145,7 +145,7 @@ class OCRResult(BaseModel):
|
|
|
145
145
|
confidence: float = Field(
|
|
146
146
|
..., description="Overall OCR confidence score (0.0-1.0)", ge=0.0, le=1.0
|
|
147
147
|
)
|
|
148
|
-
fields_extracted:
|
|
148
|
+
fields_extracted: dict[str, str] = Field(
|
|
149
149
|
default_factory=dict,
|
|
150
150
|
description="Structured fields extracted from document (names, amounts, dates)",
|
|
151
151
|
)
|
|
@@ -181,10 +181,10 @@ class DocumentAnalysis(BaseModel):
|
|
|
181
181
|
|
|
182
182
|
document_id: str = Field(..., description="Document that was analyzed")
|
|
183
183
|
summary: str = Field(..., description="High-level document summary")
|
|
184
|
-
key_findings:
|
|
184
|
+
key_findings: list[str] = Field(
|
|
185
185
|
default_factory=list, description="Important facts extracted from document"
|
|
186
186
|
)
|
|
187
|
-
recommendations:
|
|
187
|
+
recommendations: list[str] = Field(
|
|
188
188
|
default_factory=list, description="Action items or suggestions based on document content"
|
|
189
189
|
)
|
|
190
190
|
analysis_date: datetime = Field(
|
|
@@ -25,7 +25,7 @@ from __future__ import annotations
|
|
|
25
25
|
|
|
26
26
|
import re
|
|
27
27
|
from datetime import datetime
|
|
28
|
-
from typing import TYPE_CHECKING,
|
|
28
|
+
from typing import TYPE_CHECKING, Optional
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
31
|
from svc_infra.storage.base import StorageBackend
|
|
@@ -33,7 +33,7 @@ if TYPE_CHECKING:
|
|
|
33
33
|
from .models import OCRResult
|
|
34
34
|
|
|
35
35
|
# In-memory OCR cache (production: use svc-infra cache)
|
|
36
|
-
_ocr_cache:
|
|
36
|
+
_ocr_cache: dict[str, "OCRResult"] = {}
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
async def extract_text(
|
|
@@ -36,7 +36,7 @@ Quick Start:
|
|
|
36
36
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
|
-
from typing import TYPE_CHECKING,
|
|
39
|
+
from typing import TYPE_CHECKING, Optional
|
|
40
40
|
|
|
41
41
|
try:
|
|
42
42
|
from svc_infra.documents import (
|
|
@@ -242,7 +242,7 @@ def list_documents(
|
|
|
242
242
|
tax_year: Optional[int] = None,
|
|
243
243
|
limit: int = 100,
|
|
244
244
|
offset: int = 0,
|
|
245
|
-
) ->
|
|
245
|
+
) -> list["FinancialDocument"]:
|
|
246
246
|
"""
|
|
247
247
|
List user's financial documents with optional filters (delegates to svc-infra).
|
|
248
248
|
|
|
@@ -29,7 +29,7 @@ add_goals(app)
|
|
|
29
29
|
|
|
30
30
|
import logging
|
|
31
31
|
from datetime import datetime
|
|
32
|
-
from typing import Any,
|
|
32
|
+
from typing import Any, Optional, cast
|
|
33
33
|
|
|
34
34
|
from fastapi import FastAPI, HTTPException, status, Query, Body
|
|
35
35
|
from pydantic import BaseModel, Field
|
|
@@ -89,7 +89,7 @@ class CreateGoalRequest(BaseModel):
|
|
|
89
89
|
description: Optional[str] = Field(None, description="Goal description")
|
|
90
90
|
current_amount: Optional[float] = Field(0.0, ge=0, description="Current amount")
|
|
91
91
|
auto_contribute: Optional[bool] = Field(False, description="Auto-contribute enabled")
|
|
92
|
-
tags: Optional[
|
|
92
|
+
tags: Optional[list[str]] = Field(None, description="Goal tags")
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
class UpdateGoalRequest(BaseModel):
|
|
@@ -102,7 +102,7 @@ class UpdateGoalRequest(BaseModel):
|
|
|
102
102
|
current_amount: Optional[float] = Field(None, ge=0)
|
|
103
103
|
status: Optional[GoalStatus] = None
|
|
104
104
|
auto_contribute: Optional[bool] = None
|
|
105
|
-
tags: Optional[
|
|
105
|
+
tags: Optional[list[str]] = None
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
class AddMilestoneRequest(BaseModel):
|
|
@@ -238,14 +238,14 @@ def add_goals(
|
|
|
238
238
|
except ValueError as e:
|
|
239
239
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
240
240
|
|
|
241
|
-
@router.get("", response_model=
|
|
241
|
+
@router.get("", response_model=list[dict])
|
|
242
242
|
async def list_goals_endpoint(
|
|
243
243
|
user_id: Optional[str] = Query(
|
|
244
244
|
None, description="User identifier (optional, returns all if not provided)"
|
|
245
245
|
),
|
|
246
246
|
goal_type: Optional[str] = Query(None, description="Filter by goal type"),
|
|
247
247
|
status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
|
|
248
|
-
) ->
|
|
248
|
+
) -> list[dict]:
|
|
249
249
|
"""
|
|
250
250
|
List all goals for a user with optional filters.
|
|
251
251
|
|
|
@@ -442,8 +442,8 @@ def add_goals(
|
|
|
442
442
|
except ValueError as e:
|
|
443
443
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
444
444
|
|
|
445
|
-
@router.get("/{goal_id}/milestones", response_model=
|
|
446
|
-
async def list_milestones_endpoint(goal_id: str) ->
|
|
445
|
+
@router.get("/{goal_id}/milestones", response_model=list[dict])
|
|
446
|
+
async def list_milestones_endpoint(goal_id: str) -> list[dict]:
|
|
447
447
|
"""
|
|
448
448
|
List all milestones for a goal.
|
|
449
449
|
|
|
@@ -540,8 +540,8 @@ def add_goals(
|
|
|
540
540
|
except ValueError as e:
|
|
541
541
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
542
542
|
|
|
543
|
-
@router.get("/{goal_id}/funding", response_model=
|
|
544
|
-
async def list_funding_sources_endpoint(goal_id: str) ->
|
|
543
|
+
@router.get("/{goal_id}/funding", response_model=list[dict])
|
|
544
|
+
async def list_funding_sources_endpoint(goal_id: str) -> list[dict]:
|
|
545
545
|
"""
|
|
546
546
|
List all funding sources for a goal.
|
|
547
547
|
|
|
@@ -22,14 +22,12 @@ Example:
|
|
|
22
22
|
>>> # Raises ValueError if total allocation > 100%
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
from typing import Dict, List
|
|
26
|
-
|
|
27
25
|
from fin_infra.goals.models import FundingSource
|
|
28
26
|
from fin_infra.goals.management import get_goal
|
|
29
27
|
|
|
30
28
|
# In-memory storage for funding allocations
|
|
31
29
|
# Structure: {account_id: {goal_id: allocation_percent}}
|
|
32
|
-
_FUNDING_STORE:
|
|
30
|
+
_FUNDING_STORE: dict[str, dict[str, float]] = {}
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
def link_account_to_goal(
|
|
@@ -108,7 +106,7 @@ def link_account_to_goal(
|
|
|
108
106
|
)
|
|
109
107
|
|
|
110
108
|
|
|
111
|
-
def get_goal_funding_sources(goal_id: str) ->
|
|
109
|
+
def get_goal_funding_sources(goal_id: str) -> list[FundingSource]:
|
|
112
110
|
"""
|
|
113
111
|
Get all accounts funding a specific goal.
|
|
114
112
|
|
|
@@ -154,7 +152,7 @@ def get_goal_funding_sources(goal_id: str) -> List[FundingSource]:
|
|
|
154
152
|
return funding_sources
|
|
155
153
|
|
|
156
154
|
|
|
157
|
-
def get_account_allocations(account_id: str) ->
|
|
155
|
+
def get_account_allocations(account_id: str) -> dict[str, float]:
|
|
158
156
|
"""
|
|
159
157
|
Get all goal allocations for a specific account.
|
|
160
158
|
|
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
from datetime import date
|
|
21
21
|
from decimal import Decimal
|
|
22
22
|
from enum import Enum
|
|
23
|
-
from typing import TYPE_CHECKING,
|
|
23
|
+
from typing import TYPE_CHECKING, Optional
|
|
24
24
|
|
|
25
25
|
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
26
26
|
|
|
@@ -374,12 +374,12 @@ class InvestmentAccount(BaseModel):
|
|
|
374
374
|
subtype: Optional[str] = Field(None, description="Account subtype (401k, ira, brokerage)")
|
|
375
375
|
|
|
376
376
|
# Balances
|
|
377
|
-
balances:
|
|
377
|
+
balances: dict[str, Optional[Decimal]] = Field(
|
|
378
378
|
..., description="Current, available, and limit balances"
|
|
379
379
|
)
|
|
380
380
|
|
|
381
381
|
# Holdings
|
|
382
|
-
holdings:
|
|
382
|
+
holdings: list[Holding] = Field(default_factory=list, description="List of holdings in account")
|
|
383
383
|
|
|
384
384
|
if TYPE_CHECKING:
|
|
385
385
|
|
|
@@ -487,11 +487,11 @@ class AssetAllocation(BaseModel):
|
|
|
487
487
|
},
|
|
488
488
|
)
|
|
489
489
|
|
|
490
|
-
by_security_type:
|
|
490
|
+
by_security_type: dict[SecurityType, float] = Field(
|
|
491
491
|
default_factory=dict,
|
|
492
492
|
description="Percentage breakdown by security type (equity, bond, etc.)",
|
|
493
493
|
)
|
|
494
|
-
by_sector:
|
|
494
|
+
by_sector: dict[str, float] = Field(
|
|
495
495
|
default_factory=dict,
|
|
496
496
|
description="Percentage breakdown by sector (Technology, Healthcare, etc.)",
|
|
497
497
|
)
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from datetime import date
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Optional
|
|
8
8
|
|
|
9
9
|
# Import will work once models.py is fully implemented in Task 3
|
|
10
10
|
# For now, using TYPE_CHECKING to avoid circular imports
|
|
@@ -30,8 +30,8 @@ class InvestmentProvider(ABC):
|
|
|
30
30
|
|
|
31
31
|
@abstractmethod
|
|
32
32
|
async def get_holdings(
|
|
33
|
-
self, access_token: str, account_ids: Optional[
|
|
34
|
-
) ->
|
|
33
|
+
self, access_token: str, account_ids: Optional[list[str]] = None
|
|
34
|
+
) -> list[Holding]:
|
|
35
35
|
"""Fetch holdings for investment accounts.
|
|
36
36
|
|
|
37
37
|
Args:
|
|
@@ -54,8 +54,8 @@ class InvestmentProvider(ABC):
|
|
|
54
54
|
access_token: str,
|
|
55
55
|
start_date: date,
|
|
56
56
|
end_date: date,
|
|
57
|
-
account_ids: Optional[
|
|
58
|
-
) ->
|
|
57
|
+
account_ids: Optional[list[str]] = None,
|
|
58
|
+
) -> list[InvestmentTransaction]:
|
|
59
59
|
"""Fetch investment transactions within date range.
|
|
60
60
|
|
|
61
61
|
Args:
|
|
@@ -77,7 +77,7 @@ class InvestmentProvider(ABC):
|
|
|
77
77
|
pass
|
|
78
78
|
|
|
79
79
|
@abstractmethod
|
|
80
|
-
async def get_securities(self, access_token: str, security_ids:
|
|
80
|
+
async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
|
|
81
81
|
"""Fetch security details (ticker, name, type, current price).
|
|
82
82
|
|
|
83
83
|
Args:
|
|
@@ -95,7 +95,7 @@ class InvestmentProvider(ABC):
|
|
|
95
95
|
pass
|
|
96
96
|
|
|
97
97
|
@abstractmethod
|
|
98
|
-
async def get_investment_accounts(self, access_token: str) ->
|
|
98
|
+
async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
|
|
99
99
|
"""Fetch investment accounts with aggregated holdings.
|
|
100
100
|
|
|
101
101
|
Args:
|
|
@@ -113,7 +113,7 @@ class InvestmentProvider(ABC):
|
|
|
113
113
|
|
|
114
114
|
# Helper methods (concrete - shared across all providers)
|
|
115
115
|
|
|
116
|
-
def calculate_allocation(self, holdings:
|
|
116
|
+
def calculate_allocation(self, holdings: list[Holding]) -> AssetAllocation:
|
|
117
117
|
"""Calculate asset allocation by security type and sector.
|
|
118
118
|
|
|
119
119
|
Groups holdings by security type (equity, bond, ETF, etc.) and calculates
|
|
@@ -183,7 +183,7 @@ class InvestmentProvider(ABC):
|
|
|
183
183
|
cash_percent=cash_percent,
|
|
184
184
|
)
|
|
185
185
|
|
|
186
|
-
def calculate_portfolio_metrics(self, holdings:
|
|
186
|
+
def calculate_portfolio_metrics(self, holdings: list[Holding]) -> dict:
|
|
187
187
|
"""Calculate total value, cost basis, unrealized gain/loss.
|
|
188
188
|
|
|
189
189
|
Aggregates holdings to calculate portfolio-level metrics.
|
|
@@ -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,
|
|
13
|
+
from typing import Any, Optional, cast
|
|
14
14
|
|
|
15
15
|
try:
|
|
16
16
|
from plaid.api import plaid_api
|
|
@@ -131,8 +131,8 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
131
131
|
return cast(str, hosts.get(environment.lower(), plaid.Environment.Sandbox))
|
|
132
132
|
|
|
133
133
|
async def get_holdings(
|
|
134
|
-
self, access_token: str, account_ids: Optional[
|
|
135
|
-
) ->
|
|
134
|
+
self, access_token: str, account_ids: Optional[list[str]] = None
|
|
135
|
+
) -> list[Holding]:
|
|
136
136
|
"""Fetch investment holdings from Plaid.
|
|
137
137
|
|
|
138
138
|
Retrieves holdings with security details, quantity, cost basis, and current value.
|
|
@@ -189,8 +189,8 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
189
189
|
access_token: str,
|
|
190
190
|
start_date: date,
|
|
191
191
|
end_date: date,
|
|
192
|
-
account_ids: Optional[
|
|
193
|
-
) ->
|
|
192
|
+
account_ids: Optional[list[str]] = None,
|
|
193
|
+
) -> list[InvestmentTransaction]:
|
|
194
194
|
"""Fetch investment transactions from Plaid.
|
|
195
195
|
|
|
196
196
|
Retrieves buy/sell/dividend transactions within the specified date range.
|
|
@@ -252,7 +252,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
252
252
|
except ApiException as e:
|
|
253
253
|
raise self._transform_error(e)
|
|
254
254
|
|
|
255
|
-
async def get_securities(self, access_token: str, security_ids:
|
|
255
|
+
async def get_securities(self, access_token: str, security_ids: list[str]) -> list[Security]:
|
|
256
256
|
"""Fetch security details from Plaid holdings.
|
|
257
257
|
|
|
258
258
|
Note: Plaid doesn't have a dedicated securities endpoint.
|
|
@@ -290,7 +290,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
290
290
|
except ApiException as e:
|
|
291
291
|
raise self._transform_error(e)
|
|
292
292
|
|
|
293
|
-
async def get_investment_accounts(self, access_token: str) ->
|
|
293
|
+
async def get_investment_accounts(self, access_token: str) -> list[InvestmentAccount]:
|
|
294
294
|
"""Fetch investment accounts with aggregated holdings.
|
|
295
295
|
|
|
296
296
|
Returns accounts with total value, cost basis, and unrealized P&L.
|
|
@@ -321,7 +321,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
# Group holdings by account
|
|
324
|
-
accounts_map:
|
|
324
|
+
accounts_map: dict[str, dict[str, Any]] = {}
|
|
325
325
|
for plaid_holding in response.holdings:
|
|
326
326
|
holding_dict = plaid_holding.to_dict()
|
|
327
327
|
account_id = holding_dict["account_id"]
|
|
@@ -373,7 +373,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
373
373
|
|
|
374
374
|
# Helper methods for data transformation
|
|
375
375
|
|
|
376
|
-
def _transform_security(self, plaid_security:
|
|
376
|
+
def _transform_security(self, plaid_security: dict[str, Any]) -> Security:
|
|
377
377
|
"""Transform Plaid security data to Security model."""
|
|
378
378
|
# Handle close_price - Plaid may return None for securities without recent pricing
|
|
379
379
|
close_price_raw = plaid_security.get("close_price")
|
|
@@ -394,7 +394,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
394
394
|
currency=plaid_security.get("iso_currency_code", "USD"),
|
|
395
395
|
)
|
|
396
396
|
|
|
397
|
-
def _transform_holding(self, plaid_holding:
|
|
397
|
+
def _transform_holding(self, plaid_holding: dict[str, Any], security: Security) -> Holding:
|
|
398
398
|
"""Transform Plaid holding data to Holding model."""
|
|
399
399
|
return Holding(
|
|
400
400
|
account_id=plaid_holding["account_id"],
|
|
@@ -410,7 +410,7 @@ class PlaidInvestmentProvider(InvestmentProvider):
|
|
|
410
410
|
)
|
|
411
411
|
|
|
412
412
|
def _transform_transaction(
|
|
413
|
-
self, plaid_transaction:
|
|
413
|
+
self, plaid_transaction: dict[str, Any], security: Security
|
|
414
414
|
) -> InvestmentTransaction:
|
|
415
415
|
"""Transform Plaid investment transaction to InvestmentTransaction model."""
|
|
416
416
|
# Map Plaid transaction type to our enum
|