alpaca-py-nopandas 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.
Files changed (62) hide show
  1. alpaca/__init__.py +2 -0
  2. alpaca/broker/__init__.py +8 -0
  3. alpaca/broker/client.py +2360 -0
  4. alpaca/broker/enums.py +528 -0
  5. alpaca/broker/models/__init__.py +7 -0
  6. alpaca/broker/models/accounts.py +347 -0
  7. alpaca/broker/models/cip.py +265 -0
  8. alpaca/broker/models/documents.py +159 -0
  9. alpaca/broker/models/funding.py +114 -0
  10. alpaca/broker/models/journals.py +71 -0
  11. alpaca/broker/models/rebalancing.py +80 -0
  12. alpaca/broker/models/trading.py +13 -0
  13. alpaca/broker/requests.py +1135 -0
  14. alpaca/common/__init__.py +6 -0
  15. alpaca/common/constants.py +13 -0
  16. alpaca/common/enums.py +64 -0
  17. alpaca/common/exceptions.py +47 -0
  18. alpaca/common/models.py +21 -0
  19. alpaca/common/requests.py +82 -0
  20. alpaca/common/rest.py +438 -0
  21. alpaca/common/types.py +7 -0
  22. alpaca/common/utils.py +89 -0
  23. alpaca/data/__init__.py +5 -0
  24. alpaca/data/enums.py +184 -0
  25. alpaca/data/historical/__init__.py +13 -0
  26. alpaca/data/historical/corporate_actions.py +76 -0
  27. alpaca/data/historical/crypto.py +299 -0
  28. alpaca/data/historical/news.py +63 -0
  29. alpaca/data/historical/option.py +230 -0
  30. alpaca/data/historical/screener.py +72 -0
  31. alpaca/data/historical/stock.py +226 -0
  32. alpaca/data/historical/utils.py +30 -0
  33. alpaca/data/live/__init__.py +11 -0
  34. alpaca/data/live/crypto.py +168 -0
  35. alpaca/data/live/news.py +62 -0
  36. alpaca/data/live/option.py +88 -0
  37. alpaca/data/live/stock.py +199 -0
  38. alpaca/data/live/websocket.py +390 -0
  39. alpaca/data/mappings.py +84 -0
  40. alpaca/data/models/__init__.py +7 -0
  41. alpaca/data/models/bars.py +83 -0
  42. alpaca/data/models/base.py +45 -0
  43. alpaca/data/models/corporate_actions.py +309 -0
  44. alpaca/data/models/news.py +90 -0
  45. alpaca/data/models/orderbooks.py +59 -0
  46. alpaca/data/models/quotes.py +78 -0
  47. alpaca/data/models/screener.py +68 -0
  48. alpaca/data/models/snapshots.py +132 -0
  49. alpaca/data/models/trades.py +204 -0
  50. alpaca/data/requests.py +580 -0
  51. alpaca/data/timeframe.py +148 -0
  52. alpaca/py.typed +0 -0
  53. alpaca/trading/__init__.py +5 -0
  54. alpaca/trading/client.py +784 -0
  55. alpaca/trading/enums.py +412 -0
  56. alpaca/trading/models.py +697 -0
  57. alpaca/trading/requests.py +604 -0
  58. alpaca/trading/stream.py +225 -0
  59. alpaca_py_nopandas-0.1.0.dist-info/LICENSE +201 -0
  60. alpaca_py_nopandas-0.1.0.dist-info/METADATA +299 -0
  61. alpaca_py_nopandas-0.1.0.dist-info/RECORD +62 -0
  62. alpaca_py_nopandas-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,6 @@
