publicdotcom-py 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.
- public_api_sdk/__init__.py +111 -0
- public_api_sdk/api_client.py +181 -0
- public_api_sdk/auth_config.py +90 -0
- public_api_sdk/auth_manager.py +38 -0
- public_api_sdk/auth_provider.py +319 -0
- public_api_sdk/exceptions.py +83 -0
- public_api_sdk/models/__init__.py +102 -0
- public_api_sdk/models/account.py +50 -0
- public_api_sdk/models/auth.py +18 -0
- public_api_sdk/models/history.py +164 -0
- public_api_sdk/models/instrument.py +64 -0
- public_api_sdk/models/instrument_type.py +12 -0
- public_api_sdk/models/new_order.py +300 -0
- public_api_sdk/models/option.py +409 -0
- public_api_sdk/models/order.py +537 -0
- public_api_sdk/models/portfolio.py +141 -0
- public_api_sdk/models/quote.py +100 -0
- public_api_sdk/models/subscription.py +79 -0
- public_api_sdk/order_subscription_manager.py +363 -0
- public_api_sdk/price_stream.py +128 -0
- public_api_sdk/public_api_client.py +482 -0
- public_api_sdk/py.typed +2 -0
- public_api_sdk/subscription_manager.py +370 -0
- publicdotcom_py-0.1.0.dist-info/METADATA +586 -0
- publicdotcom_py-0.1.0.dist-info/RECORD +28 -0
- publicdotcom_py-0.1.0.dist-info/WHEEL +5 -0
- publicdotcom_py-0.1.0.dist-info/licenses/LICENCE +202 -0
- publicdotcom_py-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from .auth_config import AuthConfig, ApiKeyAuthConfig, OAuthAuthConfig
|
|
2
|
+
from .exceptions import APIError, AuthenticationError, RateLimitError
|
|
3
|
+
from .models import (
|
|
4
|
+
Account,
|
|
5
|
+
AccountType,
|
|
6
|
+
HistoryRequest,
|
|
7
|
+
HistoryResponsePage,
|
|
8
|
+
InstrumentsRequest,
|
|
9
|
+
InstrumentsResponse,
|
|
10
|
+
InstrumentType,
|
|
11
|
+
Trading,
|
|
12
|
+
OptionChainRequest,
|
|
13
|
+
OptionChainResponse,
|
|
14
|
+
OptionExpirationsRequest,
|
|
15
|
+
OptionExpirationsResponse,
|
|
16
|
+
OrderExpirationRequest,
|
|
17
|
+
OrderInstrument,
|
|
18
|
+
OrderSide,
|
|
19
|
+
OrderType,
|
|
20
|
+
Order,
|
|
21
|
+
OrderRequest,
|
|
22
|
+
OrderResponse,
|
|
23
|
+
OrderStatus,
|
|
24
|
+
NewOrder,
|
|
25
|
+
OrderUpdate,
|
|
26
|
+
OrderSubscriptionConfig,
|
|
27
|
+
WaitTimeoutError,
|
|
28
|
+
PreflightRequest,
|
|
29
|
+
PreflightMultiLegRequest,
|
|
30
|
+
PreflightMultiLegResponse,
|
|
31
|
+
MultilegOrderRequest,
|
|
32
|
+
MultilegOrderResult,
|
|
33
|
+
OptionGreeks,
|
|
34
|
+
Portfolio,
|
|
35
|
+
TimeInForce,
|
|
36
|
+
LegInstrument,
|
|
37
|
+
LegInstrumentType,
|
|
38
|
+
OpenCloseIndicator,
|
|
39
|
+
OrderLegRequest,
|
|
40
|
+
Quote,
|
|
41
|
+
QuoteOutcome,
|
|
42
|
+
PriceChange,
|
|
43
|
+
PriceChangeCallback,
|
|
44
|
+
SubscriptionConfig,
|
|
45
|
+
SubscriptionStatus,
|
|
46
|
+
Subscription,
|
|
47
|
+
SubscriptionInfo,
|
|
48
|
+
)
|
|
49
|
+
from .public_api_client import PublicApiClient, PublicApiClientConfiguration
|
|
50
|
+
from .subscription_manager import PriceSubscriptionManager
|
|
51
|
+
from .price_stream import PriceStream
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__version__ = "0.1.0"
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"AuthConfig",
|
|
58
|
+
"ApiKeyAuthConfig",
|
|
59
|
+
"OAuthAuthConfig",
|
|
60
|
+
"Account",
|
|
61
|
+
"AccountType",
|
|
62
|
+
"HistoryRequest",
|
|
63
|
+
"HistoryResponsePage",
|
|
64
|
+
"InstrumentsRequest",
|
|
65
|
+
"InstrumentsResponse",
|
|
66
|
+
"InstrumentType",
|
|
67
|
+
"Trading",
|
|
68
|
+
"OptionChainRequest",
|
|
69
|
+
"OptionChainResponse",
|
|
70
|
+
"OptionExpirationsRequest",
|
|
71
|
+
"OptionExpirationsResponse",
|
|
72
|
+
"OrderExpirationRequest",
|
|
73
|
+
"OrderInstrument",
|
|
74
|
+
"OrderSide",
|
|
75
|
+
"OrderType",
|
|
76
|
+
"Order",
|
|
77
|
+
"OrderRequest",
|
|
78
|
+
"OrderResponse",
|
|
79
|
+
"OrderStatus",
|
|
80
|
+
"NewOrder",
|
|
81
|
+
"OrderUpdate",
|
|
82
|
+
"OrderSubscriptionConfig",
|
|
83
|
+
"WaitTimeoutError",
|
|
84
|
+
"PreflightRequest",
|
|
85
|
+
"PreflightMultiLegRequest",
|
|
86
|
+
"PreflightMultiLegResponse",
|
|
87
|
+
"MultilegOrderRequest",
|
|
88
|
+
"MultilegOrderResult",
|
|
89
|
+
"LegInstrument",
|
|
90
|
+
"LegInstrumentType",
|
|
91
|
+
"OpenCloseIndicator",
|
|
92
|
+
"OrderLegRequest",
|
|
93
|
+
"OptionGreeks",
|
|
94
|
+
"Portfolio",
|
|
95
|
+
"TimeInForce",
|
|
96
|
+
"Quote",
|
|
97
|
+
"QuoteOutcome",
|
|
98
|
+
"PriceChange",
|
|
99
|
+
"PriceChangeCallback",
|
|
100
|
+
"SubscriptionConfig",
|
|
101
|
+
"SubscriptionStatus",
|
|
102
|
+
"Subscription",
|
|
103
|
+
"SubscriptionInfo",
|
|
104
|
+
"PublicApiClient",
|
|
105
|
+
"PublicApiClientConfiguration",
|
|
106
|
+
"PriceSubscriptionManager",
|
|
107
|
+
"PriceStream",
|
|
108
|
+
"APIError",
|
|
109
|
+
"AuthenticationError",
|
|
110
|
+
"RateLimitError",
|
|
111
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from requests.adapters import HTTPAdapter, BaseAdapter
|
|
7
|
+
from urllib3.util.retry import Retry
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
APIError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServerError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BlockHTTPAdapter(BaseAdapter):
|
|
20
|
+
def send(self, request, **kwargs):
|
|
21
|
+
raise RuntimeError("Insecure HTTP requests are not allowed. Use HTTPS endpoints only.")
|
|
22
|
+
|
|
23
|
+
def close(self):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ApiClient:
|
|
28
|
+
"""HTTP client with error handling and retry logic."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
base_url: str,
|
|
33
|
+
timeout: int = 30,
|
|
34
|
+
max_retries: int = 3,
|
|
35
|
+
backoff_factor: float = 0.3,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize the base client.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
base_url: Base URL for the API
|
|
41
|
+
timeout: Request timeout in seconds
|
|
42
|
+
max_retries: Maximum number of retries for failed requests
|
|
43
|
+
backoff_factor: Backoff factor for retry delays
|
|
44
|
+
"""
|
|
45
|
+
self.base_url = base_url.rstrip("/")
|
|
46
|
+
self.timeout = timeout
|
|
47
|
+
|
|
48
|
+
# create session with retry strategy
|
|
49
|
+
self.session = requests.Session()
|
|
50
|
+
retry_strategy = Retry(
|
|
51
|
+
total=max_retries,
|
|
52
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
53
|
+
backoff_factor=backoff_factor,
|
|
54
|
+
allowed_methods=["HEAD", "GET", "OPTIONS"],
|
|
55
|
+
)
|
|
56
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
57
|
+
self.session.mount("http://", BlockHTTPAdapter())
|
|
58
|
+
self.session.mount("https://", adapter)
|
|
59
|
+
|
|
60
|
+
# default headers
|
|
61
|
+
version = self._get_version()
|
|
62
|
+
self.session.headers.update(
|
|
63
|
+
{
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"User-Agent": f"public-python-api-sdk-{version}",
|
|
66
|
+
"X-App-Version": f"public-python-api-sdk-{version}",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _get_version(self) -> str:
|
|
71
|
+
"""Get the package version."""
|
|
72
|
+
try:
|
|
73
|
+
# Import here to avoid circular import during module initialization
|
|
74
|
+
from . import __version__
|
|
75
|
+
return __version__
|
|
76
|
+
except (ImportError, AttributeError):
|
|
77
|
+
# Fallback if version is not available
|
|
78
|
+
return "0.1.0"
|
|
79
|
+
|
|
80
|
+
def set_auth_header(self, token: str) -> None:
|
|
81
|
+
"""Set the `Authorization` header with a bearer token."""
|
|
82
|
+
self.session.headers["Authorization"] = f"Bearer {token}"
|
|
83
|
+
|
|
84
|
+
def remove_auth_header(self) -> None:
|
|
85
|
+
"""Remove the Authorization header."""
|
|
86
|
+
self.session.headers.pop("Authorization", None)
|
|
87
|
+
|
|
88
|
+
def _build_url(self, endpoint: str) -> str:
|
|
89
|
+
"""Build full URL from endpoint."""
|
|
90
|
+
return urljoin(self.base_url + "/", endpoint.lstrip("/"))
|
|
91
|
+
|
|
92
|
+
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
93
|
+
"""Handle HTTP response and raise appropriate exceptions."""
|
|
94
|
+
try:
|
|
95
|
+
response_data = response.json() if response.content else {}
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
response_data = {"raw_content": response.text}
|
|
98
|
+
|
|
99
|
+
if response.status_code == 200:
|
|
100
|
+
return response_data
|
|
101
|
+
|
|
102
|
+
# extract error message from response
|
|
103
|
+
error_message = response_data.get("message", "Unknown error")
|
|
104
|
+
if isinstance(error_message, dict):
|
|
105
|
+
error_message = str(error_message)
|
|
106
|
+
|
|
107
|
+
# raise specific exceptions based on status code
|
|
108
|
+
if response.status_code == 401:
|
|
109
|
+
raise AuthenticationError(
|
|
110
|
+
error_message, response.status_code, response_data
|
|
111
|
+
)
|
|
112
|
+
elif response.status_code == 400:
|
|
113
|
+
raise ValidationError(error_message, response.status_code, response_data)
|
|
114
|
+
elif response.status_code == 404:
|
|
115
|
+
raise NotFoundError(error_message, response.status_code, response_data)
|
|
116
|
+
elif response.status_code == 429:
|
|
117
|
+
retry_after = response.headers.get("Retry-After")
|
|
118
|
+
retry_after_int = int(retry_after) if retry_after else None
|
|
119
|
+
raise RateLimitError(
|
|
120
|
+
error_message, response.status_code, retry_after_int, response_data
|
|
121
|
+
)
|
|
122
|
+
elif 500 <= response.status_code < 600:
|
|
123
|
+
raise ServerError(error_message, response.status_code, response_data)
|
|
124
|
+
else:
|
|
125
|
+
raise APIError(error_message, response.status_code, response_data)
|
|
126
|
+
|
|
127
|
+
def get(
|
|
128
|
+
self,
|
|
129
|
+
endpoint: str,
|
|
130
|
+
params: Optional[Dict[str, Any]] = None,
|
|
131
|
+
**kwargs: Any,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
url = self._build_url(endpoint)
|
|
134
|
+
response = self.session.get(url, params=params, timeout=self.timeout, **kwargs)
|
|
135
|
+
return self._handle_response(response)
|
|
136
|
+
|
|
137
|
+
def post(
|
|
138
|
+
self,
|
|
139
|
+
endpoint: str,
|
|
140
|
+
data: Optional[Dict[str, Any]] = None,
|
|
141
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
142
|
+
**kwargs: Any,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
url = self._build_url(endpoint)
|
|
145
|
+
response = self.session.post(
|
|
146
|
+
url,
|
|
147
|
+
data=data,
|
|
148
|
+
json=json_data,
|
|
149
|
+
timeout=self.timeout,
|
|
150
|
+
**kwargs,
|
|
151
|
+
)
|
|
152
|
+
return self._handle_response(response)
|
|
153
|
+
|
|
154
|
+
def put(
|
|
155
|
+
self,
|
|
156
|
+
endpoint: str,
|
|
157
|
+
data: Optional[Dict[str, Any]] = None,
|
|
158
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
159
|
+
**kwargs: Any,
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
url = self._build_url(endpoint)
|
|
162
|
+
response = self.session.put(
|
|
163
|
+
url,
|
|
164
|
+
data=data,
|
|
165
|
+
json=json_data,
|
|
166
|
+
timeout=self.timeout,
|
|
167
|
+
**kwargs,
|
|
168
|
+
)
|
|
169
|
+
return self._handle_response(response)
|
|
170
|
+
|
|
171
|
+
def delete(
|
|
172
|
+
self,
|
|
173
|
+
endpoint: str,
|
|
174
|
+
**kwargs: Any,
|
|
175
|
+
) -> Dict[str, Any]:
|
|
176
|
+
url = self._build_url(endpoint)
|
|
177
|
+
response = self.session.delete(url, timeout=self.timeout, **kwargs)
|
|
178
|
+
return self._handle_response(response)
|
|
179
|
+
|
|
180
|
+
def close(self) -> None:
|
|
181
|
+
self.session.close()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from typing import Optional, Protocol, TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from .auth_provider import ApiKeyAuthProvider, OAuthAuthProvider
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .api_client import ApiClient
|
|
7
|
+
from .auth_provider import AuthProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthConfig(Protocol): # pylint: disable=too-few-public-methods
|
|
11
|
+
"""Protocol for authentication configuration."""
|
|
12
|
+
|
|
13
|
+
def create_provider(self, api_client: "ApiClient") -> "AuthProvider":
|
|
14
|
+
"""Create an auth provider instance with the given API client.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
api_client: API client for making HTTP requests
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Configured auth provider instance
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ApiKeyAuthConfig: # pylint: disable=too-few-public-methods
|
|
25
|
+
"""Configuration for API key authentication."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, api_secret_key: str, validity_minutes: int = 15) -> None:
|
|
28
|
+
"""Initialize API key auth configuration.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
api_secret_key: API secret key generated in the Public API settings page (secret)
|
|
32
|
+
validity_minutes: Token validity in minutes (5-1440)
|
|
33
|
+
"""
|
|
34
|
+
if not 5 <= validity_minutes <= 1440:
|
|
35
|
+
raise ValueError("Validity must be between 5 and 1440 minutes")
|
|
36
|
+
|
|
37
|
+
self.api_secret_key = api_secret_key
|
|
38
|
+
self.validity_minutes = validity_minutes
|
|
39
|
+
|
|
40
|
+
def create_provider(self, api_client: "ApiClient") -> "AuthProvider":
|
|
41
|
+
return ApiKeyAuthProvider(
|
|
42
|
+
api_client=api_client,
|
|
43
|
+
api_secret_key=self.api_secret_key,
|
|
44
|
+
validity_minutes=self.validity_minutes,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class OAuthAuthConfig: # pylint: disable=too-few-public-methods
|
|
49
|
+
"""Configuration for OAuth2 authentication."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
client_id: str,
|
|
54
|
+
redirect_uri: str,
|
|
55
|
+
client_secret: Optional[str] = None,
|
|
56
|
+
scope: Optional[str] = None,
|
|
57
|
+
use_pkce: bool = True,
|
|
58
|
+
authorization_base_url: str = "/userapiauthservice/oauth2/authorize",
|
|
59
|
+
token_url: str = "/userapiauthservice/oauth2/token",
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Initialize OAuth auth configuration.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
client_id: OAuth client ID
|
|
65
|
+
redirect_uri: Redirect URI for OAuth flow
|
|
66
|
+
client_secret: OAuth client secret (optional for public clients)
|
|
67
|
+
scope: Space-separated list of scopes
|
|
68
|
+
use_pkce: Whether to use PKCE for enhanced security
|
|
69
|
+
authorization_base_url: Authorization endpoint path
|
|
70
|
+
token_url: Token exchange endpoint path
|
|
71
|
+
"""
|
|
72
|
+
self.client_id = client_id
|
|
73
|
+
self.client_secret = client_secret
|
|
74
|
+
self.redirect_uri = redirect_uri
|
|
75
|
+
self.scope = scope
|
|
76
|
+
self.use_pkce = use_pkce
|
|
77
|
+
self.authorization_base_url = authorization_base_url
|
|
78
|
+
self.token_url = token_url
|
|
79
|
+
|
|
80
|
+
def create_provider(self, api_client: "ApiClient") -> "AuthProvider":
|
|
81
|
+
return OAuthAuthProvider(
|
|
82
|
+
api_client=api_client,
|
|
83
|
+
client_id=self.client_id,
|
|
84
|
+
client_secret=self.client_secret,
|
|
85
|
+
redirect_uri=self.redirect_uri,
|
|
86
|
+
scope=self.scope,
|
|
87
|
+
use_pkce=self.use_pkce,
|
|
88
|
+
authorization_base_url=self.authorization_base_url,
|
|
89
|
+
token_url=self.token_url,
|
|
90
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Authentication manager for managing access tokens"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .auth_provider import AuthProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthManager:
|
|
10
|
+
"""Authentication manager that delegates to auth providers"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, auth_provider: "AuthProvider") -> None:
|
|
13
|
+
"""Initialize the authentication manager.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
auth_provider: Authentication provider (already initialized with ApiClient)
|
|
17
|
+
"""
|
|
18
|
+
super().__init__()
|
|
19
|
+
|
|
20
|
+
self.auth_provider = auth_provider
|
|
21
|
+
self.initialize_auth()
|
|
22
|
+
|
|
23
|
+
def initialize_auth(self) -> None:
|
|
24
|
+
"""Initialize authentication by getting the first token."""
|
|
25
|
+
try:
|
|
26
|
+
# try to get an access token (will create one for API key auth)
|
|
27
|
+
self.auth_provider.get_access_token()
|
|
28
|
+
except ValueError:
|
|
29
|
+
# for oauth, user needs to complete the flow first
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def refresh_token_if_needed(self) -> None:
|
|
33
|
+
"""Refresh the access token if it's expired or about to expire."""
|
|
34
|
+
self.auth_provider.refresh_if_needed()
|
|
35
|
+
|
|
36
|
+
def revoke_current_token(self) -> None:
|
|
37
|
+
"""Revoke the current access token and clear it from memory."""
|
|
38
|
+
self.auth_provider.revoke_token()
|