finatic-server-python 0.1.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.
- finatic_server/__init__.py +127 -0
- finatic_server/core/__init__.py +6 -0
- finatic_server/core/api_client.py +868 -0
- finatic_server/core/client.py +577 -0
- finatic_server/types/__init__.py +103 -0
- finatic_server/types/auth.py +142 -0
- finatic_server/types/broker.py +147 -0
- finatic_server/types/common.py +177 -0
- finatic_server/types/orders.py +74 -0
- finatic_server/types/portfolio.py +61 -0
- finatic_server/utils/__init__.py +17 -0
- finatic_server/utils/errors.py +62 -0
- finatic_server_python-0.1.0.dist-info/METADATA +330 -0
- finatic_server_python-0.1.0.dist-info/RECORD +16 -0
- finatic_server_python-0.1.0.dist-info/WHEEL +5 -0
- finatic_server_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
"""Main client class for the Finatic Server SDK."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
from .api_client import ApiClient
|
|
8
|
+
from ..types import (
|
|
9
|
+
DeviceInfo,
|
|
10
|
+
SessionInitResponse,
|
|
11
|
+
SessionResponse,
|
|
12
|
+
OtpRequestResponse,
|
|
13
|
+
OtpVerifyResponse,
|
|
14
|
+
SessionAuthenticateResponse,
|
|
15
|
+
PortalUrlResponse,
|
|
16
|
+
UserToken,
|
|
17
|
+
Holding,
|
|
18
|
+
Order,
|
|
19
|
+
Portfolio,
|
|
20
|
+
BrokerInfo,
|
|
21
|
+
BrokerAccount,
|
|
22
|
+
BrokerOrder,
|
|
23
|
+
BrokerPosition,
|
|
24
|
+
BrokerConnection,
|
|
25
|
+
BrokerDataOptions,
|
|
26
|
+
OrdersFilter,
|
|
27
|
+
PositionsFilter,
|
|
28
|
+
AccountsFilter,
|
|
29
|
+
OrderResponse,
|
|
30
|
+
BrokerOrderParams,
|
|
31
|
+
BrokerExtras,
|
|
32
|
+
CryptoOrderOptions,
|
|
33
|
+
OptionsOrderOptions,
|
|
34
|
+
TradingContext,
|
|
35
|
+
ApiPaginationInfo,
|
|
36
|
+
PaginatedResult,
|
|
37
|
+
)
|
|
38
|
+
from ..utils.errors import (
|
|
39
|
+
AuthenticationError,
|
|
40
|
+
ValidationError,
|
|
41
|
+
ApiError,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FinaticServerClient:
|
|
46
|
+
"""Main client for interacting with the Finatic Server API.
|
|
47
|
+
|
|
48
|
+
This client provides a high-level interface for authentication, portfolio management,
|
|
49
|
+
and trading operations. It handles API key authentication and session management
|
|
50
|
+
automatically.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
api_key: str,
|
|
56
|
+
base_url: str = "https://api.finatic.dev",
|
|
57
|
+
device_info: Optional[DeviceInfo] = None,
|
|
58
|
+
timeout: int = 30
|
|
59
|
+
):
|
|
60
|
+
"""Initialize the Finatic Server client.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
api_key: API key for authentication
|
|
64
|
+
base_url: Base URL for the API
|
|
65
|
+
device_info: Device information for requests
|
|
66
|
+
timeout: Request timeout in seconds
|
|
67
|
+
"""
|
|
68
|
+
self.api_key = api_key
|
|
69
|
+
self.base_url = base_url
|
|
70
|
+
self.device_info = device_info
|
|
71
|
+
self.timeout = timeout
|
|
72
|
+
|
|
73
|
+
# Initialize API client
|
|
74
|
+
self._api_client = ApiClient(base_url, device_info, timeout)
|
|
75
|
+
|
|
76
|
+
# Session state
|
|
77
|
+
self._session_id: Optional[str] = None
|
|
78
|
+
self._company_id: Optional[str] = None
|
|
79
|
+
self._user_token: Optional[UserToken] = None
|
|
80
|
+
self._one_time_token: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
# Trading context
|
|
83
|
+
self._trading_context: TradingContext = TradingContext()
|
|
84
|
+
|
|
85
|
+
async def __aenter__(self):
|
|
86
|
+
"""Async context manager entry."""
|
|
87
|
+
await self._api_client.__aenter__()
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
91
|
+
"""Async context manager exit."""
|
|
92
|
+
try:
|
|
93
|
+
await self._api_client.__aexit__(exc_type, exc_val, exc_tb)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
# Log cleanup errors but don't raise them
|
|
96
|
+
print(f"Warning: Error during client cleanup: {e}")
|
|
97
|
+
|
|
98
|
+
def __del__(self):
|
|
99
|
+
"""Destructor to ensure cleanup if context manager is not used."""
|
|
100
|
+
try:
|
|
101
|
+
if hasattr(self, '_api_client') and self._api_client:
|
|
102
|
+
# Try to close the session if it's still open
|
|
103
|
+
if hasattr(self._api_client, '_session') and self._api_client._session:
|
|
104
|
+
if not self._api_client._session.closed:
|
|
105
|
+
asyncio.create_task(self._api_client._session.close())
|
|
106
|
+
except Exception:
|
|
107
|
+
# Ignore cleanup errors in destructor
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
async def _initialize_session(self) -> str:
|
|
111
|
+
"""Initialize a session by getting a one-time token.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
One-time token for session initialization
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
AuthenticationError: If API key is invalid
|
|
118
|
+
ApiError: For other API errors
|
|
119
|
+
"""
|
|
120
|
+
if self._one_time_token:
|
|
121
|
+
return self._one_time_token
|
|
122
|
+
|
|
123
|
+
# Call the session init endpoint with API key
|
|
124
|
+
response = await self._api_client._request(
|
|
125
|
+
method='POST',
|
|
126
|
+
path='/auth/session/init',
|
|
127
|
+
additional_headers={'X-API-Key': self.api_key}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
session_init = SessionInitResponse(**response)
|
|
131
|
+
self._one_time_token = session_init.data['one_time_token']
|
|
132
|
+
|
|
133
|
+
return self._one_time_token
|
|
134
|
+
|
|
135
|
+
async def start_session(self, user_id: Optional[str] = None) -> SessionResponse:
|
|
136
|
+
"""Start a session with the one-time token.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
user_id: Optional user ID for direct authentication
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Session response
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
AuthenticationError: If token is invalid
|
|
146
|
+
ApiError: For other API errors
|
|
147
|
+
"""
|
|
148
|
+
# Get one-time token if not already available
|
|
149
|
+
token = await self._initialize_session()
|
|
150
|
+
|
|
151
|
+
# Start session
|
|
152
|
+
response = await self._api_client._request(
|
|
153
|
+
method='POST',
|
|
154
|
+
path='/auth/session/start',
|
|
155
|
+
data={'user_id': user_id} if user_id else {},
|
|
156
|
+
additional_headers={'One-Time-Token': token}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
session_response = SessionResponse(**response)
|
|
160
|
+
|
|
161
|
+
# Handle both nested data structure and flat structure
|
|
162
|
+
if session_response.data:
|
|
163
|
+
# Nested structure (like frontend SDK expects)
|
|
164
|
+
self._session_id = session_response.data.session_id
|
|
165
|
+
self._company_id = session_response.data.company_id
|
|
166
|
+
else:
|
|
167
|
+
# Flat structure (what your API currently returns)
|
|
168
|
+
self._session_id = session_response.session_id
|
|
169
|
+
self._company_id = session_response.company_id
|
|
170
|
+
|
|
171
|
+
# Set session context in API client (only if we have valid IDs)
|
|
172
|
+
if self._session_id and self._company_id:
|
|
173
|
+
self._api_client.set_session_context(
|
|
174
|
+
self._session_id,
|
|
175
|
+
self._company_id
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return session_response
|
|
179
|
+
|
|
180
|
+
async def request_otp(self, email: str) -> OtpRequestResponse:
|
|
181
|
+
"""Request OTP for session authentication.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
email: Email address to send OTP to
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
OTP request response
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
AuthenticationError: If session is not active
|
|
191
|
+
ValidationError: If email is invalid
|
|
192
|
+
ApiError: For other API errors
|
|
193
|
+
"""
|
|
194
|
+
if not self._session_id:
|
|
195
|
+
raise AuthenticationError("Session not initialized. Call start_session() first.")
|
|
196
|
+
|
|
197
|
+
response = await self._api_client._request(
|
|
198
|
+
method='POST',
|
|
199
|
+
path='/auth/otp/request',
|
|
200
|
+
data={'email': email}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return OtpRequestResponse(**response)
|
|
204
|
+
|
|
205
|
+
async def verify_otp(self, otp: str) -> OtpVerifyResponse:
|
|
206
|
+
"""Verify OTP for session authentication.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
otp: One-time password to verify
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
OTP verification response with tokens
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
AuthenticationError: If OTP is invalid or session is not active
|
|
216
|
+
ApiError: For other API errors
|
|
217
|
+
"""
|
|
218
|
+
if not self._session_id:
|
|
219
|
+
raise AuthenticationError("Session not initialized. Call start_session() first.")
|
|
220
|
+
|
|
221
|
+
response = await self._api_client._request(
|
|
222
|
+
method='POST',
|
|
223
|
+
path='/auth/otp/verify',
|
|
224
|
+
data={'otp': otp}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
otp_response = OtpVerifyResponse(**response)
|
|
228
|
+
|
|
229
|
+
# Store tokens
|
|
230
|
+
if otp_response.success and otp_response.data:
|
|
231
|
+
self._user_token = UserToken(
|
|
232
|
+
access_token=otp_response.data['access_token'],
|
|
233
|
+
refresh_token=otp_response.data['refresh_token'],
|
|
234
|
+
expires_in=otp_response.data['expires_in'],
|
|
235
|
+
user_id=otp_response.data['user_id'],
|
|
236
|
+
token_type=otp_response.data.get('token_type', 'Bearer'),
|
|
237
|
+
scope=otp_response.data.get('scope', '')
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Set tokens in API client
|
|
241
|
+
expires_at = (datetime.now() + timedelta(seconds=otp_response.data['expires_in'])).isoformat()
|
|
242
|
+
self._api_client.set_tokens(
|
|
243
|
+
otp_response.data['access_token'],
|
|
244
|
+
otp_response.data['refresh_token'],
|
|
245
|
+
expires_at,
|
|
246
|
+
otp_response.data['user_id']
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return otp_response
|
|
250
|
+
|
|
251
|
+
async def authenticate_directly(self, user_id: str) -> SessionAuthenticateResponse:
|
|
252
|
+
"""Authenticate session directly with user ID.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
user_id: User ID for direct authentication
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Authentication response with tokens
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
AuthenticationError: If authentication fails
|
|
262
|
+
ApiError: For other API errors
|
|
263
|
+
"""
|
|
264
|
+
if not self._session_id:
|
|
265
|
+
raise AuthenticationError("Session not initialized. Call start_session() first.")
|
|
266
|
+
|
|
267
|
+
response = await self._api_client._request(
|
|
268
|
+
method='POST',
|
|
269
|
+
path='/auth/session/authenticate',
|
|
270
|
+
data={'session_id': self._session_id, 'user_id': user_id}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
auth_response = SessionAuthenticateResponse(**response)
|
|
274
|
+
|
|
275
|
+
# Store tokens
|
|
276
|
+
if auth_response.success and auth_response.data:
|
|
277
|
+
self._user_token = UserToken(
|
|
278
|
+
access_token=auth_response.data['access_token'],
|
|
279
|
+
refresh_token=auth_response.data['refresh_token'],
|
|
280
|
+
expires_in=3600, # Default 1 hour
|
|
281
|
+
user_id=user_id,
|
|
282
|
+
token_type='Bearer',
|
|
283
|
+
scope='api:access'
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Set tokens in API client
|
|
287
|
+
expires_at = (datetime.now() + timedelta(hours=1)).isoformat()
|
|
288
|
+
self._api_client.set_tokens(
|
|
289
|
+
auth_response.data['access_token'],
|
|
290
|
+
auth_response.data['refresh_token'],
|
|
291
|
+
expires_at,
|
|
292
|
+
user_id
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return auth_response
|
|
296
|
+
|
|
297
|
+
async def get_portal_url(self) -> str:
|
|
298
|
+
"""Get the portal URL for user authentication.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Portal URL string
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
AuthenticationError: If session is not initialized
|
|
305
|
+
"""
|
|
306
|
+
if not self._session_id:
|
|
307
|
+
raise AuthenticationError("Session not initialized. Call start_session() first.")
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
response = await self._api_client.get_portal_url(self._session_id)
|
|
311
|
+
return response.data['portal_url']
|
|
312
|
+
except Exception as e:
|
|
313
|
+
raise AuthenticationError(f"Failed to get portal URL: {str(e)}")
|
|
314
|
+
|
|
315
|
+
async def get_session_user(self) -> Dict[str, Any]:
|
|
316
|
+
"""Get the user and tokens for a completed session.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Dict containing user_id, access_token, refresh_token, and other user info
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
AuthenticationError: If session is not initialized or not completed
|
|
323
|
+
"""
|
|
324
|
+
if not self._session_id:
|
|
325
|
+
raise AuthenticationError("Session not initialized. Call start_session() first.")
|
|
326
|
+
|
|
327
|
+
if not self._company_id:
|
|
328
|
+
raise AuthenticationError("Company ID not available. Session may not be properly initialized.")
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
# Call the new endpoint with session ID as Bearer token and company ID header
|
|
332
|
+
response = await self._api_client.get_session_user(self._session_id, self._company_id)
|
|
333
|
+
|
|
334
|
+
# Store tokens internally for future API calls
|
|
335
|
+
self._store_tokens(response)
|
|
336
|
+
|
|
337
|
+
# Return user info using the getter methods
|
|
338
|
+
return {
|
|
339
|
+
"user_id": response.get_user_id(),
|
|
340
|
+
"access_token": response.get_access_token(),
|
|
341
|
+
"refresh_token": response.get_refresh_token(),
|
|
342
|
+
"expires_in": response.get_expires_in(),
|
|
343
|
+
"token_type": response.get_token_type(),
|
|
344
|
+
"scope": response.get_scope(),
|
|
345
|
+
"company_id": response.get_company_id()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
raise AuthenticationError(f"Failed to get session user: {str(e)}")
|
|
350
|
+
|
|
351
|
+
def _store_tokens(self, user_response):
|
|
352
|
+
"""Store tokens internally for automatic use in API calls."""
|
|
353
|
+
# Store in the API client for automatic token management
|
|
354
|
+
expires_at = datetime.now() + timedelta(seconds=user_response.get_expires_in())
|
|
355
|
+
self._api_client.set_tokens(
|
|
356
|
+
user_response.get_access_token(),
|
|
357
|
+
user_response.get_refresh_token(),
|
|
358
|
+
expires_at.isoformat(),
|
|
359
|
+
user_response.get_user_id()
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Also store in our local state
|
|
363
|
+
self._user_token = UserToken(
|
|
364
|
+
access_token=user_response.get_access_token(),
|
|
365
|
+
refresh_token=user_response.get_refresh_token(),
|
|
366
|
+
expires_in=user_response.get_expires_in(),
|
|
367
|
+
user_id=user_response.get_user_id(),
|
|
368
|
+
token_type=user_response.get_token_type(),
|
|
369
|
+
scope=user_response.get_scope()
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def is_authenticated(self) -> bool:
|
|
373
|
+
"""Check if the client is authenticated.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
True if authenticated, False otherwise
|
|
377
|
+
"""
|
|
378
|
+
return (
|
|
379
|
+
self._user_token is not None and
|
|
380
|
+
self._user_token.access_token is not None and
|
|
381
|
+
self._user_token.refresh_token is not None and
|
|
382
|
+
self._user_token.user_id is not None
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
async def _ensure_authenticated(self):
|
|
386
|
+
"""Ensure the client is authenticated.
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
AuthenticationError: If not authenticated
|
|
390
|
+
"""
|
|
391
|
+
if not self.is_authenticated():
|
|
392
|
+
raise AuthenticationError("Client not authenticated. Complete authentication flow first.")
|
|
393
|
+
|
|
394
|
+
# Portfolio methods
|
|
395
|
+
async def get_holdings(self) -> List[Holding]:
|
|
396
|
+
"""Get portfolio holdings.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
List of holdings
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
AuthenticationError: If not authenticated
|
|
403
|
+
ApiError: For other API errors
|
|
404
|
+
"""
|
|
405
|
+
await self._ensure_authenticated()
|
|
406
|
+
access_token = await self._api_client.get_valid_access_token()
|
|
407
|
+
|
|
408
|
+
response = await self._api_client._request(
|
|
409
|
+
method='GET',
|
|
410
|
+
path='/portfolio/holdings',
|
|
411
|
+
access_token=access_token
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
return [Holding(**holding) for holding in response.get('data', [])]
|
|
415
|
+
|
|
416
|
+
async def get_portfolio(self) -> Portfolio:
|
|
417
|
+
"""Get portfolio information.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Portfolio information
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
AuthenticationError: If not authenticated
|
|
424
|
+
ApiError: For other API errors
|
|
425
|
+
"""
|
|
426
|
+
await self._ensure_authenticated()
|
|
427
|
+
access_token = await self._api_client.get_valid_access_token()
|
|
428
|
+
|
|
429
|
+
response = await self._api_client._request(
|
|
430
|
+
method='GET',
|
|
431
|
+
path='/portfolio',
|
|
432
|
+
access_token=access_token
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
return Portfolio(**response['data'])
|
|
436
|
+
|
|
437
|
+
async def get_orders(self) -> List[Order]:
|
|
438
|
+
"""Get portfolio orders.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
List of orders
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
AuthenticationError: If not authenticated
|
|
445
|
+
ApiError: For other API errors
|
|
446
|
+
"""
|
|
447
|
+
await self._ensure_authenticated()
|
|
448
|
+
access_token = await self._api_client.get_valid_access_token()
|
|
449
|
+
|
|
450
|
+
response = await self._api_client._request(
|
|
451
|
+
method='GET',
|
|
452
|
+
path='/portfolio/orders',
|
|
453
|
+
access_token=access_token
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return [Order(**order) for order in response.get('data', [])]
|
|
457
|
+
|
|
458
|
+
# Trading context methods
|
|
459
|
+
def set_broker(self, broker: str):
|
|
460
|
+
"""Set the current broker."""
|
|
461
|
+
self._trading_context.broker = broker
|
|
462
|
+
self._api_client.set_broker(broker)
|
|
463
|
+
|
|
464
|
+
def set_account(self, account_number: str, account_id: Optional[str] = None):
|
|
465
|
+
"""Set the current account."""
|
|
466
|
+
self._trading_context.account_number = account_number
|
|
467
|
+
self._trading_context.account_id = account_id
|
|
468
|
+
self._api_client.set_account(account_number, account_id)
|
|
469
|
+
|
|
470
|
+
def get_trading_context(self) -> TradingContext:
|
|
471
|
+
"""Get the current trading context."""
|
|
472
|
+
return self._trading_context
|
|
473
|
+
|
|
474
|
+
def clear_trading_context(self):
|
|
475
|
+
"""Clear the trading context."""
|
|
476
|
+
self._trading_context = TradingContext()
|
|
477
|
+
self._api_client.clear_trading_context()
|
|
478
|
+
|
|
479
|
+
# Utility methods
|
|
480
|
+
def get_user_id(self) -> Optional[str]:
|
|
481
|
+
"""Get the current user ID."""
|
|
482
|
+
return self._user_token.user_id if self._user_token else None
|
|
483
|
+
|
|
484
|
+
def get_session_id(self) -> Optional[str]:
|
|
485
|
+
"""Get the current session ID."""
|
|
486
|
+
return self._session_id
|
|
487
|
+
|
|
488
|
+
def get_company_id(self) -> Optional[str]:
|
|
489
|
+
"""Get the current company ID."""
|
|
490
|
+
return self._company_id
|
|
491
|
+
|
|
492
|
+
def is_authed(self) -> bool:
|
|
493
|
+
"""Return True if the client has a valid access and refresh token."""
|
|
494
|
+
token_info = self._api_client.token_info
|
|
495
|
+
if not token_info:
|
|
496
|
+
return False
|
|
497
|
+
access_token = token_info.get('access_token')
|
|
498
|
+
refresh_token = token_info.get('refresh_token')
|
|
499
|
+
expires_at = token_info.get('expires_at')
|
|
500
|
+
if not access_token or not refresh_token:
|
|
501
|
+
return False
|
|
502
|
+
# Optionally, check if access token is expired
|
|
503
|
+
if expires_at:
|
|
504
|
+
from datetime import datetime
|
|
505
|
+
try:
|
|
506
|
+
expires_dt = datetime.fromisoformat(expires_at)
|
|
507
|
+
if expires_dt < datetime.now():
|
|
508
|
+
return False
|
|
509
|
+
except Exception:
|
|
510
|
+
pass
|
|
511
|
+
return True
|
|
512
|
+
|
|
513
|
+
# Simple methods that automatically use stored tokens
|
|
514
|
+
async def get_holdings(self) -> List[Holding]:
|
|
515
|
+
"""Get holdings using stored access token."""
|
|
516
|
+
return await self._api_client.get_holdings_auto()
|
|
517
|
+
|
|
518
|
+
async def get_orders(self) -> List[Order]:
|
|
519
|
+
"""Get orders using stored access token."""
|
|
520
|
+
return await self._api_client.get_orders_auto()
|
|
521
|
+
|
|
522
|
+
async def get_portfolio(self) -> Portfolio:
|
|
523
|
+
"""Get portfolio using stored access token."""
|
|
524
|
+
return await self._api_client.get_portfolio_auto()
|
|
525
|
+
|
|
526
|
+
async def get_broker_list(self) -> List[BrokerInfo]:
|
|
527
|
+
"""Get broker list using stored access token."""
|
|
528
|
+
return await self._api_client.get_broker_list_auto()
|
|
529
|
+
|
|
530
|
+
async def get_broker_accounts(self, page: int = 1, per_page: int = 100, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> PaginatedResult:
|
|
531
|
+
"""Get broker accounts with pagination support."""
|
|
532
|
+
return await self._api_client.get_broker_accounts(page, per_page, options, filters)
|
|
533
|
+
|
|
534
|
+
async def get_broker_orders(self, page: int = 1, per_page: int = 100, options: Optional[BrokerDataOptions] = None, filters: Optional[OrdersFilter] = None) -> PaginatedResult:
|
|
535
|
+
"""Get broker orders with pagination support."""
|
|
536
|
+
return await self._api_client.get_broker_orders(page, per_page, options, filters)
|
|
537
|
+
|
|
538
|
+
async def get_broker_positions(self, page: int = 1, per_page: int = 100, options: Optional[BrokerDataOptions] = None, filters: Optional[PositionsFilter] = None) -> PaginatedResult:
|
|
539
|
+
"""Get broker positions with pagination support."""
|
|
540
|
+
return await self._api_client.get_broker_positions(page, per_page, options, filters)
|
|
541
|
+
|
|
542
|
+
async def get_broker_connections(self) -> List[BrokerConnection]:
|
|
543
|
+
"""Get broker connections using stored access token."""
|
|
544
|
+
return await self._api_client.get_broker_connections_auto()
|
|
545
|
+
|
|
546
|
+
# Helper methods to get all data across pages
|
|
547
|
+
async def get_all_broker_accounts(self, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> List[BrokerAccount]:
|
|
548
|
+
"""Get all broker accounts across all pages."""
|
|
549
|
+
return await self._api_client.get_all_broker_accounts(options, filters)
|
|
550
|
+
|
|
551
|
+
async def get_all_broker_orders(self, options: Optional[BrokerDataOptions] = None, filters: Optional[OrdersFilter] = None) -> List[BrokerOrder]:
|
|
552
|
+
"""Get all broker orders across all pages."""
|
|
553
|
+
return await self._api_client.get_all_broker_orders(options, filters)
|
|
554
|
+
|
|
555
|
+
async def get_all_broker_positions(self, options: Optional[BrokerDataOptions] = None, filters: Optional[PositionsFilter] = None) -> List[BrokerPosition]:
|
|
556
|
+
"""Get all broker positions across all pages."""
|
|
557
|
+
return await self._api_client.get_all_broker_positions(options, filters)
|
|
558
|
+
|
|
559
|
+
async def close(self):
|
|
560
|
+
"""Manually close the client and cleanup resources."""
|
|
561
|
+
try:
|
|
562
|
+
if hasattr(self, '_api_client') and self._api_client:
|
|
563
|
+
await self._api_client.__aexit__(None, None, None)
|
|
564
|
+
except Exception as e:
|
|
565
|
+
print(f"Warning: Error during client close: {e}")
|
|
566
|
+
|
|
567
|
+
def __del__(self):
|
|
568
|
+
"""Destructor to ensure cleanup if context manager is not used."""
|
|
569
|
+
try:
|
|
570
|
+
if hasattr(self, '_api_client') and self._api_client:
|
|
571
|
+
# Try to close the session if it's still open
|
|
572
|
+
if hasattr(self._api_client, '_session') and self._api_client._session:
|
|
573
|
+
if not self._api_client._session.closed:
|
|
574
|
+
asyncio.create_task(self._api_client._session.close())
|
|
575
|
+
except Exception:
|
|
576
|
+
# Ignore cleanup errors in destructor
|
|
577
|
+
pass
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Type definitions and data models for the Finatic Server SDK."""
|
|
2
|
+
|
|
3
|
+
# Common types
|
|
4
|
+
from .common import (
|
|
5
|
+
DeviceInfo,
|
|
6
|
+
ApiResponse,
|
|
7
|
+
ApiPaginationInfo,
|
|
8
|
+
PaginationMetadata,
|
|
9
|
+
PaginatedResult,
|
|
10
|
+
TradingContext,
|
|
11
|
+
RequestHeaders,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Authentication types
|
|
15
|
+
from .auth import (
|
|
16
|
+
UserToken,
|
|
17
|
+
SessionResponse,
|
|
18
|
+
SessionInitResponse,
|
|
19
|
+
OtpRequestResponse,
|
|
20
|
+
OtpVerifyResponse,
|
|
21
|
+
SessionAuthenticateResponse,
|
|
22
|
+
PortalUrlResponse,
|
|
23
|
+
SessionValidationResponse,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Portfolio types
|
|
27
|
+
from .portfolio import (
|
|
28
|
+
Portfolio,
|
|
29
|
+
Holding,
|
|
30
|
+
PerformanceMetrics,
|
|
31
|
+
PortfolioSnapshot,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Order types
|
|
35
|
+
from .orders import (
|
|
36
|
+
Order,
|
|
37
|
+
OptionsOrder,
|
|
38
|
+
CryptoOrderOptions,
|
|
39
|
+
OptionsOrderOptions,
|
|
40
|
+
OrderResponse,
|
|
41
|
+
BrokerOrderParams,
|
|
42
|
+
BrokerExtras,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Broker types
|
|
46
|
+
from .broker import (
|
|
47
|
+
BrokerAccount,
|
|
48
|
+
BrokerOrder,
|
|
49
|
+
BrokerPosition,
|
|
50
|
+
BrokerInfo,
|
|
51
|
+
BrokerConnection,
|
|
52
|
+
BrokerDataOptions,
|
|
53
|
+
OrdersFilter,
|
|
54
|
+
PositionsFilter,
|
|
55
|
+
AccountsFilter,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Common
|
|
60
|
+
"DeviceInfo",
|
|
61
|
+
"ApiResponse",
|
|
62
|
+
"ApiPaginationInfo",
|
|
63
|
+
"PaginationMetadata",
|
|
64
|
+
"PaginatedResult",
|
|
65
|
+
"TradingContext",
|
|
66
|
+
"RequestHeaders",
|
|
67
|
+
|
|
68
|
+
# Auth
|
|
69
|
+
"UserToken",
|
|
70
|
+
"SessionResponse",
|
|
71
|
+
"SessionInitResponse",
|
|
72
|
+
"OtpRequestResponse",
|
|
73
|
+
"OtpVerifyResponse",
|
|
74
|
+
"SessionAuthenticateResponse",
|
|
75
|
+
"PortalUrlResponse",
|
|
76
|
+
"SessionValidationResponse",
|
|
77
|
+
|
|
78
|
+
# Portfolio
|
|
79
|
+
"Portfolio",
|
|
80
|
+
"Holding",
|
|
81
|
+
"PerformanceMetrics",
|
|
82
|
+
"PortfolioSnapshot",
|
|
83
|
+
|
|
84
|
+
# Orders
|
|
85
|
+
"Order",
|
|
86
|
+
"OptionsOrder",
|
|
87
|
+
"CryptoOrderOptions",
|
|
88
|
+
"OptionsOrderOptions",
|
|
89
|
+
"OrderResponse",
|
|
90
|
+
"BrokerOrderParams",
|
|
91
|
+
"BrokerExtras",
|
|
92
|
+
|
|
93
|
+
# Broker
|
|
94
|
+
"BrokerAccount",
|
|
95
|
+
"BrokerOrder",
|
|
96
|
+
"BrokerPosition",
|
|
97
|
+
"BrokerInfo",
|
|
98
|
+
"BrokerConnection",
|
|
99
|
+
"BrokerDataOptions",
|
|
100
|
+
"OrdersFilter",
|
|
101
|
+
"PositionsFilter",
|
|
102
|
+
"AccountsFilter",
|
|
103
|
+
]
|