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.
@@ -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()