fin-infra 0.1.69__py3-none-any.whl → 0.4.0__py3-none-any.whl

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 (131) hide show
  1. fin_infra/__init__.py +53 -3
  2. fin_infra/analytics/__init__.py +13 -2
  3. fin_infra/analytics/add.py +24 -24
  4. fin_infra/analytics/cash_flow.py +3 -3
  5. fin_infra/analytics/ease.py +19 -20
  6. fin_infra/analytics/models.py +5 -5
  7. fin_infra/analytics/portfolio.py +18 -18
  8. fin_infra/analytics/projections.py +1 -3
  9. fin_infra/analytics/spending.py +4 -5
  10. fin_infra/banking/__init__.py +27 -28
  11. fin_infra/banking/history.py +12 -13
  12. fin_infra/banking/utils.py +27 -26
  13. fin_infra/brokerage/__init__.py +29 -31
  14. fin_infra/budgets/__init__.py +3 -3
  15. fin_infra/budgets/add.py +16 -17
  16. fin_infra/budgets/alerts.py +4 -4
  17. fin_infra/budgets/ease.py +1 -2
  18. fin_infra/budgets/models.py +1 -2
  19. fin_infra/budgets/templates.py +4 -4
  20. fin_infra/budgets/tracker.py +4 -4
  21. fin_infra/cashflows/__init__.py +3 -3
  22. fin_infra/cashflows/core.py +1 -1
  23. fin_infra/categorization/__init__.py +1 -1
  24. fin_infra/categorization/add.py +2 -3
  25. fin_infra/categorization/ease.py +3 -3
  26. fin_infra/categorization/engine.py +18 -15
  27. fin_infra/categorization/llm_layer.py +13 -10
  28. fin_infra/categorization/models.py +3 -4
  29. fin_infra/categorization/rules.py +2 -4
  30. fin_infra/categorization/taxonomy.py +2 -2
  31. fin_infra/chat/__init__.py +6 -6
  32. fin_infra/chat/planning.py +1 -2
  33. fin_infra/cli/cmds/scaffold_cmds.py +16 -17
  34. fin_infra/clients/__init__.py +23 -1
  35. fin_infra/clients/base.py +1 -1
  36. fin_infra/clients/plaid.py +2 -2
  37. fin_infra/compliance/__init__.py +5 -4
  38. fin_infra/credit/add.py +6 -7
  39. fin_infra/credit/experian/auth.py +2 -2
  40. fin_infra/credit/experian/client.py +1 -1
  41. fin_infra/credit/experian/parser.py +5 -5
  42. fin_infra/credit/experian/provider.py +4 -4
  43. fin_infra/crypto/__init__.py +9 -11
  44. fin_infra/crypto/insights.py +4 -3
  45. fin_infra/documents/add.py +6 -8
  46. fin_infra/documents/analysis.py +9 -9
  47. fin_infra/documents/ease.py +14 -14
  48. fin_infra/documents/models.py +5 -6
  49. fin_infra/documents/ocr.py +7 -7
  50. fin_infra/documents/storage.py +21 -13
  51. fin_infra/exceptions.py +0 -1
  52. fin_infra/goals/__init__.py +8 -8
  53. fin_infra/goals/add.py +36 -36
  54. fin_infra/goals/funding.py +4 -6
  55. fin_infra/goals/management.py +5 -6
  56. fin_infra/goals/milestones.py +7 -8
  57. fin_infra/goals/models.py +9 -13
  58. fin_infra/insights/__init__.py +6 -3
  59. fin_infra/insights/aggregator.py +1 -1
  60. fin_infra/investments/__init__.py +3 -3
  61. fin_infra/investments/add.py +23 -23
  62. fin_infra/investments/ease.py +2 -2
  63. fin_infra/investments/models.py +27 -29
  64. fin_infra/investments/providers/base.py +12 -13
  65. fin_infra/investments/providers/plaid.py +52 -26
  66. fin_infra/investments/providers/snaptrade.py +19 -19
  67. fin_infra/investments/scaffold_templates/README.md +17 -17
  68. fin_infra/markets/__init__.py +7 -5
  69. fin_infra/models/__init__.py +10 -10
  70. fin_infra/models/accounts.py +4 -5
  71. fin_infra/models/brokerage.py +2 -1
  72. fin_infra/models/candle.py +1 -0
  73. fin_infra/models/money.py +1 -0
  74. fin_infra/models/quotes.py +4 -3
  75. fin_infra/models/tax.py +2 -1
  76. fin_infra/models/transactions.py +4 -5
  77. fin_infra/net_worth/__init__.py +8 -1
  78. fin_infra/net_worth/aggregator.py +5 -3
  79. fin_infra/net_worth/calculator.py +1 -1
  80. fin_infra/net_worth/insights.py +7 -8
  81. fin_infra/normalization/__init__.py +4 -4
  82. fin_infra/normalization/currency_converter.py +7 -8
  83. fin_infra/normalization/models.py +9 -10
  84. fin_infra/normalization/providers/exchangerate.py +5 -5
  85. fin_infra/normalization/providers/static_mappings.py +1 -1
  86. fin_infra/normalization/symbol_resolver.py +3 -4
  87. fin_infra/obs/classifier.py +3 -3
  88. fin_infra/providers/banking/plaid_client.py +5 -5
  89. fin_infra/providers/banking/teller_client.py +7 -6
  90. fin_infra/providers/base.py +27 -2
  91. fin_infra/providers/brokerage/alpaca.py +4 -4
  92. fin_infra/providers/market/alphavantage.py +6 -11
  93. fin_infra/providers/market/ccxt_crypto.py +19 -3
  94. fin_infra/providers/market/coingecko.py +5 -6
  95. fin_infra/providers/market/yahoo.py +23 -8
  96. fin_infra/providers/tax/__init__.py +1 -1
  97. fin_infra/providers/tax/irs.py +1 -1
  98. fin_infra/providers/tax/mock.py +5 -5
  99. fin_infra/providers/tax/taxbit.py +1 -1
  100. fin_infra/recurring/__init__.py +6 -6
  101. fin_infra/recurring/add.py +6 -5
  102. fin_infra/recurring/detector.py +7 -7
  103. fin_infra/recurring/detectors_llm.py +10 -10
  104. fin_infra/recurring/ease.py +6 -8
  105. fin_infra/recurring/insights.py +25 -24
  106. fin_infra/recurring/normalizer.py +7 -7
  107. fin_infra/recurring/normalizers.py +31 -30
  108. fin_infra/recurring/summary.py +13 -15
  109. fin_infra/scaffold/budgets.py +9 -9
  110. fin_infra/scaffold/goals.py +9 -9
  111. fin_infra/security/__init__.py +8 -8
  112. fin_infra/security/add.py +1 -2
  113. fin_infra/security/audit.py +6 -7
  114. fin_infra/security/encryption.py +6 -6
  115. fin_infra/security/models.py +7 -7
  116. fin_infra/security/pii_filter.py +16 -16
  117. fin_infra/security/token_store.py +2 -3
  118. fin_infra/settings.py +2 -1
  119. fin_infra/tax/__init__.py +1 -1
  120. fin_infra/tax/add.py +5 -4
  121. fin_infra/tax/tlh.py +10 -10
  122. fin_infra/utils/__init__.py +15 -1
  123. fin_infra/utils/deprecation.py +161 -0
  124. fin_infra/utils/http.py +4 -3
  125. fin_infra/utils/retry.py +2 -1
  126. {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/METADATA +30 -16
  127. fin_infra-0.4.0.dist-info/RECORD +181 -0
  128. fin_infra-0.1.69.dist-info/RECORD +0 -180
  129. {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/LICENSE +0 -0
  130. {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/WHEEL +0 -0
  131. {fin_infra-0.1.69.dist-info → fin_infra-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -12,7 +12,6 @@ Usage:
12
12
  from __future__ import annotations
13
13
 
14
14
  from pathlib import Path
15
- from typing import Optional
16
15
 
17
16
  import click
18
17
  import typer
@@ -54,47 +53,47 @@ def cmd_scaffold(
54
53
  "--overwrite/--no-overwrite",
55
54
  help="Overwrite existing files",
56
55
  ),
57
- models_filename: Optional[str] = typer.Option(
56
+ models_filename: str | None = typer.Option(
58
57
  None,
59
58
  "--models-filename",
60
59
  help="Custom filename for models (default: {domain}.py)",
61
60
  ),
62
- schemas_filename: Optional[str] = typer.Option(
61
+ schemas_filename: str | None = typer.Option(
63
62
  None,
64
63
  "--schemas-filename",
65
64
  help="Custom filename for schemas (default: {domain}_schemas.py)",
66
65
  ),
67
- repository_filename: Optional[str] = typer.Option(
66
+ repository_filename: str | None = typer.Option(
68
67
  None,
69
68
  "--repository-filename",
70
69
  help="Custom filename for repository (default: {domain}_repository.py)",
71
70
  ),
72
71
  ) -> None:
73
72
  """Generate SQLAlchemy models, Pydantic schemas, and repository code from templates.
74
-
73
+
75
74
  The scaffold command generates production-ready persistence layer code that works
76
75
  seamlessly with svc-infra's add_sql_resources() for automatic CRUD APIs.
77
-
76
+
78
77
  Examples:
79
78
  # Basic scaffold (models + schemas + repository)
80
79
  fin-infra scaffold budgets --dest-dir app/models/
81
-
80
+
82
81
  # Financial goals tracking
83
82
  fin-infra scaffold goals --dest-dir app/models/goals/
84
-
83
+
85
84
  # With multi-tenancy and soft deletes
86
85
  fin-infra scaffold budgets --dest-dir app/models/ \
87
86
  --include-tenant --include-soft-delete
88
-
87
+
89
88
  # Without repository (use svc-infra SqlRepository directly)
90
89
  fin-infra scaffold goals --dest-dir app/models/ \\
91
90
  --no-with-repository
92
-
91
+
93
92
  # Custom filenames
94
93
  fin-infra scaffold budgets --dest-dir app/models/ \\
95
94
  --models-filename custom_budget.py \\
96
95
  --schemas-filename custom_schemas.py
97
-
96
+
98
97
  After scaffolding, integrate with svc-infra:
99
98
  1. Run migrations: svc-infra revision -m "add budgets" --autogenerate
100
99
  2. Apply: svc-infra upgrade head
@@ -103,7 +102,7 @@ def cmd_scaffold(
103
102
  # Validate required parameters
104
103
  if dest_dir is None:
105
104
  typer.secho(
106
- " Error: --dest-dir is required",
105
+ "[X] Error: --dest-dir is required",
107
106
  fg=typer.colors.RED,
108
107
  err=True,
109
108
  )
@@ -138,7 +137,7 @@ def cmd_scaffold(
138
137
  )
139
138
  else:
140
139
  typer.secho(
141
- f" Unknown domain: {domain}. Must be one of: budgets, goals",
140
+ f"[X] Unknown domain: {domain}. Must be one of: budgets, goals",
142
141
  fg=typer.colors.RED,
143
142
  err=True,
144
143
  )
@@ -146,7 +145,7 @@ def cmd_scaffold(
146
145
 
147
146
  # Display results
148
147
  typer.echo("")
149
- typer.secho("📦 Scaffold Results:", bold=True)
148
+ typer.secho(" Scaffold Results:", bold=True)
150
149
  typer.echo("")
151
150
 
152
151
  files = result.get("files", [])
@@ -158,7 +157,7 @@ def cmd_scaffold(
158
157
  action = file_info.get("action", "unknown")
159
158
 
160
159
  if action == "wrote":
161
- typer.secho(f" Created: {path}", fg=typer.colors.GREEN)
160
+ typer.secho(f" [OK] Created: {path}", fg=typer.colors.GREEN)
162
161
  wrote_count += 1
163
162
  elif action == "skipped":
164
163
  reason = file_info.get("reason", "unknown")
@@ -169,7 +168,7 @@ def cmd_scaffold(
169
168
 
170
169
  # Summary
171
170
  typer.echo("")
172
- typer.secho(f" Done! Created {wrote_count} file(s), skipped {skipped_count}.", bold=True)
171
+ typer.secho(f" Done! Created {wrote_count} file(s), skipped {skipped_count}.", bold=True)
173
172
  typer.echo("")
174
173
 
175
174
  # Next steps
@@ -188,7 +187,7 @@ def cmd_scaffold(
188
187
  }
189
188
  route_prefix = prefix_map.get(domain, f"/{domain}")
190
189
 
191
- typer.secho("📝 Next Steps:", bold=True)
190
+ typer.secho(" Next Steps:", bold=True)
192
191
  typer.echo("")
193
192
  typer.echo(" 1. Review generated files and customize as needed")
194
193
  typer.echo(" 2. Run migrations:")
@@ -1,3 +1,25 @@
1
- from .base import BankingClient, MarketDataClient, CreditClient
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
+ )
2
24
 
3
25
  __all__ = ["BankingClient", "MarketDataClient", "CreditClient"]
fin_infra/clients/base.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import abc
4
- from typing import Iterable, Sequence
4
+ from collections.abc import Iterable, Sequence
5
5
 
6
6
  from ..models import Account, Quote, Transaction
7
7
 
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Sequence
3
+ from collections.abc import Sequence
4
4
 
5
- from .base import BankingClient
6
5
  from ..models import Account
6
+ from .base import BankingClient
7
7
 
8
8
 
9
9
  class PlaidClient(BankingClient):
@@ -20,8 +20,9 @@ Example:
20
20
  from __future__ import annotations
21
21
 
22
22
  import logging
23
+ from collections.abc import Callable
23
24
  from datetime import datetime
24
- from typing import Any, Callable, TYPE_CHECKING, cast
25
+ from typing import TYPE_CHECKING, Any, cast
25
26
 
26
27
  if TYPE_CHECKING:
27
28
  from fastapi import FastAPI, Request, Response
@@ -32,7 +33,7 @@ logger = logging.getLogger(__name__)
32
33
 
33
34
 
34
35
  def log_compliance_event(
35
- app: "FastAPI",
36
+ app: FastAPI,
36
37
  event: str,
37
38
  context: dict[str, Any] | None = None,
38
39
  ) -> None:
@@ -62,7 +63,7 @@ def log_compliance_event(
62
63
 
63
64
 
64
65
  def add_compliance_tracking(
65
- app: "FastAPI",
66
+ app: FastAPI,
66
67
  *,
67
68
  track_banking: bool = True,
68
69
  track_credit: bool = True,
@@ -111,7 +112,7 @@ def add_compliance_tracking(
111
112
  """
112
113
 
113
114
  @app.middleware("http")
114
- async def compliance_tracking_middleware(request: "Request", call_next: Callable) -> "Response":
115
+ async def compliance_tracking_middleware(request: Request, call_next: Callable) -> Response:
115
116
  """Middleware to track compliance events for financial endpoints."""
116
117
  path = request.url.path
117
118
  method = request.method
fin_infra/credit/add.py CHANGED
@@ -25,15 +25,14 @@ Example:
25
25
  import logging
26
26
  from typing import cast
27
27
 
28
- from fastapi import FastAPI, Depends, HTTPException, status
29
-
30
- from svc_infra.api.fastapi.dual.protected import user_router, RequireUser
28
+ from fastapi import Depends, FastAPI, HTTPException, status
31
29
  from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
30
+ from svc_infra.api.fastapi.dual.protected import RequireUser, user_router
32
31
  from svc_infra.cache import resource
33
32
  from svc_infra.webhooks import add_webhooks
34
33
 
34
+ from fin_infra.models.credit import CreditReport, CreditScore
35
35
  from fin_infra.providers.base import CreditProvider
36
- from fin_infra.models.credit import CreditScore, CreditReport
37
36
 
38
37
  logger = logging.getLogger(__name__)
39
38
 
@@ -156,8 +155,8 @@ def add_credit(
156
155
  if enable_webhooks and hasattr(app.state, "webhooks_outbox"):
157
156
  try:
158
157
  # Get webhook service from app state
159
- from svc_infra.webhooks.service import WebhookService
160
158
  from svc_infra.db.outbox import OutboxStore
159
+ from svc_infra.webhooks.service import WebhookService
161
160
 
162
161
  outbox: OutboxStore = app.state.webhooks_outbox
163
162
  subs = app.state.webhooks_subscriptions
@@ -176,7 +175,7 @@ def add_credit(
176
175
  # Don't fail request if webhook publishing fails
177
176
  logger.warning(f"Failed to publish credit.score_changed webhook: {e}")
178
177
 
179
- return cast(CreditScore, score)
178
+ return cast("CreditScore", score)
180
179
 
181
180
  @router.post("/report", response_model=CreditReport)
182
181
  @credit_resource.cache_read(ttl=cache_ttl, suffix="report")
@@ -220,7 +219,7 @@ def add_credit(
220
219
  detail="Credit bureau service unavailable",
221
220
  )
222
221
 
223
- return cast(CreditReport, report)
222
+ return cast("CreditReport", report)
224
223
 
225
224
  # Mount router with dual routes (with/without trailing slash)
226
225
  app.include_router(router, include_in_schema=True)
@@ -86,7 +86,7 @@ class ExperianAuthManager:
86
86
  >>> headers = {"Authorization": f"Bearer {token}"}
87
87
  """
88
88
  # Call the cached implementation with client_id for cache key
89
- return cast(str, await self._get_token_cached(client_id=self.client_id))
89
+ return cast("str", await self._get_token_cached(client_id=self.client_id))
90
90
 
91
91
  @cache_read(
92
92
  key="oauth_token:experian:{client_id}", # Use client_id for uniqueness
@@ -141,7 +141,7 @@ class ExperianAuthManager:
141
141
 
142
142
  # Parse and return token
143
143
  data = response.json()
144
- return cast(str, data["access_token"])
144
+ return cast("str", data["access_token"])
145
145
 
146
146
  async def invalidate(self) -> None:
147
147
  """Invalidate cached token for THIS client (force refresh on next get_token call).
@@ -155,7 +155,7 @@ class ExperianClient:
155
155
  **kwargs,
156
156
  )
157
157
  response.raise_for_status()
158
- return cast(dict[str, Any], response.json())
158
+ return cast("dict[str, Any]", response.json())
159
159
 
160
160
  except httpx.HTTPStatusError as e:
161
161
  # Parse error response
@@ -1,11 +1,11 @@
1
1
  """Response parsers for Experian API data to fin_infra models.
2
2
 
3
3
  Converts Experian API JSON responses to typed Pydantic models:
4
- - parse_credit_score(): dict CreditScore
5
- - parse_credit_report(): dict CreditReport
6
- - parse_account(): dict CreditAccount
7
- - parse_inquiry(): dict CreditInquiry
8
- - parse_public_record(): dict PublicRecord
4
+ - parse_credit_score(): dict -> CreditScore
5
+ - parse_credit_report(): dict -> CreditReport
6
+ - parse_account(): dict -> CreditAccount
7
+ - parse_inquiry(): dict -> CreditInquiry
8
+ - parse_public_record(): dict -> PublicRecord
9
9
 
10
10
  Example:
11
11
  >>> data = await client.get_credit_score("user123")
@@ -30,7 +30,7 @@ Example:
30
30
  """
31
31
 
32
32
  import logging
33
- from datetime import datetime, timezone
33
+ from datetime import UTC, datetime
34
34
  from typing import Literal, cast
35
35
 
36
36
  from fin_infra.credit.experian.auth import ExperianAuthManager
@@ -177,7 +177,7 @@ class ExperianProvider(CreditProvider):
177
177
 
178
178
  # FCRA Audit Log - REQUIRED for regulatory compliance (15 USC § 1681b)
179
179
  # This log must be retained for at least 2 years per FCRA requirements
180
- timestamp = datetime.now(timezone.utc).isoformat()
180
+ timestamp = datetime.now(UTC).isoformat()
181
181
  fcra_audit_logger.info(
182
182
  "FCRA_CREDIT_PULL",
183
183
  extra={
@@ -266,7 +266,7 @@ class ExperianProvider(CreditProvider):
266
266
  # FCRA Audit Log - REQUIRED for regulatory compliance (15 USC § 1681b)
267
267
  # Full credit report pulls have stricter requirements than score-only pulls
268
268
  # This log must be retained for at least 2 years per FCRA requirements
269
- timestamp = datetime.now(timezone.utc).isoformat()
269
+ timestamp = datetime.now(UTC).isoformat()
270
270
  fcra_audit_logger.info(
271
271
  "FCRA_CREDIT_PULL",
272
272
  extra={
@@ -360,4 +360,4 @@ class ExperianProvider(CreditProvider):
360
360
  signature_key=signature_key,
361
361
  )
362
362
 
363
- return cast(str, data.get("subscriptionId", "unknown"))
363
+ return cast("str", data.get("subscriptionId", "unknown"))
@@ -13,7 +13,7 @@ Quick start:
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- from datetime import datetime, timezone
16
+ from datetime import UTC, datetime
17
17
  from typing import TYPE_CHECKING, Literal
18
18
 
19
19
  if TYPE_CHECKING:
@@ -29,8 +29,8 @@ def easy_crypto(
29
29
  """Create a crypto data provider with zero or minimal configuration.
30
30
 
31
31
  Auto-detects provider based on environment variables:
32
- 1. If COINGECKO_API_KEY is set CoinGecko Pro
33
- 2. Otherwise CoinGecko Free (no key needed)
32
+ 1. If COINGECKO_API_KEY is set -> CoinGecko Pro
33
+ 2. Otherwise -> CoinGecko Free (no key needed)
34
34
 
35
35
  Args:
36
36
  provider: Provider name ("coingecko"). If None, defaults to coingecko.
@@ -74,7 +74,7 @@ def easy_crypto(
74
74
 
75
75
 
76
76
  def add_crypto_data(
77
- app: "FastAPI",
77
+ app: FastAPI,
78
78
  *,
79
79
  provider: str | CryptoDataProvider | None = None,
80
80
  prefix: str = "/crypto",
@@ -131,9 +131,9 @@ def add_crypto_data(
131
131
  >>> add_observability(app)
132
132
  >>> crypto = add_crypto_data(app)
133
133
  """
134
- from svc_infra.api.fastapi.dual.public import public_router
135
- from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
136
134
  from fastapi import HTTPException, Query
135
+ from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
136
+ from svc_infra.api.fastapi.dual.public import public_router
137
137
 
138
138
  # Initialize provider if string or None
139
139
  if isinstance(provider, str):
@@ -168,11 +168,11 @@ def add_crypto_data(
168
168
  "price": float(ticker.price),
169
169
  "as_of": ticker.as_of.isoformat()
170
170
  if ticker.as_of
171
- else datetime.now(timezone.utc).isoformat(),
171
+ else datetime.now(UTC).isoformat(),
172
172
  }
173
173
  except Exception as e:
174
174
  raise HTTPException(
175
- status_code=400, detail=f"Error fetching ticker for {symbol}: {str(e)}"
175
+ status_code=400, detail=f"Error fetching ticker for {symbol}: {e!s}"
176
176
  )
177
177
 
178
178
  @router.get("/ohlcv/{symbol}")
@@ -216,9 +216,7 @@ def add_crypto_data(
216
216
  ],
217
217
  }
218
218
  except Exception as e:
219
- raise HTTPException(
220
- status_code=400, detail=f"Error fetching OHLCV for {symbol}: {str(e)}"
221
- )
219
+ raise HTTPException(status_code=400, detail=f"Error fetching OHLCV for {symbol}: {e!s}")
222
220
 
223
221
  # Mount router
224
222
  app.include_router(router, include_in_schema=True)
@@ -8,6 +8,7 @@ CRITICAL: Uses ai-infra.llm.LLM (NEVER custom LLM clients).
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import logging
11
12
  from datetime import datetime
12
13
  from decimal import Decimal
13
14
  from typing import TYPE_CHECKING
@@ -17,6 +18,8 @@ from pydantic import BaseModel, Field
17
18
  if TYPE_CHECKING:
18
19
  from ai_infra.llm import LLM
19
20
 
21
+ logger = logging.getLogger(__name__)
22
+
20
23
 
21
24
  class CryptoInsight(BaseModel):
22
25
  """Personalized cryptocurrency insight.
@@ -284,8 +287,6 @@ Provide your insight:"""
284
287
  )
285
288
  )
286
289
  except Exception as e:
287
- # Graceful degradation - log error but don't fail
288
- # In production, use svc-infra logging
289
- print(f"Warning: LLM insight generation failed: {e}")
290
+ logger.warning("LLM insight generation failed: %s", e)
290
291
 
291
292
  return insights
@@ -23,23 +23,22 @@ Quick Start:
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
- from typing import TYPE_CHECKING, Optional
26
+ from typing import TYPE_CHECKING
27
27
 
28
28
  if TYPE_CHECKING:
29
29
  from fastapi import FastAPI
30
-
31
30
  from svc_infra.storage.base import StorageBackend
32
31
 
33
32
  from .ease import FinancialDocumentManager
34
33
 
35
34
 
36
35
  def add_documents(
37
- app: "FastAPI",
38
- storage: Optional["StorageBackend"] = None,
36
+ app: FastAPI,
37
+ storage: StorageBackend | None = None,
39
38
  default_ocr_provider: str = "tesseract",
40
39
  prefix: str = "/documents",
41
- tags: Optional[list[str]] = None,
42
- ) -> "FinancialDocumentManager":
40
+ tags: list[str] | None = None,
41
+ ) -> FinancialDocumentManager:
43
42
  """
44
43
  Add financial document management endpoints to FastAPI app.
45
44
 
@@ -87,7 +86,6 @@ def add_documents(
87
86
  - Stores manager on app.state.financial_documents
88
87
  """
89
88
  from fastapi import HTTPException
90
-
91
89
  from svc_infra.api.fastapi.dual.protected import user_router
92
90
 
93
91
  # Import svc-infra base function to mount base endpoints (with fallback)
@@ -128,7 +126,7 @@ def add_documents(
128
126
  @router.post("/{document_id}/ocr", response_model=OCRResult)
129
127
  async def extract_text_ocr(
130
128
  document_id: str,
131
- provider: Optional[str] = None,
129
+ provider: str | None = None,
132
130
  force_refresh: bool = False,
133
131
  ) -> OCRResult:
134
132
  """
@@ -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, Dict
27
+ from typing import TYPE_CHECKING
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  from svc_infra.storage.base import StorageBackend
@@ -32,14 +32,14 @@ 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: Dict[str, "DocumentAnalysis"] = {}
35
+ _analysis_cache: dict[str, DocumentAnalysis] = {}
36
36
 
37
37
 
38
38
  async def analyze_document(
39
- storage: "StorageBackend",
39
+ storage: StorageBackend,
40
40
  document_id: str,
41
41
  force_refresh: bool = False,
42
- ) -> "DocumentAnalysis":
42
+ ) -> DocumentAnalysis:
43
43
  """
44
44
  Analyze a document using AI to extract insights and recommendations.
45
45
 
@@ -165,7 +165,7 @@ Important: This analysis is not a substitute for professional financial advice.
165
165
  return prompt
166
166
 
167
167
 
168
- def _validate_analysis(analysis: "DocumentAnalysis") -> bool:
168
+ def _validate_analysis(analysis: DocumentAnalysis) -> bool:
169
169
  """
170
170
  Validate LLM analysis output.
171
171
 
@@ -201,7 +201,7 @@ def _validate_analysis(analysis: "DocumentAnalysis") -> bool:
201
201
  return True
202
202
 
203
203
 
204
- def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) -> "DocumentAnalysis":
204
+ def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
205
205
  """
206
206
  Specialized analysis for tax documents.
207
207
 
@@ -301,7 +301,7 @@ def _analyze_tax_document(ocr_text: str, metadata: dict, document_id: str) -> "D
301
301
  )
302
302
 
303
303
 
304
- def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) -> "DocumentAnalysis":
304
+ def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
305
305
  """
306
306
  Specialized analysis for bank statements.
307
307
 
@@ -352,7 +352,7 @@ def _analyze_bank_statement(ocr_text: str, metadata: dict, document_id: str) ->
352
352
  )
353
353
 
354
354
 
355
- def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) -> "DocumentAnalysis":
355
+ def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) -> DocumentAnalysis:
356
356
  """
357
357
  Specialized analysis for receipts.
358
358
 
@@ -394,7 +394,7 @@ def _analyze_receipt(ocr_text: str, metadata: dict, document_id: str) -> "Docume
394
394
 
395
395
  def _analyze_generic_document(
396
396
  ocr_text: str, document_type: str, metadata: dict, document_id: str
397
- ) -> "DocumentAnalysis":
397
+ ) -> DocumentAnalysis:
398
398
  """
399
399
  Generic analysis for other document types.
400
400
 
@@ -36,7 +36,7 @@ Quick Start:
36
36
 
37
37
  from __future__ import annotations
38
38
 
39
- from typing import TYPE_CHECKING, Optional
39
+ from typing import TYPE_CHECKING
40
40
 
41
41
  try:
42
42
  from svc_infra.documents import DocumentManager as BaseDocumentManager
@@ -94,7 +94,7 @@ class FinancialDocumentManager(BaseDocumentManager):
94
94
 
95
95
  def __init__(
96
96
  self,
97
- storage: "StorageBackend",
97
+ storage: StorageBackend,
98
98
  default_ocr_provider: str = "tesseract",
99
99
  ):
100
100
  """
@@ -111,12 +111,12 @@ class FinancialDocumentManager(BaseDocumentManager):
111
111
  self,
112
112
  user_id: str,
113
113
  file: bytes,
114
- document_type: "DocumentType",
114
+ document_type: DocumentType,
115
115
  filename: str,
116
- metadata: Optional[dict] = None,
117
- tax_year: Optional[int] = None,
118
- form_type: Optional[str] = None,
119
- ) -> "FinancialDocument":
116
+ metadata: dict | None = None,
117
+ tax_year: int | None = None,
118
+ form_type: str | None = None,
119
+ ) -> FinancialDocument:
120
120
  """
121
121
  Upload a financial document with financial-specific fields.
122
122
 
@@ -159,11 +159,11 @@ class FinancialDocumentManager(BaseDocumentManager):
159
159
  def list_financial(
160
160
  self,
161
161
  user_id: str,
162
- document_type: Optional["DocumentType"] = None,
163
- tax_year: Optional[int] = None,
162
+ document_type: DocumentType | None = None,
163
+ tax_year: int | None = None,
164
164
  limit: int = 100,
165
165
  offset: int = 0,
166
- ) -> list["FinancialDocument"]:
166
+ ) -> list[FinancialDocument]:
167
167
  """
168
168
  List user's financial documents with filters.
169
169
 
@@ -207,9 +207,9 @@ class FinancialDocumentManager(BaseDocumentManager):
207
207
  async def extract_text(
208
208
  self,
209
209
  document_id: str,
210
- provider: Optional[str] = None,
210
+ provider: str | None = None,
211
211
  force_refresh: bool = False,
212
- ) -> "OCRResult":
212
+ ) -> OCRResult:
213
213
  """
214
214
  Extract text from document using OCR (financial extension).
215
215
 
@@ -239,7 +239,7 @@ class FinancialDocumentManager(BaseDocumentManager):
239
239
  self,
240
240
  document_id: str,
241
241
  force_refresh: bool = False,
242
- ) -> "DocumentAnalysis":
242
+ ) -> DocumentAnalysis:
243
243
  """
244
244
  Analyze document using AI (financial extension).
245
245
 
@@ -268,7 +268,7 @@ DocumentManager = FinancialDocumentManager
268
268
 
269
269
 
270
270
  def easy_documents(
271
- storage: Optional["StorageBackend"] = None,
271
+ storage: StorageBackend | None = None,
272
272
  default_ocr_provider: str = "tesseract",
273
273
  ) -> FinancialDocumentManager:
274
274
  """
@@ -31,7 +31,6 @@ from __future__ import annotations
31
31
 
32
32
  from datetime import datetime
33
33
  from enum import Enum
34
- from typing import Dict, List, Optional
35
34
 
36
35
  from pydantic import BaseModel, ConfigDict, Field
37
36
  from svc_infra.documents import Document as BaseDocument
@@ -111,8 +110,8 @@ class FinancialDocument(BaseDocument):
111
110
 
112
111
  # Financial-specific fields
113
112
  type: DocumentType = Field(..., description="Document type category (financial-specific)")
114
- tax_year: Optional[int] = Field(None, description="Tax year for tax documents (e.g., 2024)")
115
- form_type: Optional[str] = Field(None, description="Tax form type (W-2, 1099-INT, 1040, etc.)")
113
+ tax_year: int | None = Field(None, description="Tax year for tax documents (e.g., 2024)")
114
+ form_type: str | None = Field(None, description="Tax form type (W-2, 1099-INT, 1040, etc.)")
116
115
 
117
116
 
118
117
  # Backward compatibility: alias for existing code
@@ -145,7 +144,7 @@ class OCRResult(BaseModel):
145
144
  confidence: float = Field(
146
145
  ..., description="Overall OCR confidence score (0.0-1.0)", ge=0.0, le=1.0
147
146
  )
148
- fields_extracted: Dict[str, str] = Field(
147
+ fields_extracted: dict[str, str] = Field(
149
148
  default_factory=dict,
150
149
  description="Structured fields extracted from document (names, amounts, dates)",
151
150
  )
@@ -181,10 +180,10 @@ class DocumentAnalysis(BaseModel):
181
180
 
182
181
  document_id: str = Field(..., description="Document that was analyzed")
183
182
  summary: str = Field(..., description="High-level document summary")
184
- key_findings: List[str] = Field(
183
+ key_findings: list[str] = Field(
185
184
  default_factory=list, description="Important facts extracted from document"
186
185
  )
187
- recommendations: List[str] = Field(
186
+ recommendations: list[str] = Field(
188
187
  default_factory=list, description="Action items or suggestions based on document content"
189
188
  )
190
189
  analysis_date: datetime = Field(