quantlib-pro 1.0.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 (170) hide show
  1. quantlib_api/__init__.py +50 -0
  2. quantlib_api/_http.py +243 -0
  3. quantlib_api/auth.py +164 -0
  4. quantlib_api/client.py +349 -0
  5. quantlib_api/exceptions.py +69 -0
  6. quantlib_api/resources/__init__.py +43 -0
  7. quantlib_api/resources/analytics.py +84 -0
  8. quantlib_api/resources/backtesting.py +76 -0
  9. quantlib_api/resources/base.py +15 -0
  10. quantlib_api/resources/compliance.py +95 -0
  11. quantlib_api/resources/data.py +66 -0
  12. quantlib_api/resources/execution.py +113 -0
  13. quantlib_api/resources/health.py +34 -0
  14. quantlib_api/resources/liquidity.py +83 -0
  15. quantlib_api/resources/macro.py +63 -0
  16. quantlib_api/resources/market_analysis.py +83 -0
  17. quantlib_api/resources/options.py +123 -0
  18. quantlib_api/resources/portfolio.py +131 -0
  19. quantlib_api/resources/regime.py +68 -0
  20. quantlib_api/resources/risk.py +105 -0
  21. quantlib_api/resources/signals.py +77 -0
  22. quantlib_api/resources/systemic_risk.py +86 -0
  23. quantlib_api/resources/uat.py +92 -0
  24. quantlib_api/resources/volatility.py +84 -0
  25. quantlib_cli/__init__.py +20 -0
  26. quantlib_cli/_endpoints.py +101 -0
  27. quantlib_cli/auth_store.py +105 -0
  28. quantlib_cli/cli.py +617 -0
  29. quantlib_cli/formatters.py +120 -0
  30. quantlib_pro/__init__.py +208 -0
  31. quantlib_pro/analytics/__init__.py +30 -0
  32. quantlib_pro/analytics/correlation_analysis.py +459 -0
  33. quantlib_pro/analytics/manager.py +54 -0
  34. quantlib_pro/api/__init__.py +154 -0
  35. quantlib_pro/api/auth.py +221 -0
  36. quantlib_pro/api/dependencies.py +612 -0
  37. quantlib_pro/api/health.py +242 -0
  38. quantlib_pro/api/models.py +452 -0
  39. quantlib_pro/api/optimizations.py +381 -0
  40. quantlib_pro/api/routers.py +1200 -0
  41. quantlib_pro/api/routers_analytics.py +451 -0
  42. quantlib_pro/api/routers_backtesting.py +402 -0
  43. quantlib_pro/api/routers_compliance.py +464 -0
  44. quantlib_pro/api/routers_data.py +465 -0
  45. quantlib_pro/api/routers_execution.py +471 -0
  46. quantlib_pro/api/routers_liquidity.py +410 -0
  47. quantlib_pro/api/routers_macro.py +323 -0
  48. quantlib_pro/api/routers_market_analysis.py +462 -0
  49. quantlib_pro/api/routers_realdata.py +396 -0
  50. quantlib_pro/api/routers_signals.py +416 -0
  51. quantlib_pro/api/routers_systemic_risk.py +421 -0
  52. quantlib_pro/api/routers_uat.py +403 -0
  53. quantlib_pro/audit/__init__.py +15 -0
  54. quantlib_pro/audit/calculation_log.py +269 -0
  55. quantlib_pro/cli.py +313 -0
  56. quantlib_pro/compliance/__init__.py +54 -0
  57. quantlib_pro/compliance/audit_trail.py +520 -0
  58. quantlib_pro/compliance/gdpr.py +478 -0
  59. quantlib_pro/compliance/reporting.py +404 -0
  60. quantlib_pro/data/__init__.py +113 -0
  61. quantlib_pro/data/alpha_vantage_client.py +349 -0
  62. quantlib_pro/data/cache.py +159 -0
  63. quantlib_pro/data/cache_manager.py +378 -0
  64. quantlib_pro/data/data_router.py +306 -0
  65. quantlib_pro/data/database.py +123 -0
  66. quantlib_pro/data/fetcher.py +199 -0
  67. quantlib_pro/data/fred_client.py +324 -0
  68. quantlib_pro/data/fred_provider.py +156 -0
  69. quantlib_pro/data/manager.py +250 -0
  70. quantlib_pro/data/market_data.py +237 -0
  71. quantlib_pro/data/models/__init__.py +22 -0
  72. quantlib_pro/data/models/audit.py +27 -0
  73. quantlib_pro/data/models/backtest.py +42 -0
  74. quantlib_pro/data/models/base.py +16 -0
  75. quantlib_pro/data/models/celery_task.py +36 -0
  76. quantlib_pro/data/models/portfolio.py +51 -0
  77. quantlib_pro/data/models/timeseries.py +54 -0
  78. quantlib_pro/data/models/user.py +26 -0
  79. quantlib_pro/data/providers/__init__.py +24 -0
  80. quantlib_pro/data/providers/alpha_vantage.py +338 -0
  81. quantlib_pro/data/providers/capital_iq.py +359 -0
  82. quantlib_pro/data/providers/factset.py +482 -0
  83. quantlib_pro/data/providers/multi_provider.py +210 -0
  84. quantlib_pro/data/providers_legacy.py +509 -0
  85. quantlib_pro/data/quality.py +205 -0
  86. quantlib_pro/data/yahoo_client.py +321 -0
  87. quantlib_pro/execution/__init__.py +84 -0
  88. quantlib_pro/execution/backtesting.py +551 -0
  89. quantlib_pro/execution/manager.py +55 -0
  90. quantlib_pro/execution/market_impact.py +325 -0
  91. quantlib_pro/execution/order_book.py +309 -0
  92. quantlib_pro/execution/strategies.py +351 -0
  93. quantlib_pro/governance/__init__.py +30 -0
  94. quantlib_pro/governance/policies.py +544 -0
  95. quantlib_pro/macro/__init__.py +97 -0
  96. quantlib_pro/macro/correlation.py +374 -0
  97. quantlib_pro/macro/economic.py +438 -0
  98. quantlib_pro/macro/macro_regime.py +167 -0
  99. quantlib_pro/macro/manager.py +44 -0
  100. quantlib_pro/macro/sentiment.py +443 -0
  101. quantlib_pro/market_microstructure/__init__.py +13 -0
  102. quantlib_pro/market_microstructure/calibrated_orderbook.py +289 -0
  103. quantlib_pro/market_regime/__init__.py +54 -0
  104. quantlib_pro/market_regime/hmm_detector.py +259 -0
  105. quantlib_pro/market_regime/manager.py +45 -0
  106. quantlib_pro/market_regime/trend_regime.py +296 -0
  107. quantlib_pro/market_regime/volatility_regime.py +284 -0
  108. quantlib_pro/monitoring/health_check.py +361 -0
  109. quantlib_pro/observability/__init__.py +144 -0
  110. quantlib_pro/observability/health.py +350 -0
  111. quantlib_pro/observability/metrics.py +342 -0
  112. quantlib_pro/observability/monitoring.py +475 -0
  113. quantlib_pro/observability/performance.py +369 -0
  114. quantlib_pro/observability/profiler.py +444 -0
  115. quantlib_pro/options/__init__.py +89 -0
  116. quantlib_pro/options/bachelier.py +625 -0
  117. quantlib_pro/options/black_scholes.py +551 -0
  118. quantlib_pro/options/greeks.py +367 -0
  119. quantlib_pro/options/manager.py +313 -0
  120. quantlib_pro/options/monte_carlo.py +377 -0
  121. quantlib_pro/portfolio/__init__.py +52 -0
  122. quantlib_pro/portfolio/black_litterman.py +246 -0
  123. quantlib_pro/portfolio/manager.py +186 -0
  124. quantlib_pro/portfolio/optimization.py +337 -0
  125. quantlib_pro/portfolio/optimizer.py +165 -0
  126. quantlib_pro/portfolio/risk_parity.py +236 -0
  127. quantlib_pro/resilience/__init__.py +17 -0
  128. quantlib_pro/resilience/circuit_breaker.py +204 -0
  129. quantlib_pro/risk/__init__.py +80 -0
  130. quantlib_pro/risk/advanced_analytics.py +504 -0
  131. quantlib_pro/risk/limits.py +206 -0
  132. quantlib_pro/risk/manager.py +220 -0
  133. quantlib_pro/risk/stress.py +21 -0
  134. quantlib_pro/risk/stress_testing.py +356 -0
  135. quantlib_pro/risk/var.py +358 -0
  136. quantlib_pro/sdk.py +294 -0
  137. quantlib_pro/security/__init__.py +40 -0
  138. quantlib_pro/security/authentication.py +203 -0
  139. quantlib_pro/security/encryption.py +118 -0
  140. quantlib_pro/security/rate_limiting.py +226 -0
  141. quantlib_pro/testing/__init__.py +64 -0
  142. quantlib_pro/testing/chaos.py +553 -0
  143. quantlib_pro/testing/load_testing.py +482 -0
  144. quantlib_pro/testing/model_validation.py +485 -0
  145. quantlib_pro/testing/reporting.py +554 -0
  146. quantlib_pro/uat/__init__.py +69 -0
  147. quantlib_pro/uat/bug_tracker.py +397 -0
  148. quantlib_pro/uat/feedback.py +514 -0
  149. quantlib_pro/uat/performance_validation.py +456 -0
  150. quantlib_pro/uat/scenarios.py +632 -0
  151. quantlib_pro/ui/__init__.py +9 -0
  152. quantlib_pro/ui/caching.py +419 -0
  153. quantlib_pro/ui/components.py +436 -0
  154. quantlib_pro/utils/__init__.py +41 -0
  155. quantlib_pro/utils/logging.py +72 -0
  156. quantlib_pro/utils/types.py +108 -0
  157. quantlib_pro/utils/usability.py +422 -0
  158. quantlib_pro/utils/validation.py +89 -0
  159. quantlib_pro/validation/__init__.py +17 -0
  160. quantlib_pro/validation/model_validation.py +294 -0
  161. quantlib_pro/volatility/__init__.py +48 -0
  162. quantlib_pro/volatility/manager.py +39 -0
  163. quantlib_pro/volatility/smile_models.py +348 -0
  164. quantlib_pro/volatility/surface.py +337 -0
  165. quantlib_pro-1.0.0.dist-info/METADATA +659 -0
  166. quantlib_pro-1.0.0.dist-info/RECORD +170 -0
  167. quantlib_pro-1.0.0.dist-info/WHEEL +5 -0
  168. quantlib_pro-1.0.0.dist-info/entry_points.txt +2 -0
  169. quantlib_pro-1.0.0.dist-info/licenses/LICENSE +1 -0
  170. quantlib_pro-1.0.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,50 @@