1
+ from alpaca.common.models import *
2
+ from alpaca.common.enums import *
3
+ from alpaca.common.constants import *
4
+ from alpaca.common.exceptions import *
5
+ from alpaca.common.types import *
6
+ from alpaca.common.utils import *
@@ -0,0 +1,13 @@
1
+ from typing import TypeVar
2
+
3
+ DATA_V2_MAX_LIMIT = 10000 # max items per api call
4
+
5
+ ACCOUNT_ACTIVITIES_DEFAULT_PAGE_SIZE = 100
6
+
7
+ BROKER_DOCUMENT_UPLOAD_LIMIT = 10
8
+
9
+ PageItem = TypeVar("PageItem") # Generic type for an item from a paginated request.
10
+
11
+ DEFAULT_RETRY_ATTEMPTS = 3
12
+ DEFAULT_RETRY_WAIT_SECONDS = 3
13
+ DEFAULT_RETRY_EXCEPTION_CODES = [429, 504]
alpaca/common/enums.py ADDED
@@ -0,0 +1,64 @@
1
+ from enum import Enum
2
+
3
+
4
+ class BaseURL(str, Enum):
5
+ """Base urls for API endpoints"""
6
+
7
+ BROKER_SANDBOX = "https://broker-api.sandbox.alpaca.markets"
8
+ BROKER_PRODUCTION = "https://broker-api.alpaca.markets"
9
+ TRADING_PAPER = "https://paper-api.alpaca.markets"
10
+ TRADING_LIVE = "https://api.alpaca.markets"
11
+ DATA = "https://data.alpaca.markets"
12
+ DATA_SANDBOX = "https://data.sandbox.alpaca.markets"
13
+ MARKET_DATA_STREAM = "wss://stream.data.alpaca.markets"
14
+ OPTION_DATA_STREAM = "wss://stream.data.alpaca.markets" # Deprecated: use MARKET_DATA_STREAM instead!
15
+ TRADING_STREAM_PAPER = "wss://paper-api.alpaca.markets/stream"
16
+ TRADING_STREAM_LIVE = "wss://api.alpaca.markets/stream"
17
+
18
+
19
+ class PaginationType(str, Enum):
20
+ """
21
+ An enum for choosing what type of pagination of results you'd like for BrokerClient functions that support
22
+ pagination.
23
+
24
+ Attributes:
25
+ NONE: Requests that we perform no pagination of results and just return the single response the API gave us.
26
+ FULL: Requests that we perform all the pagination and return just a single List/dict/etc containing all the
27
+ results. This is the default for most functions.
28
+ ITERATOR: Requests that we return an Iterator that yields one "page" of results at a time
29
+ """
30
+
31
+ NONE = "none"
32
+ FULL = "full"
33
+ ITERATOR = "iterator"
34
+
35
+
36
+ class Sort(str, Enum):
37
+ ASC = "asc"
38
+ DESC = "desc"
39
+
40
+
41
+ class SupportedCurrencies(str, Enum):
42
+ """
43
+ The various currencies that can be supported for LCT.
44
+
45
+ see https://alpaca.markets/support/local-currency-trading-faq
46
+ """
47
+
48
+ USD = "USD"
49
+ GBP = "GBP"
50
+ CHF = "CHF"
51
+ EUR = "EUR"
52
+ CAD = "CAD"
53
+ JPY = "JPY"
54
+ TRY = "TRY"
55
+ AUD = "AUD"
56
+ CZK = "CZK"
57
+ SEK = "SEK"
58
+ DKK = "DKK"
59
+ SGD = "SGD"
60
+ HKD = "HKD"
61
+ HUF = "HUF"
62
+ NZD = "NZD"
63
+ NOK = "NOK"
64
+ PLN = "PLN"
@@ -0,0 +1,47 @@
1
+ import json
2
+
3
+
4
+ class APIError(Exception):
5
+ """
6
+ Represent API related error.
7
+ error.status_code will have http status code.
8
+ """
9
+
10
+ def __init__(self, error, http_error=None):
11
+ super().__init__(error)
12
+ self._error = error
13
+ self._http_error = http_error
14
+
15
+ @property
16
+ def code(self):
17
+ error = json.loads(self._error)
18
+ return error["code"]
19
+
20
+ @property
21
+ def message(self):
22
+ error = json.loads(self._error)
23
+ return error["message"]
24
+
25
+ @property
26
+ def status_code(self):
27
+ http_error = self._http_error
28
+ if http_error is not None and hasattr(http_error, "response"):
29
+ return http_error.response.status_code
30
+
31
+ @property
32
+ def request(self):
33
+ if self._http_error is not None:
34
+ return self._http_error.request
35
+
36
+ @property
37
+ def response(self):
38
+ if self._http_error is not None:
39
+ return self._http_error.response
40
+
41
+
42
+ class RetryException(Exception):
43
+ """
44
+ Thrown by RESTClient's internally to represent a request that should be retried.
45
+ """
46
+
47
+ pass
@@ -0,0 +1,21 @@
1
+ from uuid import UUID
2
+ from pydantic import BaseModel
3
+ import pprint
4
+
5
+
6
+ class ValidateBaseModel(BaseModel, validate_assignment=True):
7
+ """
8
+ This model simply sets up BaseModel with the validate_assignment flag to True, so we don't have to keep specifying
9
+ it or forget to specify it in our models where we want assignment validation
10
+ """
11
+
12
+ def __repr__(self):
13
+ return pprint.pformat(self.model_dump(), indent=4)
14
+
15
+
16
+ class ModelWithID(ValidateBaseModel):
17
+ """
18
+ This is the base model for response models with IDs that are UUIDs.
19
+ """
20
+
21
+ id: UUID
@@ -0,0 +1,82 @@
1
+ from datetime import date, datetime, timezone
2
+ from ipaddress import IPv4Address, IPv6Address
3
+ from typing import Any
4
+ from uuid import UUID
5
+
6
+ from alpaca.common.models import ValidateBaseModel as BaseModel
7
+
8
+
9
+ class NonEmptyRequest(BaseModel):
10
+ """
11
+ Mixin for models that represent requests where we don't want to send nulls for optional fields.
12
+ """
13
+
14
+ def to_request_fields(self) -> dict:
15
+ """
16
+ the equivalent of self::dict but removes empty values and handles converting non json serializable types.
17
+
18
+ Ie say we only set trusted_contact.given_name instead of generating a dict like:
19
+ {contact: {city: None, country: None...}, etc}
20
+ we generate just:
21
+ {trusted_contact:{given_name: "new value"}}
22
+
23
+ NOTE: This function recurses to handle nested models, so do not use on a self-referential model
24
+
25
+ Returns:
26
+ dict: a dict containing any set fields
27
+ """
28
+
29
+ def map_values(val: Any) -> Any:
30
+ """
31
+ Some types have issues being json encoded, we convert them here to be encodable
32
+
33
+ also handles nested models and lists
34
+ """
35
+
36
+ if isinstance(val, UUID):
37
+ return str(val)
38
+
39
+ if isinstance(val, NonEmptyRequest):
40
+ return val.to_request_fields()
41
+
42
+ if isinstance(val, dict):
43
+ return {k: map_values(v) for k, v in val.items()}
44
+
45
+ if isinstance(val, list):
46
+ return [map_values(v) for v in val]
47
+
48
+ # RFC 3339
49
+ if isinstance(val, datetime):
50
+ # if the datetime is naive, assume it's UTC
51
+ # https://docs.python.org/3/library/datetime.html#determining-if-an-object-is-aware-or-naive
52
+ if val.tzinfo is None or val.tzinfo.utcoffset(val) is None:
53
+ val = val.replace(tzinfo=timezone.utc)
54
+ return val.isoformat()
55
+
56
+ if isinstance(val, date):
57
+ return val.isoformat()
58
+
59
+ if isinstance(val, IPv4Address):
60
+ return str(val)
61
+
62
+ if isinstance(val, IPv6Address):
63
+ return str(val)
64
+
65
+ return val
66
+
67
+ d = self.model_dump(exclude_none=True)
68
+ if "symbol_or_symbols" in d:
69
+ s = d["symbol_or_symbols"]
70
+ if isinstance(s, list):
71
+ s = ",".join(s)
72
+ d["symbols"] = s
73
+ del d["symbol_or_symbols"]
74
+
75
+ # pydantic almost has what we need by passing exclude_none to dict() but it returns:
76
+ # {trusted_contact: {}, contact: {}, identity: None, etc}
77
+ # so we do a simple list comprehension to filter out None and {}
78
+ return {
79
+ key: map_values(val)
80
+ for key, val in d.items()
81
+ if val is not None and val != {} and len(str(val)) > 0
82
+ }
alpaca/common/rest.py ADDED
@@ -0,0 +1,438 @@
1
+ from collections import defaultdict
2
+ from collections.abc import Callable
3
+ import time
4
+ import base64
5
+ from abc import ABC
6
+ from typing import Any, Dict, List, Optional, Type, Union, Tuple, Iterator
7
+
8
+ from pydantic import BaseModel
9
+ from requests import Session
10
+ from requests.exceptions import HTTPError
11
+ from itertools import chain
12
+
13
+ from alpaca.common.constants import (
14
+ DEFAULT_RETRY_ATTEMPTS,
15
+ DEFAULT_RETRY_WAIT_SECONDS,
16
+ DEFAULT_RETRY_EXCEPTION_CODES,
17
+ )
18
+
19
+ from alpaca import __version__
20
+ from alpaca.common.exceptions import APIError, RetryException
21
+ from alpaca.common.types import RawData, HTTPResult, Credentials
22
+ from .constants import PageItem
23
+ from .enums import PaginationType, BaseURL
24
+
25
+
26
+ class RESTClient(ABC):
27
+ """Abstract base class for REST clients"""
28
+
29
+ def __init__(
30
+ self,
31
+ base_url: Union[BaseURL, str],
32
+ api_key: Optional[str] = None,
33
+ secret_key: Optional[str] = None,
34
+ oauth_token: Optional[str] = None,
35
+ use_basic_auth: bool = False,
36
+ api_version: str = "v2",
37
+ sandbox: bool = False,
38
+ raw_data: bool = False,
39
+ retry_attempts: Optional[int] = None,
40
+ retry_wait_seconds: Optional[int] = None,
41
+ retry_exception_codes: Optional[List[int]] = None,
42
+ ) -> None:
43
+ """Abstract base class for REST clients. Handles submitting HTTP requests to
44
+ Alpaca API endpoints.
45
+
46
+ Args:
47
+ base_url (Union[BaseURL, str]): The base url to target requests to. Should be an instance of BaseURL, but
48
+ allows for raw str if you need to override
49
+ api_key (Optional[str]): The api key string for authentication.
50
+ secret_key (Optional[str]): The corresponding secret key string for the api key.
51
+ oauth_token (Optional[str]): The oauth token if authenticating via OAuth.
52
+ use_basic_auth (bool): Whether API requests should use basic authorization headers.
53
+ api_version (Optional[str]): The API version for the endpoints.
54
+ sandbox (bool): False if the live API should be used.
55
+ raw_data (bool): Whether API responses should be wrapped in data models or returned raw.
56
+ retry_attempts (Optional[int]): The number of times to retry a request that returns a RetryException.
57
+ retry_wait_seconds (Optional[int]): The number of seconds to wait between requests before retrying.
58
+ retry_exception_codes (Optional[List[int]]): The API exception codes to retry a request on.
59
+ """
60
+
61
+ self._api_key, self._secret_key, self._oauth_token = self._validate_credentials(
62
+ api_key=api_key, secret_key=secret_key, oauth_token=oauth_token
63
+ )
64
+ self._api_version: str = api_version
65
+ self._base_url: Union[BaseURL, str] = base_url
66
+ self._sandbox: bool = sandbox
67
+ self._use_basic_auth: bool = use_basic_auth
68
+ self._use_raw_data: bool = raw_data
69
+ self._session: Session = Session()
70
+
71
+ # setting up request retry configurations
72
+ self._retry: int = DEFAULT_RETRY_ATTEMPTS
73
+ self._retry_wait: int = DEFAULT_RETRY_WAIT_SECONDS
74
+ self._retry_codes: List[int] = DEFAULT_RETRY_EXCEPTION_CODES
75
+
76
+ if retry_attempts and retry_attempts > 0:
77
+ self._retry = retry_attempts
78
+
79
+ if retry_wait_seconds and retry_wait_seconds > 0:
80
+ self._retry_wait = retry_wait_seconds
81
+
82
+ if retry_exception_codes:
83
+ self._retry_codes = retry_exception_codes
84
+
85
+ def _request(
86
+ self,
87
+ method: str,
88
+ path: str,
89
+ data: Optional[Union[dict, str]] = None,
90
+ base_url: Optional[Union[BaseURL, str]] = None,
91
+ api_version: Optional[str] = None,
92
+ ) -> HTTPResult:
93
+ """Prepares and submits HTTP requests to given API endpoint and returns response.
94
+ Handles retrying if 429 (Rate Limit) error arises.
95
+
96
+ Args:
97
+ method (str): The API endpoint HTTP method
98
+ path (str): The API endpoint path
99
+ data (Optional[Union[dict, str]]): Either the payload in json format, query params urlencoded, or a dict
100
+ of values to be converted to appropriate format based on `method`. Defaults to None.
101
+ base_url (Optional[Union[BaseURL, str]]): The base URL of the API. Defaults to None.
102
+ api_version (Optional[str]): The API version. Defaults to None.
103
+
104
+ Returns:
105
+ HTTPResult: The response from the API
106
+ """
107
+ base_url = base_url or self._base_url
108
+ version = api_version if api_version else self._api_version
109
+ url: str = base_url + "/" + version + path
110
+
111
+ headers = self._get_default_headers()
112
+
113
+ opts = {
114
+ "headers": headers,
115
+ # Since we allow users to set endpoint URL via env var,
116
+ # human error to put non-SSL endpoint could exploit
117
+ # uncanny issues in non-GET request redirecting http->https.
118
+ # It's better to fail early if the URL isn't right.
119
+ "allow_redirects": False,
120
+ }
121
+
122
+ if method.upper() in ["GET", "DELETE"]:
123
+ opts["params"] = data
124
+ else:
125
+ opts["json"] = data
126
+
127
+ retry = self._retry
128
+
129
+ while retry >= 0:
130
+ try:
131
+ return self._one_request(method, url, opts, retry)
132
+ except RetryException:
133
+ time.sleep(self._retry_wait)
134
+ retry -= 1
135
+ continue
136
+
137
+ def _get_default_headers(self) -> dict:
138
+ """
139
+ Returns a dict with some default headers set; ie AUTH headers and such that should be useful on all requests
140
+ Extracted for cases when using the default request functions are insufficient
141
+
142
+ Returns:
143
+ dict: The resulting dict of headers
144
+ """
145
+ headers = self._get_auth_headers()
146
+
147
+ headers["User-Agent"] = "APCA-PY/" + __version__
148
+
149
+ return headers
150
+
151
+ def _get_auth_headers(self) -> dict:
152
+ """
153
+ Get the auth headers for a request. Meant to be overridden in clients that don't use this format for requests,
154
+ ie: BrokerClient
155
+
156
+ Returns:
157
+ dict: A dict containing the expected auth headers
158
+ """
159
+
160
+ headers = {}
161
+
162
+ if self._oauth_token:
163
+ headers["Authorization"] = "Bearer " + self._oauth_token
164
+ elif self._use_basic_auth:
165
+ api_key_secret = "{key}:{secret}".format(
166
+ key=self._api_key, secret=self._secret_key
167
+ ).encode("utf-8")
168
+ encoded_api_key_secret = base64.b64encode(api_key_secret).decode("utf-8")
169
+ headers["Authorization"] = "Basic " + encoded_api_key_secret
170
+ else:
171
+ headers["APCA-API-KEY-ID"] = self._api_key
172
+ headers["APCA-API-SECRET-KEY"] = self._secret_key
173
+
174
+ return headers
175
+
176
+ def _one_request(self, method: str, url: str, opts: dict, retry: int) -> dict:
177
+ """Perform one request, possibly raising RetryException in the case
178
+ the response is 429. Otherwise, if error text contain "code" string,
179
+ then it decodes to json object and returns APIError.
180
+ Returns the body json in the 200 status.
181
+
182
+ Args:
183
+ method (str): The HTTP method - GET, POST, etc
184
+ url (str): The API endpoint URL
185
+ opts (dict): Contains optional parameters including headers and parameters
186
+ retry (int): The number of times to retry in case of RetryException
187
+
188
+ Raises:
189
+ RetryException: Raised if request produces 429 error and retry limit has not been reached
190
+ APIError: Raised if API returns an error
191
+
192
+ Returns:
193
+ dict: The response data
194
+ """
195
+ response = self._session.request(method, url, **opts)
196
+
197
+ try:
198
+ response.raise_for_status()
199
+ except HTTPError as http_error:
200
+ # retry if we hit Rate Limit
201
+ if response.status_code in self._retry_codes and retry > 0:
202
+ raise RetryException()
203
+
204
+ # raise API error for all other errors
205
+ error = response.text
206
+
207
+ raise APIError(error, http_error)
208
+
209
+ if response.text != "":
210
+ return response.json()
211
+
212
+ def get(
213
+ self, path: str, data: Optional[Union[dict, str]] = None, **kwargs
214
+ ) -> HTTPResult:
215
+ """Performs a single GET request
216
+
217
+ Args:
218
+ path (str): The API endpoint path
219
+ data (Union[dict, str], optional): Query parameters to send, either
220
+ as a str urlencoded, or a dict of values to be converted. Defaults to None.
221
+
222
+ Returns:
223
+ dict: The response
224
+ """
225
+ return self._request("GET", path, data, **kwargs)
226
+
227
+ def post(
228
+ self, path: str, data: Optional[Union[dict, List[dict]]] = None
229
+ ) -> HTTPResult:
230
+ """Performs a single POST request
231
+
232
+ Args:
233
+ path (str): The API endpoint path
234
+ data (Optional[Union[dict, List[dict]]): The json payload as a dict of values to be converted.
235
+ Defaults to None.
236
+
237
+ Returns:
238
+ dict: The response
239
+ """
240
+ return self._request("POST", path, data)
241
+
242
+ def put(self, path: str, data: Optional[dict] = None) -> dict:
243
+ """Performs a single PUT request
244
+
245
+ Args:
246
+ path (str): The API endpoint path
247
+ data (Optional[dict]): The json payload as a dict of values to be converted.
248
+ Defaults to None.
249
+
250
+ Returns:
251
+ dict: The response
252
+ """
253
+ return self._request("PUT", path, data)
254
+
255
+ def patch(self, path: str, data: Optional[dict] = None) -> dict:
256
+ """Performs a single PATCH request
257
+
258
+ Args:
259
+ path (str): The API endpoint path
260
+ data (Optional[dict]): The json payload as a dict of values to be converted.
261
+ Defaults to None.
262
+
263
+ Returns:
264
+ dict: The response
265
+ """
266
+ return self._request("PATCH", path, data)
267
+
268
+ def delete(self, path, data: Optional[Union[dict, str]] = None) -> dict:
269
+ """Performs a single DELETE request
270
+
271
+ Args:
272
+ path (str): The API endpoint path
273
+ data (Union[dict, str], optional): The payload if any. Defaults to None.
274
+
275
+ Returns:
276
+ dict: The response
277
+ """
278
+ return self._request("DELETE", path, data)
279
+
280
+ # TODO: Refactor to be able to handle both parsing to types and parsing to collections of types (parse_as_obj)
281
+ def response_wrapper(
282
+ self, model: Type[BaseModel], raw_data: RawData, **kwargs
283
+ ) -> Union[BaseModel, RawData]:
284
+ """To allow the user to get raw response from the api, we wrap all
285
+ functions with this method, checking if the user has set raw_data
286
+ bool. if they didn't, we wrap the response with a BaseModel object.
287
+
288
+ Args:
289
+ model (Type[BaseModel]): Class that response will be wrapped in
290
+ raw_data (RawData): The raw data from API in dictionary
291
+ kwargs : Any constructor parameters necessary for the base model
292
+
293
+ Returns:
294
+ Union[BaseModel, RawData]: either raw or parsed data
295
+ """
296
+ if self._use_raw_data:
297
+ return raw_data
298
+ else:
299
+ return model(raw_data=raw_data, **kwargs)
300
+
301
+ @staticmethod
302
+ def _validate_pagination(
303
+ max_items_limit: Optional[int], handle_pagination: Optional[PaginationType]
304
+ ) -> PaginationType:
305
+ """
306
+ Private method for validating the max_items_limit and handle_pagination arguments, returning the resolved
307
+ PaginationType.
308
+ """
309
+ if handle_pagination is None:
310
+ handle_pagination = PaginationType.FULL
311
+
312
+ if handle_pagination != PaginationType.FULL and max_items_limit is not None:
313
+ raise ValueError(
314
+ "max_items_limit can only be specified for PaginationType.FULL"
315
+ )
316
+ return handle_pagination
317
+
318
+ @staticmethod
319
+ def _return_paginated_result(
320
+ iterator: Iterator[PageItem], handle_pagination: PaginationType
321
+ ) -> Union[List[PageItem], Iterator[List[PageItem]]]:
322
+ """
323
+ Private method for converting an iterator that yields results to the proper pagination type result.
324
+ """
325
+ if handle_pagination == PaginationType.NONE:
326
+ # user wants no pagination, so just do a single page
327
+ return next(iterator)
328
+ elif handle_pagination == PaginationType.FULL:
329
+ # the iterator returns "pages", so we use chain to flatten them all into 1 list
330
+ return list(chain.from_iterable(iterator))
331
+ elif handle_pagination == PaginationType.ITERATOR:
332
+ return iterator
333
+ else:
334
+ raise ValueError(f"Invalid pagination type: {handle_pagination}.")
335
+
336
+ @staticmethod
337
+ def _validate_credentials(
338
+ api_key: Optional[str] = None,
339
+ secret_key: Optional[str] = None,
340
+ oauth_token: Optional[str] = None,
341
+ ) -> Credentials:
342
+ """Gathers API credentials from parameters and environment variables, and validates them.
343
+ Args:
344
+ api_key (Optional[str]): The API key for authentication. Defaults to None.
345
+ secret_key (Optional[str]): The secret key for authentication. Defaults to None.
346
+ oauth_token (Optional[str]): The oauth token if authenticating via OAuth. Defaults to None.
347
+ Raises:
348
+ ValueError: If the combination of keys and tokens provided are not valid.
349
+ Returns:
350
+ Credentials: The set of validated authentication keys
351
+ """
352
+
353
+ if not oauth_token and not api_key:
354
+ raise ValueError("You must supply a method of authentication")
355
+
356
+ if oauth_token and (api_key or secret_key):
357
+ raise ValueError(
358
+ "Either an oauth_token or an api_key may be supplied, but not both"
359
+ )
360
+
361
+ if not oauth_token and not (api_key and secret_key):
362
+ raise ValueError(
363
+ "A corresponding secret_key must be supplied with the api_key"
364
+ )
365
+
366
+ return api_key, secret_key, oauth_token
367
+
368
+ def _get_marketdata(
369
+ self,
370
+ path: str,
371
+ params: Dict[str, Any],
372
+ page_limit: int = 10_000,
373
+ page_size: Optional[int] = None,
374
+ no_sub_key: bool = False,
375
+ ) -> Dict[str, List[Any]]:
376
+ d = defaultdict(list)
377
+ # Missing page_size indicates that we should not set a limit (e.g. for latest endpoints)
378
+ actual_limit = min(page_size, page_limit) if page_size else None
379
+ limit = params.get("limit")
380
+ total_items = 0
381
+ page_token = params.get("page_token")
382
+
383
+ while True:
384
+ # adjusts the limit parameter value if it is over the page_limit
385
+ if limit:
386
+ # actual_limit is the adjusted total number of items to query per request
387
+ actual_limit = min(int(limit) - total_items, page_limit)
388
+ if actual_limit < 1:
389
+ break
390
+ params["limit"] = actual_limit
391
+ params["page_token"] = page_token
392
+
393
+ response = self.get(path=path, data=params)
394
+
395
+ for k, v in _get_marketdata_entries(response, no_sub_key).items():
396
+ if isinstance(v, list):
397
+ d[k].extend(v)
398
+ else:
399
+ d[k] = v
400
+
401
+ # if we've sent a request with a limit, increment count
402
+ if actual_limit:
403
+ total_items = sum([len(items) for items in d.values()])
404
+
405
+ page_token = response.get("next_page_token", None)
406
+ if page_token is None:
407
+ break
408
+ return dict(d)
409
+
410
+
411
+ def _get_marketdata_entries(response: HTTPResult, no_sub_key: bool) -> RawData:
412
+ if no_sub_key:
413
+ return response
414
+
415
+ data_keys = {
416
+ "bar",
417
+ "bars",
418
+ "corporate_actions",
419
+ "news",
420
+ "orderbook",
421
+ "orderbooks",
422
+ "quote",
423
+ "quotes",
424
+ "snapshot",
425
+ "snapshots",
426
+ "trade",
427
+ "trades",
428
+ }
429
+ selected_keys = data_keys.intersection(response)
430
+ # Neither of these should ever happen!
431
+ if selected_keys is None or len(selected_keys) < 1:
432
+ raise ValueError("The data in response does not match any known keys.")
433
+ if len(selected_keys) > 1:
434
+ raise ValueError("The data in response matches multiple known keys.")
435
+ selected_key = selected_keys.pop()
436
+ if selected_key == "news":
437
+ return {"news": response[selected_key]}
438
+ return response[selected_key]
alpaca/common/types.py ADDED
@@ -0,0 +1,7 @@
1
+ from typing import Any, Dict, List, Optional, Tuple, Union
2
+
3
+ RawData = Dict[str, Any]
4
+
5
+ # TODO: Refine this type
6
+ HTTPResult = Union[dict, List[dict], Any]
7
+ Credentials = Tuple[Optional[str], Optional[str], Optional[str]]