fin-infra 0.1.62__py3-none-any.whl → 0.1.82__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 (126) hide show
  1. fin_infra/__init__.py +53 -3
  2. fin_infra/analytics/__init__.py +13 -2
  3. fin_infra/analytics/add.py +30 -32
  4. fin_infra/analytics/cash_flow.py +6 -5
  5. fin_infra/analytics/ease.py +19 -20
  6. fin_infra/analytics/portfolio.py +19 -26
  7. fin_infra/analytics/projections.py +1 -3
  8. fin_infra/analytics/rebalancing.py +2 -4
  9. fin_infra/analytics/savings.py +1 -1
  10. fin_infra/analytics/spending.py +15 -11
  11. fin_infra/banking/__init__.py +33 -31
  12. fin_infra/banking/history.py +11 -12
  13. fin_infra/banking/utils.py +116 -110
  14. fin_infra/brokerage/__init__.py +27 -27
  15. fin_infra/budgets/__init__.py +3 -3
  16. fin_infra/budgets/add.py +16 -17
  17. fin_infra/budgets/alerts.py +3 -3
  18. fin_infra/budgets/tracker.py +4 -5
  19. fin_infra/cashflows/__init__.py +8 -10
  20. fin_infra/cashflows/core.py +1 -1
  21. fin_infra/categorization/__init__.py +1 -1
  22. fin_infra/categorization/add.py +17 -19
  23. fin_infra/categorization/ease.py +3 -4
  24. fin_infra/categorization/engine.py +21 -18
  25. fin_infra/categorization/llm_layer.py +10 -10
  26. fin_infra/categorization/models.py +1 -1
  27. fin_infra/categorization/rules.py +2 -4
  28. fin_infra/categorization/taxonomy.py +2 -2
  29. fin_infra/chat/__init__.py +13 -22
  30. fin_infra/chat/planning.py +57 -1
  31. fin_infra/cli/cmds/scaffold_cmds.py +11 -12
  32. fin_infra/clients/__init__.py +23 -1
  33. fin_infra/clients/base.py +1 -1
  34. fin_infra/clients/plaid.py +2 -2
  35. fin_infra/compliance/__init__.py +7 -6
  36. fin_infra/credit/add.py +7 -7
  37. fin_infra/credit/experian/auth.py +3 -2
  38. fin_infra/credit/experian/client.py +2 -2
  39. fin_infra/credit/experian/provider.py +19 -19
  40. fin_infra/crypto/__init__.py +8 -10
  41. fin_infra/crypto/insights.py +5 -6
  42. fin_infra/documents/add.py +11 -13
  43. fin_infra/documents/analysis.py +9 -9
  44. fin_infra/documents/ease.py +18 -17
  45. fin_infra/documents/models.py +7 -7
  46. fin_infra/documents/ocr.py +8 -8
  47. fin_infra/documents/storage.py +23 -14
  48. fin_infra/exceptions.py +1 -2
  49. fin_infra/goals/__init__.py +8 -8
  50. fin_infra/goals/add.py +36 -36
  51. fin_infra/goals/funding.py +4 -6
  52. fin_infra/goals/management.py +6 -7
  53. fin_infra/goals/milestones.py +2 -3
  54. fin_infra/goals/models.py +7 -11
  55. fin_infra/insights/__init__.py +12 -10
  56. fin_infra/insights/aggregator.py +1 -1
  57. fin_infra/investments/__init__.py +14 -9
  58. fin_infra/investments/add.py +53 -73
  59. fin_infra/investments/ease.py +16 -13
  60. fin_infra/investments/models.py +135 -69
  61. fin_infra/investments/providers/base.py +9 -15
  62. fin_infra/investments/providers/plaid.py +70 -55
  63. fin_infra/investments/providers/snaptrade.py +35 -53
  64. fin_infra/markets/__init__.py +16 -11
  65. fin_infra/models/__init__.py +10 -10
  66. fin_infra/models/accounts.py +2 -1
  67. fin_infra/models/brokerage.py +2 -1
  68. fin_infra/models/candle.py +1 -0
  69. fin_infra/models/money.py +1 -0
  70. fin_infra/models/quotes.py +4 -3
  71. fin_infra/models/tax.py +2 -1
  72. fin_infra/models/transactions.py +4 -4
  73. fin_infra/net_worth/__init__.py +7 -0
  74. fin_infra/net_worth/add.py +8 -5
  75. fin_infra/net_worth/aggregator.py +9 -6
  76. fin_infra/net_worth/calculator.py +8 -6
  77. fin_infra/net_worth/ease.py +36 -15
  78. fin_infra/net_worth/insights.py +4 -5
  79. fin_infra/net_worth/models.py +237 -116
  80. fin_infra/normalization/__init__.py +17 -15
  81. fin_infra/normalization/providers/exchangerate.py +5 -5
  82. fin_infra/obs/classifier.py +3 -3
  83. fin_infra/providers/banking/plaid_client.py +23 -22
  84. fin_infra/providers/banking/teller_client.py +14 -7
  85. fin_infra/providers/base.py +131 -14
  86. fin_infra/providers/brokerage/alpaca.py +7 -7
  87. fin_infra/providers/credit/experian.py +5 -0
  88. fin_infra/providers/market/alphavantage.py +6 -11
  89. fin_infra/providers/market/ccxt_crypto.py +25 -4
  90. fin_infra/providers/market/coingecko.py +5 -6
  91. fin_infra/providers/market/yahoo.py +23 -8
  92. fin_infra/providers/tax/__init__.py +1 -1
  93. fin_infra/providers/tax/irs.py +1 -1
  94. fin_infra/providers/tax/mock.py +8 -8
  95. fin_infra/providers/tax/taxbit.py +1 -1
  96. fin_infra/recurring/__init__.py +6 -6
  97. fin_infra/recurring/add.py +24 -12
  98. fin_infra/recurring/detector.py +8 -8
  99. fin_infra/recurring/detectors_llm.py +14 -13
  100. fin_infra/recurring/ease.py +3 -5
  101. fin_infra/recurring/insights.py +20 -19
  102. fin_infra/recurring/models.py +3 -3
  103. fin_infra/recurring/normalizer.py +3 -2
  104. fin_infra/recurring/normalizers.py +11 -10
  105. fin_infra/recurring/summary.py +13 -15
  106. fin_infra/scaffold/__init__.py +1 -1
  107. fin_infra/scaffold/budgets.py +9 -9
  108. fin_infra/scaffold/goals.py +5 -5
  109. fin_infra/security/__init__.py +8 -8
  110. fin_infra/security/encryption.py +6 -6
  111. fin_infra/security/models.py +7 -7
  112. fin_infra/security/pii_filter.py +6 -6
  113. fin_infra/security/pii_patterns.py +1 -1
  114. fin_infra/security/token_store.py +3 -1
  115. fin_infra/settings.py +2 -1
  116. fin_infra/tax/__init__.py +2 -2
  117. fin_infra/tax/add.py +3 -2
  118. fin_infra/tax/tlh.py +5 -5
  119. fin_infra/utils/http.py +5 -3
  120. fin_infra/utils/retry.py +2 -1
  121. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/METADATA +14 -9
  122. fin_infra-0.1.82.dist-info/RECORD +180 -0
  123. fin_infra-0.1.62.dist-info/RECORD +0 -180
  124. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/LICENSE +0 -0
  125. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/WHEEL +0 -0
  126. {fin_infra-0.1.62.dist-info → fin_infra-0.1.82.dist-info}/entry_points.txt +0 -0