1
+ """
2
+ QuantLib Pro Python SDK
3
+ ~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ A Python client library for the QuantLib Pro quantitative finance API.
6
+
7
+ Usage::
8
+
9
+ from quantlib_api import QuantLibClient
10
+
11
+ client = QuantLibClient(
12
+ base_url="http://localhost:8000",
13
+ username="demo",
14
+ password="demo123",
15
+ auto_login=True
16
+ )
17
+
18
+ result = client.portfolio.optimize(
19
+ tickers=["AAPL", "GOOGL", "MSFT"],
20
+ budget=100_000,
21
+ optimization_target="sharpe"
22
+ )
23
+ print(result)
24
+
25
+ :license: MIT
26
+ """
27
+
28
+ from quantlib_api.client import QuantLibClient
29
+ from quantlib_api.exceptions import (
30
+ QuantLibError,
31
+ QuantLibAPIError,
32
+ QuantLibAuthError,
33
+ QuantLibNotFoundError,
34
+ QuantLibRateLimitError,
35
+ QuantLibNetworkError,
36
+ QuantLibValidationError,
37
+ )
38
+
39
+ __version__ = "1.0.0"
40
+ __author__ = "tubakhxn"
41
+ __all__ = [
42
+ "QuantLibClient",
43
+ "QuantLibError",
44
+ "QuantLibAPIError",
45
+ "QuantLibAuthError",
46
+ "QuantLibNotFoundError",
47
+ "QuantLibRateLimitError",
48
+ "QuantLibNetworkError",
49
+ "QuantLibValidationError",
50
+ ]
quantlib_api/_http.py ADDED
@@ -0,0 +1,243 @@
1
+ """
2
+ QuantLib Pro SDK — HTTP Session Wrapper
3
+
4
+ Handles:
5
+ - httpx session with connection pooling
6
+ - Automatic retry with exponential backoff (3 attempts)
7
+ - JWT Bearer token injection
8
+ - Timeout configuration (connect: 5s, read: 30s)
9
+ - Verbose mode for debugging (masks auth headers)
10
+ - Async support via httpx.AsyncClient
11
+ """
12
+
13
+ import logging
14
+ import time
15
+ from typing import Any
16
+
17
+ import httpx
18
+
19
+ from quantlib_api.exceptions import (
20
+ QuantLibAPIError,
21
+ QuantLibAuthError,
22
+ QuantLibNetworkError,
23
+ QuantLibNotFoundError,
24
+ QuantLibRateLimitError,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Default timeouts
30
+ CONNECT_TIMEOUT = 5.0 # seconds
31
+ READ_TIMEOUT = 30.0 # seconds
32
+ MAX_RETRIES = 3
33
+ BACKOFF_BASE = 0.5 # seconds
34
+
35
+
36
+ class HTTPSession:
37
+ """
38
+ Synchronous HTTP session wrapping httpx.Client with retry logic.
39
+
40
+ Automatically injects Authorization header and handles
41
+ 4xx/5xx responses by raising typed exceptions.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ base_url: str,
47
+ *,
48
+ timeout: float = READ_TIMEOUT,
49
+ max_retries: int = MAX_RETRIES,
50
+ verbose: bool = False,
51
+ ):
52
+ self.base_url = base_url.rstrip("/")
53
+ self.timeout = timeout
54
+ self.max_retries = max_retries
55
+ self.verbose = verbose
56
+ self._token: str | None = None
57
+ self._api_key: str | None = None
58
+ self._client = httpx.Client(
59
+ base_url=self.base_url,
60
+ timeout=httpx.Timeout(connect=CONNECT_TIMEOUT, read=self.timeout, write=10.0, pool=5.0),
61
+ follow_redirects=True,
62
+ )
63
+
64
+ def set_token(self, token: str):
65
+ """Update the Bearer token used for all requests."""
66
+ self._token = token
67
+
68
+ def set_api_key(self, api_key: str):
69
+ """Update the API key used for all requests."""
70
+ self._api_key = api_key
71
+
72
+ def _headers(self) -> dict:
73
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
74
+ if self._token:
75
+ headers["Authorization"] = f"Bearer {self._token}"
76
+ elif self._api_key:
77
+ headers["X-API-Key"] = self._api_key
78
+ return headers
79
+
80
+ def _log_request(self, method: str, url: str, body: Any = None):
81
+ if self.verbose:
82
+ safe_headers = self._headers()
83
+ if "Authorization" in safe_headers:
84
+ safe_headers["Authorization"] = "Bearer ***"
85
+ logger.debug(f"→ {method} {url} headers={safe_headers} body={body}")
86
+
87
+ def _handle_response(self, response: httpx.Response) -> dict:
88
+ status = response.status_code
89
+ if self.verbose:
90
+ logger.debug(f"← {status} ({len(response.content)} bytes)")
91
+
92
+ if status == 200 or status == 201:
93
+ try:
94
+ return response.json()
95
+ except Exception:
96
+ return {"raw": response.text}
97
+
98
+ # Error handling
99
+ try:
100
+ detail = response.json().get("detail", response.text)
101
+ except Exception:
102
+ detail = response.text
103
+
104
+ if status in (401, 403):
105
+ raise QuantLibAuthError(f"HTTP {status}: {detail}")
106
+ if status == 404:
107
+ raise QuantLibNotFoundError(detail)
108
+ if status == 429:
109
+ retry_after = int(response.headers.get("Retry-After", 60))
110
+ raise QuantLibRateLimitError(retry_after)
111
+ raise QuantLibAPIError(f"HTTP {status}: {detail}", status_code=status, response={"detail": detail})
112
+
113
+ def _request_with_retry(self, method: str, path: str, **kwargs) -> dict:
114
+ url = path if path.startswith("http") else self.base_url + path
115
+ self._log_request(method, url, kwargs.get("json"))
116
+
117
+ last_exc = None
118
+ for attempt in range(self.max_retries):
119
+ try:
120
+ response = self._client.request(method, url, headers=self._headers(), **kwargs)
121
+ return self._handle_response(response)
122
+ except (QuantLibAuthError, QuantLibNotFoundError):
123
+ raise # Don't retry auth/404 errors
124
+ except QuantLibRateLimitError as e:
125
+ wait = e.retry_after
126
+ logger.warning(f"Rate limited. Waiting {wait}s before retry {attempt + 1}/{self.max_retries}")
127
+ time.sleep(min(wait, 60))
128
+ last_exc = e
129
+ except QuantLibAPIError as e:
130
+ if e.status_code < 500:
131
+ raise # Don't retry 4xx
132
+ wait = BACKOFF_BASE * (2 ** attempt)
133
+ logger.warning(f"Server error {e.status_code}. Retrying in {wait:.1f}s (attempt {attempt + 1}/{self.max_retries})")
134
+ time.sleep(wait)
135
+ last_exc = e
136
+ except httpx.TimeoutException as e:
137
+ wait = BACKOFF_BASE * (2 ** attempt)
138
+ logger.warning(f"Timeout. Retrying in {wait:.1f}s (attempt {attempt + 1}/{self.max_retries})")
139
+ time.sleep(wait)
140
+ last_exc = QuantLibNetworkError(f"Request timed out after {self.timeout}s")
141
+ except httpx.ConnectError as e:
142
+ raise QuantLibNetworkError(f"Cannot connect to {self.base_url}. Is the server running?") from e
143
+ except httpx.RequestError as e:
144
+ raise QuantLibNetworkError(str(e)) from e
145
+
146
+ raise last_exc or QuantLibNetworkError("Max retries exceeded")
147
+
148
+ def get(self, path: str, params: dict = None) -> dict:
149
+ return self._request_with_retry("GET", path, params=params)
150
+
151
+ def post(self, path: str, json: dict = None) -> dict:
152
+ return self._request_with_retry("POST", path, json=json or {})
153
+
154
+ def put(self, path: str, json: dict = None) -> dict:
155
+ return self._request_with_retry("PUT", path, json=json or {})
156
+
157
+ def delete(self, path: str) -> dict:
158
+ return self._request_with_retry("DELETE", path)
159
+
160
+ def close(self):
161
+ self._client.close()
162
+
163
+ def __enter__(self):
164
+ return self
165
+
166
+ def __exit__(self, *args):
167
+ self.close()
168
+
169
+
170
+ class AsyncHTTPSession:
171
+ """
172
+ Async HTTP session (httpx.AsyncClient) for use with asyncio.
173
+
174
+ Usage::
175
+
176
+ async with AsyncHTTPSession("http://localhost:8000") as session:
177
+ session.set_token(token)
178
+ result = await session.get("/api/v1/portfolio/performance")
179
+ """
180
+
181
+ def __init__(self, base_url: str, *, timeout: float = READ_TIMEOUT, verbose: bool = False):
182
+ self.base_url = base_url.rstrip("/")
183
+ self.timeout = timeout
184
+ self.verbose = verbose
185
+ self._token: str | None = None
186
+ self._client = httpx.AsyncClient(
187
+ base_url=self.base_url,
188
+ timeout=httpx.Timeout(connect=CONNECT_TIMEOUT, read=timeout, write=10.0, pool=5.0),
189
+ follow_redirects=True,
190
+ )
191
+
192
+ def set_token(self, token: str):
193
+ self._token = token
194
+
195
+ def _headers(self) -> dict:
196
+ h = {"Content-Type": "application/json", "Accept": "application/json"}
197
+ if self._token:
198
+ h["Authorization"] = f"Bearer {self._token}"
199
+ return h
200
+
201
+ async def _handle_response_async(self, response: httpx.Response) -> dict:
202
+ status = response.status_code
203
+ if status in (200, 201):
204
+ try:
205
+ return response.json()
206
+ except Exception:
207
+ return {"raw": response.text}
208
+ try:
209
+ detail = response.json().get("detail", response.text)
210
+ except Exception:
211
+ detail = response.text
212
+ if status in (401, 403):
213
+ raise QuantLibAuthError(f"HTTP {status}: {detail}")
214
+ if status == 404:
215
+ raise QuantLibNotFoundError(detail)
216
+ if status == 429:
217
+ raise QuantLibRateLimitError(int(response.headers.get("Retry-After", 60)))
218
+ raise QuantLibAPIError(f"HTTP {status}: {detail}", status_code=status)
219
+
220
+ async def get(self, path: str, params: dict = None) -> dict:
221
+ url = self.base_url + path
222
+ try:
223
+ r = await self._client.get(url, headers=self._headers(), params=params)
224
+ return await self._handle_response_async(r)
225
+ except httpx.ConnectError as e:
226
+ raise QuantLibNetworkError(str(e)) from e
227
+
228
+ async def post(self, path: str, json: dict = None) -> dict:
229
+ url = self.base_url + path
230
+ try:
231
+ r = await self._client.post(url, headers=self._headers(), json=json or {})
232
+ return await self._handle_response_async(r)
233
+ except httpx.ConnectError as e:
234
+ raise QuantLibNetworkError(str(e)) from e
235
+
236
+ async def close(self):
237
+ await self._client.aclose()
238
+
239
+ async def __aenter__(self):
240
+ return self
241
+
242
+ async def __aexit__(self, *args):
243
+ await self.close()
quantlib_api/auth.py ADDED
@@ -0,0 +1,164 @@
1
+ """
2
+ QuantLib Pro SDK — JWT Authentication Manager
3
+
4
+ Handles:
5
+ - Login via POST /auth/login
6
+ - Token storage in memory (never to disk)
7
+ - Auto-refresh on 401 responses
8
+ - Token expiry detection via JWT decode
9
+ - API key auth as alternative
10
+ """
11
+
12
+ import base64
13
+ import json
14
+ import logging
15
+ import os
16
+ import time
17
+ from typing import Optional
18
+
19
+ import httpx
20
+
21
+ from quantlib_api.exceptions import QuantLibAuthError, QuantLibNetworkError
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ API_BASE_URL_ENV = "QUANTLIB_URL"
26
+ API_KEY_ENV = "QUANTLIB_API_KEY"
27
+ DEFAULT_BASE_URL = "http://localhost:8000"
28
+
29
+
30
+ def _decode_jwt_payload(token: str) -> dict:
31
+ """Decode JWT payload without verifying signature (for expiry check only)."""
32
+ try:
33
+ parts = token.split(".")
34
+ if len(parts) != 3:
35
+ return {}
36
+ payload = parts[1]
37
+ # Add padding
38
+ payload += "=" * (4 - len(payload) % 4)
39
+ decoded = base64.urlsafe_b64decode(payload)
40
+ return json.loads(decoded)
41
+ except Exception:
42
+ return {}
43
+
44
+
45
+ def _token_expired(token: str, buffer_seconds: int = 60) -> bool:
46
+ """Return True if the JWT token expires within buffer_seconds."""
47
+ payload = _decode_jwt_payload(token)
48
+ exp = payload.get("exp")
49
+ if not exp:
50
+ return False # No expiry claim → assume valid
51
+ return time.time() >= (exp - buffer_seconds)
52
+
53
+
54
+ class AuthManager:
55
+ """
56
+ Manages JWT authentication lifecycle for the QuantLib Pro API.
57
+
58
+ Supports:
59
+ - username/password login (POST /auth/login)
60
+ - API key via Authorization header
61
+ - Auto-refresh on expiry
62
+ - Environment variable configuration (QUANTLIB_URL, QUANTLIB_API_KEY)
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ base_url: str,
68
+ username: Optional[str] = None,
69
+ password: Optional[str] = None,
70
+ api_key: Optional[str] = None,
71
+ ):
72
+ self.base_url = base_url.rstrip("/")
73
+ self._username = username
74
+ self._password = password
75
+ self._api_key = api_key or os.environ.get(API_KEY_ENV)
76
+ self._token: Optional[str] = None
77
+
78
+ @classmethod
79
+ def from_env(cls) -> "AuthManager":
80
+ """Create AuthManager from environment variables."""
81
+ base_url = os.environ.get(API_BASE_URL_ENV, DEFAULT_BASE_URL)
82
+ api_key = os.environ.get(API_KEY_ENV)
83
+ return cls(base_url=base_url, api_key=api_key)
84
+
85
+ def login(self, username: Optional[str] = None, password: Optional[str] = None) -> str:
86
+ """
87
+ Authenticate with username/password and store the JWT token.
88
+
89
+ Returns:
90
+ str: The JWT access token
91
+
92
+ Raises:
93
+ QuantLibAuthError: If credentials are invalid
94
+ QuantLibNetworkError: If the server is unreachable
95
+ """
96
+ user = username or self._username
97
+ pwd = password or self._password
98
+
99
+ if not user or not pwd:
100
+ raise QuantLibAuthError("Username and password are required for login.")
101
+
102
+ try:
103
+ response = httpx.post(
104
+ f"{self.base_url}/auth/login",
105
+ json={"username": user, "password": pwd},
106
+ timeout=10.0,
107
+ )
108
+ except httpx.ConnectError as e:
109
+ raise QuantLibNetworkError(f"Cannot connect to {self.base_url}") from e
110
+ except httpx.TimeoutException as e:
111
+ raise QuantLibNetworkError("Login request timed out") from e
112
+
113
+ if response.status_code == 200:
114
+ data = response.json()
115
+ token = data.get("access_token") or data.get("token")
116
+ if not token:
117
+ raise QuantLibAuthError("Login succeeded but no token in response.")
118
+ self._token = token
119
+ logger.info("✅ Successfully authenticated with QuantLib Pro API")
120
+ return token
121
+ elif response.status_code in (401, 403):
122
+ raise QuantLibAuthError(f"Invalid credentials: {response.json().get('detail', '')}")
123
+ else:
124
+ raise QuantLibAuthError(f"Login failed with status {response.status_code}")
125
+
126
+ def get_token(self, auto_refresh: bool = True) -> Optional[str]:
127
+ """
128
+ Get the current valid token. Auto-refreshes if expired and credentials available.
129
+
130
+ Returns:
131
+ str | None: JWT token, or None if not authenticated
132
+ """
133
+ if self._api_key:
134
+ return self._api_key # API key doesn't expire
135
+
136
+ if not self._token:
137
+ return None
138
+
139
+ if auto_refresh and _token_expired(self._token) and self._username and self._password:
140
+ logger.info("Token expired. Auto-refreshing...")
141
+ try:
142
+ self.login()
143
+ except Exception as e:
144
+ logger.warning(f"Token refresh failed: {e}")
145
+ return None
146
+
147
+ return self._token
148
+
149
+ def get_headers(self) -> dict:
150
+ """Return Authorization headers for API requests."""
151
+ token = self.get_token()
152
+ if token:
153
+ return {"Authorization": f"Bearer {token}"}
154
+ return {}
155
+
156
+ @property
157
+ def is_authenticated(self) -> bool:
158
+ """True if a valid token is available."""
159
+ return bool(self.get_token(auto_refresh=False))
160
+
161
+ def logout(self):
162
+ """Clear stored credentials."""
163
+ self._token = None
164
+ logger.info("Logged out from QuantLib Pro API")