@@ -33,14 +33,12 @@ Integration with svc-infra:
33
33
 
34
34
  from __future__ import annotations
35
35
 
36
- from typing import List, Dict, Optional
37
- from datetime import datetime
38
36
  from collections import defaultdict
37
+ from datetime import datetime
39
38
 
40
- from pydantic import BaseModel, Field, ConfigDict
41
-
42
- from fin_infra.recurring.models import RecurringPattern, PatternType
39
+ from pydantic import BaseModel, ConfigDict, Field
43
40
 
41
+ from fin_infra.recurring.models import PatternType, RecurringPattern
44
42
 
45
43
  __all__ = [
46
44
  "RecurringItem",
@@ -121,16 +119,16 @@ class RecurringSummary(BaseModel):
121
119
  user_id: str = Field(..., description="User identifier")
122
120
  total_monthly_cost: float = Field(..., description="Total monthly recurring expenses")
123
121
  total_monthly_income: float = Field(0.0, description="Total monthly recurring income")
124
- subscriptions: List[RecurringItem] = Field(
122
+ subscriptions: list[RecurringItem] = Field(
125
123
  default_factory=list, description="List of recurring expense items"
126
124
  )
127
- recurring_income: List[RecurringItem] = Field(
125
+ recurring_income: list[RecurringItem] = Field(
128
126
  default_factory=list, description="List of recurring income items"
129
127
  )
130
- by_category: Dict[str, float] = Field(
128
+ by_category: dict[str, float] = Field(
131
129
  default_factory=dict, description="Monthly cost grouped by category"
132
130
  )
133
- cancellation_opportunities: List[CancellationOpportunity] = Field(
131
+ cancellation_opportunities: list[CancellationOpportunity] = Field(
134
132
  default_factory=list, description="Potential subscriptions to cancel"
135
133
  )
136
134
  generated_at: str = Field(
@@ -165,8 +163,8 @@ def _calculate_monthly_cost(amount: float, cadence: str) -> float:
165
163
 
166
164
 
167
165
  def _identify_cancellation_opportunities(
168
- subscriptions: List[RecurringItem],
169
- ) -> List[CancellationOpportunity]:
166
+ subscriptions: list[RecurringItem],
167
+ ) -> list[CancellationOpportunity]:
170
168
  """Identify potential cancellation opportunities from subscriptions.
171
169
 
172
170
  Looks for:
@@ -183,7 +181,7 @@ def _identify_cancellation_opportunities(
183
181
  opportunities = []
184
182
 
185
183
  # Group by category
186
- by_category: Dict[str, List[RecurringItem]] = defaultdict(list)
184
+ by_category: dict[str, list[RecurringItem]] = defaultdict(list)
187
185
  for sub in subscriptions:
188
186
  by_category[sub.category].append(sub)
189
187
 
@@ -253,8 +251,8 @@ def _identify_cancellation_opportunities(
253
251
 
254
252
  def get_recurring_summary(
255
253
  user_id: str,
256
- patterns: List[RecurringPattern],
257
- category_map: Optional[Dict[str, str]] = None,
254
+ patterns: list[RecurringPattern],
255
+ category_map: dict[str, str] | None = None,
258
256
  ) -> RecurringSummary:
259
257
  """Generate a comprehensive recurring transaction summary for a user.
260
258
 
@@ -283,7 +281,7 @@ def get_recurring_summary(
283
281
  """
284
282
  subscriptions = []
285
283
  recurring_income = []
286
- by_category: Dict[str, float] = defaultdict(float)
284
+ by_category: dict[str, float] = defaultdict(float)
287
285
 
288
286
  for pattern in patterns:
289
287
  # Determine amount (use fixed amount or average of range)
@@ -12,7 +12,7 @@ Typical usage:
12
12
  include_tenant=True,
13
13
  include_soft_delete=True,
14
14
  )
15
-
15
+
16
16
  result = scaffold_goals_core(
17
17
  dest_dir=Path("app/models/goals"),
18
18
  include_tenant=False,
@@ -19,10 +19,10 @@ Typical usage:
19
19
  from __future__ import annotations
20
20
 
21
21
  from pathlib import Path
22
- from typing import Any, Dict, List, Optional
22
+ from typing import Any
23
23
 
24
24
  # Use svc-infra's scaffold utilities to avoid duplication
25
- from svc_infra.utils import render_template, write, ensure_init_py
25
+ from svc_infra.utils import ensure_init_py, render_template, write
26
26
 
27
27
 
28
28
  def scaffold_budgets_core(
@@ -31,10 +31,10 @@ def scaffold_budgets_core(
31
31
  include_soft_delete: bool = False,
32
32
  with_repository: bool = True,
33
33
  overwrite: bool = False,
34
- models_filename: Optional[str] = None,
35
- schemas_filename: Optional[str] = None,
36
- repository_filename: Optional[str] = None,
37
- ) -> Dict[str, Any]:
34
+ models_filename: str | None = None,
35
+ schemas_filename: str | None = None,
36
+ repository_filename: str | None = None,
37
+ ) -> dict[str, Any]:
38
38
  """Generate budget persistence code from templates.
39
39
 
40
40
  Args:
@@ -72,7 +72,7 @@ def scaffold_budgets_core(
72
72
  subs = _generate_substitutions(include_tenant, include_soft_delete)
73
73
 
74
74
  # Track all file operations
75
- files: List[Dict[str, Any]] = []
75
+ files: list[dict[str, Any]] = []
76
76
 
77
77
  # Render and write models
78
78
  models_content = render_template("fin_infra.budgets.scaffold_templates", "models.py.tmpl", subs)
@@ -114,7 +114,7 @@ def scaffold_budgets_core(
114
114
  def _generate_substitutions(
115
115
  include_tenant: bool,
116
116
  include_soft_delete: bool,
117
- ) -> Dict[str, str]:
117
+ ) -> dict[str, str]:
118
118
  """Generate template variable substitutions for budgets.
119
119
 
120
120
  Args:
@@ -229,7 +229,7 @@ def _tenant_field_schema_read() -> str:
229
229
  def _generate_init_content(
230
230
  models_file: str,
231
231
  schemas_file: str,
232
- repo_file: Optional[str],
232
+ repo_file: str | None,
233
233
  ) -> str:
234
234
  """Generate __init__.py content with re-exports.
235
235
 
@@ -17,19 +17,19 @@ Typical usage:
17
17
  """
18
18
 
19
19
  from pathlib import Path
20
- from typing import Any, Dict
20
+ from typing import Any
21
21
 
22
22
  from svc_infra.utils import (
23
+ ensure_init_py,
23
24
  render_template,
24
25
  write,
25
- ensure_init_py,
26
26
  )
27
27
 
28
28
 
29
29
  def _generate_substitutions(
30
30
  include_tenant: bool = False,
31
31
  include_soft_delete: bool = False,
32
- ) -> Dict[str, str]:
32
+ ) -> dict[str, str]:
33
33
  """
34
34
  Generate template substitutions for goals domain.
35
35
 
@@ -49,7 +49,7 @@ def _generate_substitutions(
49
49
  Returns:
50
50
  Dict mapping variable names to their substitution values
51
51
  """
52
- subs: Dict[str, str] = {
52
+ subs: dict[str, str] = {
53
53
  "Entity": "Goal",
54
54
  "entity": "goal",
55
55
  "table_name": "goals",
@@ -173,7 +173,7 @@ def scaffold_goals_core(
173
173
  models_filename: str = "goal.py",
174
174
  schemas_filename: str = "goal_schemas.py",
175
175
  repository_filename: str = "goal_repository.py",
176
- ) -> Dict[str, Any]:
176
+ ) -> dict[str, Any]:
177
177
  """
178
178
  Scaffold goals domain files: models, schemas, repository (optional), and __init__.py.
179
179
 
@@ -8,25 +8,25 @@ Provides:
8
8
  """
9
9
 
10
10
  from .add import add_financial_security, generate_encryption_key
11
- from .audit import log_pii_access, get_audit_logs, clear_audit_logs
11
+ from .audit import clear_audit_logs, get_audit_logs, log_pii_access
12
12
  from .encryption import ProviderTokenEncryption
13
- from .models import ProviderTokenMetadata, PIIAccessLog
13
+ from .models import PIIAccessLog, ProviderTokenMetadata
14
14
  from .pii_filter import FinancialPIIFilter
15
15
  from .pii_patterns import (
16
- SSN_PATTERN,
17
16
  ACCOUNT_PATTERN,
18
- ROUTING_PATTERN,
19
17
  CARD_PATTERN,
20
18
  CVV_PATTERN,
21
19
  EIN_PATTERN,
22
- luhn_checksum,
20
+ ROUTING_PATTERN,
21
+ SSN_PATTERN,
23
22
  is_valid_routing_number,
23
+ luhn_checksum,
24
24
  )
25
25
  from .token_store import (
26
- store_provider_token,
27
- get_provider_token,
28
- delete_provider_token,
29
26
  ProviderToken,
27
+ delete_provider_token,
28
+ get_provider_token,
29
+ store_provider_token,
30
30
  )
31
31
 
32
32
  __all__ = [
@@ -7,7 +7,7 @@ Encrypt/decrypt financial provider API tokens at rest.
7
7
  import base64
8
8
  import json
9
9
  import os
10
- from typing import Any, Dict, Optional
10
+ from typing import Any, cast
11
11
 
12
12
  from cryptography.fernet import Fernet, InvalidToken
13
13
 
@@ -37,7 +37,7 @@ class ProviderTokenEncryption:
37
37
  >>> token = encryption.decrypt(encrypted, context={"user_id": "user123", "provider": "plaid"})
38
38
  """
39
39
 
40
- def __init__(self, key: Optional[bytes] = None):
40
+ def __init__(self, key: bytes | None = None):
41
41
  """
42
42
  Initialize token encryption.
43
43
 
@@ -64,7 +64,7 @@ class ProviderTokenEncryption:
64
64
  raise ValueError(f"Invalid encryption key: {e}") from e
65
65
 
66
66
  def encrypt(
67
- self, token: str, context: Optional[Dict[str, Any]] = None, key_id: Optional[str] = None
67
+ self, token: str, context: dict[str, Any] | None = None, key_id: str | None = None
68
68
  ) -> str:
69
69
  """
70
70
  Encrypt provider token with optional context.
@@ -104,7 +104,7 @@ class ProviderTokenEncryption:
104
104
  def decrypt(
105
105
  self,
106
106
  encrypted_token: str,
107
- context: Optional[Dict[str, Any]] = None,
107
+ context: dict[str, Any] | None = None,
108
108
  verify_context: bool = True,
109
109
  ) -> str:
110
110
  """
@@ -144,7 +144,7 @@ class ProviderTokenEncryption:
144
144
  "Token may have been tampered with or used for wrong user/provider."
145
145
  )
146
146
 
147
- return data["token"]
147
+ return cast("str", data["token"])
148
148
 
149
149
  except InvalidToken as e:
150
150
  raise ValueError(
@@ -154,7 +154,7 @@ class ProviderTokenEncryption:
154
154
  raise ValueError(f"Decryption failed: {e}") from e
155
155
 
156
156
  def rotate_key(
157
- self, encrypted_token: str, new_key: bytes, context: Optional[Dict[str, Any]] = None
157
+ self, encrypted_token: str, new_key: bytes, context: dict[str, Any] | None = None
158
158
  ) -> str:
159
159
  """
160
160
  Re-encrypt token with new key (for key rotation).
@@ -5,7 +5,7 @@ Pydantic models for security-related operations.
5
5
  """
6
6
 
7
7
  from datetime import datetime
8
- from typing import Optional
8
+
9
9
  from pydantic import BaseModel, Field
10
10
 
11
11
 
@@ -15,12 +15,12 @@ class ProviderTokenMetadata(BaseModel):
15
15
  user_id: str = Field(..., description="User ID who owns the token")
16
16
  provider: str = Field(..., description="Provider name (plaid, alpaca, alphavantage, etc.)")
17
17
  encrypted_token: str = Field(..., description="Encrypted token (base64-encoded)")
18
- key_id: Optional[str] = Field(None, description="Key ID for key rotation")
18
+ key_id: str | None = Field(None, description="Key ID for key rotation")
19
19
  created_at: datetime = Field(
20
20
  default_factory=datetime.utcnow, description="Token creation timestamp"
21
21
  )
22
- expires_at: Optional[datetime] = Field(None, description="Token expiration timestamp")
23
- last_used_at: Optional[datetime] = Field(None, description="Last time token was used")
22
+ expires_at: datetime | None = Field(None, description="Token expiration timestamp")
23
+ last_used_at: datetime | None = Field(None, description="Last time token was used")
24
24
 
25
25
 
26
26
  class PIIAccessLog(BaseModel):
@@ -30,8 +30,8 @@ class PIIAccessLog(BaseModel):
30
30
  pii_type: str = Field(..., description="Type of PII (ssn, account, card, etc.)")
31
31
  action: str = Field(..., description="Action performed (read, write, delete)")
32
32
  resource: str = Field(..., description="Resource accessed (e.g., user:123, account:456)")
33
- ip_address: Optional[str] = Field(None, description="IP address of requester")
34
- user_agent: Optional[str] = Field(None, description="User agent string")
33
+ ip_address: str | None = Field(None, description="IP address of requester")
34
+ user_agent: str | None = Field(None, description="User agent string")
35
35
  timestamp: datetime = Field(default_factory=datetime.utcnow, description="Access timestamp")
36
36
  success: bool = Field(True, description="Whether access was successful")
37
- error_message: Optional[str] = Field(None, description="Error message if failed")
37
+ error_message: str | None = Field(None, description="Error message if failed")
@@ -9,21 +9,21 @@ import re
9
9
  from typing import Any
10
10
 
11
11
  from .pii_patterns import (
12
- ACCOUNT_PATTERN,
13
12
  ACCOUNT_CONTEXT,
13
+ ACCOUNT_PATTERN,
14
14
  CARD_PATTERN,
15
- CVV_PATTERN,
16
15
  CVV_CONTEXT,
16
+ CVV_PATTERN,
17
17
  EIN_PATTERN,
18
18
  EMAIL_PATTERN,
19
19
  PHONE_PATTERN,
20
- ROUTING_PATTERN,
21
20
  ROUTING_CONTEXT,
22
- SSN_PATTERN,
23
- SSN_NO_DASH,
21
+ ROUTING_PATTERN,
24
22
  SSN_CONTEXT,
25
- luhn_checksum,
23
+ SSN_NO_DASH,
24
+ SSN_PATTERN,
26
25
  is_valid_routing_number,
26
+ luhn_checksum,
27
27
  )
28
28
 
29
29
 
@@ -67,7 +67,7 @@ def luhn_checksum(card_number: str) -> bool:
67
67
  True if valid, False otherwise
68
68
  """
69
69
 
70
- def digits_of(n):
70
+ def digits_of(n: int | str) -> list[int]:
71
71
  return [int(d) for d in str(n)]
72
72
 
73
73
  digits = digits_of(card_number)
@@ -162,7 +162,9 @@ async def get_provider_token(
162
162
 
163
163
  # Decrypt token
164
164
  context = {"user_id": user_id, "provider": provider}
165
- token = encryption.decrypt(token_obj.encrypted_token, context=context)
165
+ # Cast to str since SQLAlchemy Column[str] needs explicit conversion for type checker
166
+ encrypted_token_str: str = str(token_obj.encrypted_token)
167
+ token = encryption.decrypt(encrypted_token_str, context=context)
166
168
 
167
169
  # Update last_used_at
168
170
  update_stmt = (
fin_infra/settings.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from functools import lru_cache
4
+
4
5
  from pydantic import Field
5
6
  from pydantic_settings import BaseSettings, SettingsConfigDict
6
7
 
@@ -33,5 +34,5 @@ class Settings(BaseSettings):
33
34
 
34
35
 
35
36
  @lru_cache
36
- def get_settings() -> "Settings":
37
+ def get_settings() -> Settings:
37
38
  return Settings()
fin_infra/tax/__init__.py CHANGED
@@ -33,7 +33,7 @@ Example:
33
33
  import os
34
34
 
35
35
  from fin_infra.providers.base import TaxProvider
36
- from fin_infra.providers.tax import MockTaxProvider, IRSProvider, TaxBitProvider
36
+ from fin_infra.providers.tax import IRSProvider, MockTaxProvider, TaxBitProvider
37
37
  from fin_infra.tax.add import add_tax_data
38
38
  from fin_infra.tax.tlh import (
39
39
  TLHOpportunity,
@@ -144,7 +144,7 @@ def easy_tax(provider: str | TaxProvider = "mock", **config) -> TaxProvider:
144
144
 
145
145
  else:
146
146
  raise ValueError(
147
- f"Unknown tax provider: {provider}. " f"Supported providers: 'mock', 'irs', 'taxbit'"
147
+ f"Unknown tax provider: {provider}. Supported providers: 'mock', 'irs', 'taxbit'"
148
148
  )
149
149
 
150
150
 
fin_infra/tax/add.py CHANGED
@@ -17,7 +17,8 @@ Example:
17
17
  """
18
18
 
19
19
  from decimal import Decimal
20
- from fastapi import FastAPI, Query, Body
20
+
21
+ from fastapi import Body, FastAPI, Query
21
22
  from pydantic import BaseModel
22
23
 
23
24
  from fin_infra.providers.base import TaxProvider
@@ -89,8 +90,8 @@ def add_tax_data(
89
90
  >>> # POST /tax/tax-liability
90
91
  """
91
92
  # Use svc-infra user_router for authentication (tax data is user-specific and sensitive)
92
- from svc_infra.api.fastapi.dual.protected import user_router
93
93
  from svc_infra.api.fastapi.docs.scoped import add_prefixed_docs
94
+ from svc_infra.api.fastapi.dual.protected import user_router
94
95
 
95
96
  # Initialize provider
96
97
  if provider is None:
fin_infra/tax/tlh.py CHANGED
@@ -55,7 +55,7 @@ Cost Considerations:
55
55
 
56
56
  from __future__ import annotations
57
57
 
58
- from datetime import datetime, timezone
58
+ from datetime import UTC, datetime
59
59
  from decimal import Decimal
60
60
  from typing import TYPE_CHECKING
61
61
 
@@ -412,9 +412,9 @@ def _assess_wash_sale_risk(symbol: str, last_purchase_date: datetime | None) ->
412
412
  return "none"
413
413
 
414
414
  # Calculate days since last purchase
415
- now = datetime.now(timezone.utc)
415
+ now = datetime.now(UTC)
416
416
  if last_purchase_date.tzinfo is None:
417
- last_purchase_date = last_purchase_date.replace(tzinfo=timezone.utc)
417
+ last_purchase_date = last_purchase_date.replace(tzinfo=UTC)
418
418
 
419
419
  days_since = (now - last_purchase_date).days
420
420
 
@@ -506,8 +506,8 @@ def _generate_tlh_recommendations(
506
506
  recommendations = []
507
507
 
508
508
  # Timing recommendations
509
- now = datetime.now(timezone.utc)
510
- days_until_year_end = (datetime(now.year, 12, 31, tzinfo=timezone.utc) - now).days
509
+ now = datetime.now(UTC)
510
+ days_until_year_end = (datetime(now.year, 12, 31, tzinfo=UTC) - now).days
511
511
 
512
512
  if days_until_year_end < 30:
513
513
  recommendations.append(
fin_infra/utils/http.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import Any, cast
4
+
3
5
  import httpx
4
- from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
6
+ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
5
7
 
6
8
  _DEFAULT_TIMEOUT = httpx.Timeout(20.0)
7
9
 
@@ -12,8 +14,8 @@ _DEFAULT_TIMEOUT = httpx.Timeout(20.0)
12
14
  retry=retry_if_exception_type(httpx.HTTPError),
13
15
  reraise=True,
14
16
  )
15
- async def aget_json(url: str, **kwargs) -> dict:
17
+ async def aget_json(url: str, **kwargs) -> dict[Any, Any]:
16
18
  async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
17
19
  r = await client.get(url, **kwargs)
18
20
  r.raise_for_status()
19
- return r.json()
21
+ return cast("dict[Any, Any]", r.json())
fin_infra/utils/retry.py CHANGED
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import random
5
- from typing import Awaitable, Callable, Iterable, TypeVar
5
+ from collections.abc import Awaitable, Callable, Iterable
6
+ from typing import TypeVar
6
7
 
7
8
  from fin_infra.exceptions import RetryError
8
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fin-infra
3
- Version: 0.1.62
3
+ Version: 0.1.82
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
@@ -19,25 +19,30 @@ Classifier: Programming Language :: Python :: 3 :: Only
19
19
  Classifier: Topic :: Office/Business :: Financial
20
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Classifier: Typing :: Typed
22
+ Provides-Extra: all
23
+ Provides-Extra: banking
24
+ Provides-Extra: crypto
25
+ Provides-Extra: markets
26
+ Provides-Extra: plaid
27
+ Provides-Extra: yahoo
22
28
  Requires-Dist: ai-infra (>=0.1.142)
23
29
  Requires-Dist: cashews[redis] (>=7.0)
24
- Requires-Dist: ccxt (>=4.0.0)
30
+ Requires-Dist: ccxt (>=4.0.0) ; extra == "markets" or extra == "crypto" or extra == "all"
25
31
  Requires-Dist: httpx (>=0.25.0)
26
32
  Requires-Dist: loguru (>=0.7.0)
27
33
  Requires-Dist: numpy (>=1.24.0)
28
34
  Requires-Dist: numpy-financial (>=1.0.0)
29
- Requires-Dist: plaid-python (>=25.0.0)
35
+ Requires-Dist: plaid-python (>=25.0.0) ; extra == "plaid" or extra == "banking" or extra == "all"
30
36
  Requires-Dist: pydantic (>=2.0)
31
37
  Requires-Dist: pydantic-settings (>=2.0)
32
38
  Requires-Dist: python-dotenv (>=1.0.0)
33
- Requires-Dist: svc-infra (>=0.1.0)
34
39
  Requires-Dist: tenacity (>=8.0.0)
35
40
  Requires-Dist: typing-extensions (>=4.0)
36
- Requires-Dist: yahooquery (>=2.3.0)
37
- Project-URL: Documentation, https://github.com/your-org/fin-infra#readme
38
- Project-URL: Homepage, https://github.com/your-org/fin-infra
39
- Project-URL: Issues, https://github.com/your-org/fin-infra/issues
40
- Project-URL: Repository, https://github.com/your-org/fin-infra
41
+ Requires-Dist: yahooquery (>=2.3.0) ; extra == "markets" or extra == "yahoo" or extra == "all"
42
+ Project-URL: Documentation, https://nfrax.com/fin-infra
43
+ Project-URL: Homepage, https://github.com/nfraxlab/fin-infra
44
+ Project-URL: Issues, https://github.com/nfraxlab/fin-infra/issues
45
+ Project-URL: Repository, https://github.com/nfraxlab/fin-infra
41
46
  Description-Content-Type: text/markdown
42
47
 
43
48
  <div align="center">