earningscall 1.0.2__py3-none-any.whl → 1.1.1__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.
earningscall/__init__.py CHANGED
@@ -1,9 +1,10 @@
1
- from typing import Optional
1
+ from typing import Dict, Optional, Union
2
2
 
3
3
  from earningscall.exports import get_company, get_all_companies, get_sp500_companies
4
4
  from earningscall.symbols import Symbols, load_symbols
5
5
 
6
6
  api_key: Optional[str] = None
7
7
  enable_requests_cache: bool = True
8
+ retry_strategy: Optional[Dict[str, Union[str, int, float]]] = None
8
9
 
9
10
  __all__ = ["get_company", "get_all_companies", "get_sp500_companies", "Symbols", "load_symbols"]
earningscall/api.py CHANGED
@@ -3,18 +3,25 @@ import platform
3
3
  import urllib.parse
4
4
  import logging
5
5
  import os
6
+ import time
6
7
  from importlib.metadata import PackageNotFoundError
7
- from typing import Optional
8
+ from typing import Dict, Optional, Union
8
9
 
9
10
  import requests
10
11
  from requests_cache import CachedSession
11
12
 
12
13
  import earningscall
14
+ from earningscall.errors import InvalidApiKeyError
13
15
 
14
16
  log = logging.getLogger(__file__)
15
17
 
16
- DOMAIN = os.environ.get("ECALL_DOMAIN", "earningscall.biz")
18
+ DOMAIN = os.environ.get("EARNINGSCALL_DOMAIN", "earningscall.biz")
17
19
  API_BASE = f"https://v2.api.{DOMAIN}"
20
+ DEFAULT_RETRY_STRATEGY: Dict[str, Union[str, int, float]] = {
21
+ "strategy": "exponential",
22
+ "base_delay": 1,
23
+ "max_attempts": 10,
24
+ }
18
25
 
19
26
 
20
27
  def get_api_key():
@@ -81,13 +88,27 @@ def get_headers():
81
88
  }
82
89
 
83
90
 
91
+ def can_retry(response: requests.Response) -> bool:
92
+ if response.status_code == 429:
93
+ return True
94
+ # Check for 5XX errors
95
+ if response.status_code >= 500 and response.status_code < 600:
96
+ return True
97
+ return False
98
+
99
+
100
+ def is_success(response: requests.Response) -> bool:
101
+ # TODO: Do we need to check for 2xx status codes?
102
+ return response.status_code == 200
103
+
104
+
84
105
  def do_get(
85
106
  path: str,
86
107
  use_cache: bool = False,
87
108
  **kwargs,
88
109
  ) -> requests.Response:
89
110
  """
90
- Do a GET request to the API.
111
+ Do a GET request to the API with exponential backoff retry for rate limits.
91
112
 
92
113
  Args:
93
114
  path (str): The path to request.
@@ -105,15 +126,47 @@ def do_get(
105
126
  if log.isEnabledFor(logging.DEBUG):
106
127
  full_url = f"{url}?{urllib.parse.urlencode(params)}"
107
128
  log.debug(f"GET: {full_url}")
108
- if use_cache and earningscall.enable_requests_cache:
109
- return cache_session().get(url, params=params)
110
- else:
111
- return requests.get(
112
- url,
113
- params=params,
114
- headers=get_headers(),
115
- stream=kwargs.get("stream"),
116
- )
129
+
130
+ retry_strategy = earningscall.retry_strategy or DEFAULT_RETRY_STRATEGY
131
+ delay = retry_strategy["base_delay"]
132
+ max_attempts = int(retry_strategy["max_attempts"])
133
+
134
+ for attempt in range(max_attempts):
135
+ if use_cache and earningscall.enable_requests_cache:
136
+ response = cache_session().get(url, params=params)
137
+ else:
138
+ response = requests.get(
139
+ url,
140
+ params=params,
141
+ headers=get_headers(),
142
+ stream=kwargs.get("stream"),
143
+ )
144
+
145
+ if is_success(response):
146
+ return response
147
+
148
+ if response.status_code == 401:
149
+ raise InvalidApiKeyError(
150
+ "Your API key is invalid. You can get your API key at: https://earningscall.biz/api-key"
151
+ )
152
+
153
+ if not can_retry(response):
154
+ return response
155
+
156
+ if attempt < max_attempts - 1: # Don't sleep after the last attempt
157
+ if retry_strategy["strategy"] == "exponential":
158
+ wait_time = delay * (2**attempt) # Exponential backoff: 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s
159
+ elif retry_strategy["strategy"] == "linear":
160
+ wait_time = delay * (attempt + 1) # Linear backoff: 1s -> 2s -> 3s -> 4s -> 5s -> 6s -> 7s
161
+ else:
162
+ raise ValueError("Invalid retry strategy. Must be one of: 'exponential', 'linear'")
163
+ # TODO: Should we log a warning here? Does the customer want to see this log?
164
+ log.warning(
165
+ f"Rate limited (429). Retrying in {wait_time} seconds... (Attempt {attempt + 1}/{max_attempts})"
166
+ )
167
+ time.sleep(wait_time)
168
+
169
+ return response # Return the last response if all retries failed
117
170
 
118
171
 
119
172
  def get_events(exchange: str, symbol: str):
earningscall/errors.py CHANGED
@@ -22,3 +22,7 @@ class ClientError(BaseError):
22
22
 
23
23
  class InsufficientApiAccessError(ClientError):
24
24
  pass
25
+
26
+
27
+ class InvalidApiKeyError(ClientError):
28
+ pass
earningscall/symbols.py CHANGED
@@ -8,7 +8,17 @@ from earningscall.errors import InsufficientApiAccessError
8
8
  from earningscall.sectors import sector_to_index, industry_to_index, index_to_sector, index_to_industry
9
9
 
10
10
  # WARNING: Add new indexes to the *END* of this list
11
- EXCHANGES_IN_ORDER = ["NYSE", "NASDAQ", "AMEX", "TSX", "TSXV", "OTC", "LSE", "CBOE", "STO"]
11
+ EXCHANGES_IN_ORDER = [
12
+ "NYSE",
13
+ "NASDAQ",
14
+ "AMEX",
15
+ "TSX",
16
+ "TSXV",
17
+ "OTC",
18
+ "LSE",
19
+ "CBOE",
20
+ "STO",
21
+ ]
12
22
 
13
23
  log = logging.getLogger(__file__)
14
24
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: earningscall
3
- Version: 1.0.2
3
+ Version: 1.1.1
4
4
  Summary: The EarningsCall Python library provides convenient access to the EarningsCall API. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses.
5
5
  Project-URL: Homepage, https://earningscall.biz
6
6
  Project-URL: Documentation, https://github.com/EarningsCall/earningscall-python
@@ -32,7 +32,7 @@ License: MIT License
32
32
  SOFTWARE.
33
33
  License-File: LICENSE
34
34
  Keywords: earning call app,earnings call,earnings call api,earnings call app,earnings call transcript api,earnings call transcripts,earnings call transcripts api,earnings calls,earnings transcript api,listen to earnings calls,transcripts,where to listen to earnings calls
35
- Classifier: Development Status :: 3 - Alpha
35
+ Classifier: Development Status :: 5 - Production/Stable
36
36
  Classifier: Intended Audience :: Developers
37
37
  Classifier: License :: OSI Approved :: MIT License
38
38
  Classifier: Programming Language :: Python :: 3
@@ -41,6 +41,7 @@ Classifier: Programming Language :: Python :: 3.9
41
41
  Classifier: Programming Language :: Python :: 3.10
42
42
  Classifier: Programming Language :: Python :: 3.11
43
43
  Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Programming Language :: Python :: 3.13
44
45
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
45
46
  Classifier: Typing :: Typed
46
47
  Requires-Python: >=3.8
@@ -55,6 +56,7 @@ Description-Content-Type: text/markdown
55
56
  [![Build Status](https://github.com/EarningsCall/earningscall-python/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/EarningsCall/earningscall-python/actions?query=branch%3Amaster)
56
57
  [![Coverage Status](https://coveralls.io/repos/github/EarningsCall/earningscall-python/badge.svg?branch=master)](https://coveralls.io/github/EarningsCall/earningscall-python?branch=master)
57
58
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/earningscall?color=blue)](https://pypi.org/project/earningscall/)
59
+ [![GitHub Stars](https://img.shields.io/github/stars/EarningsCall/earningscall-python.svg?style=social&label=Star)](https://github.com/EarningsCall/earningscall-python)
58
60
 
59
61
  [![Python](https://img.shields.io/badge/Python-14354C?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/)
60
62
 
@@ -310,3 +312,60 @@ import earningscall
310
312
 
311
313
  earningscall.enable_requests_cache = False
312
314
  ```
315
+
316
+ ### Retry Strategy
317
+
318
+ The library implements a flexible retry strategy to handle rate limiting and HTTP 5xx errors effectively. By default, it retries with increasing delays: 3 seconds, 6 seconds, 12 seconds, 24 seconds, and finally 48 seconds. If the request fails after five attempts, the library raises an exception.
319
+
320
+ #### Customizing the Retry Strategy
321
+
322
+ Depending on your specific requirements, you can adjust the retry strategy. For latency-sensitive applications, consider reducing the base delay and limiting the number of retry attempts. Conversely, for plans with lower rate limits, such as the "Starter" plan, a higher base delay with more retry attempts can improve reliability. For higher-rate-limit plans, such as "Enterprise," a shorter delay and fewer attempts may be more appropriate.
323
+
324
+ To customize the retry behavior, set the `retry_strategy` variable with the desired parameters:
325
+
326
+ - **strategy**: "exponential" | "linear" — defines the type of retry strategy (default is "exponential").
327
+ - **base_delay**: float (in seconds) — specifies the delay between retries (default is 1 seconds).
328
+ - **max_attempts**: int — sets the maximum number of total request attempts (default is 10).
329
+
330
+ #### Default Retry Strategy
331
+
332
+ Below is the default retry configuration:
333
+
334
+ ```python
335
+ import earningscall
336
+
337
+ earningscall.retry_strategy = {
338
+ "strategy": "exponential",
339
+ "base_delay": 1,
340
+ "max_attempts": 10,
341
+ }
342
+ ```
343
+
344
+ #### Disabling Retries
345
+
346
+ To disable retries entirely and limit the request to a single attempt, set `max_attempts` to `1`:
347
+
348
+ ```python
349
+ import earningscall
350
+
351
+ earningscall.retry_strategy = {
352
+ "strategy": "exponential",
353
+ "base_delay": 1,
354
+ "max_attempts": 1,
355
+ }
356
+ ```
357
+
358
+ #### Linear Retry Strategy
359
+
360
+ You can switch to a linear retry strategy by setting the `strategy` parameter to "linear":
361
+
362
+ ```python
363
+ import earningscall
364
+
365
+ earningscall.retry_strategy = {
366
+ "strategy": "linear",
367
+ "base_delay": 1,
368
+ "max_attempts": 3,
369
+ }
370
+ ```
371
+
@@ -0,0 +1,14 @@
1
+ earningscall/__init__.py,sha256=6yPTdUeBrKo4sVVW_hJHduTlQUDTw-30BwV5-2PlzYM,413
2
+ earningscall/api.py,sha256=honHHVfJISlWrikNYNE3CjXAkKJWqCa0Jk4q2cg9v1U,7633
3
+ earningscall/company.py,sha256=Ie3LwW5GjXsy3_it5F25JjHfbU3pW8Zefhpv3IjIk4U,6609
4
+ earningscall/errors.py,sha256=aLgwrrpMmYThYEZjCGOhqS57a-GoC0xj2BdbtJ20sy8,490
5
+ earningscall/event.py,sha256=Jf7KPvpeaF9KkeHe46LbL_HIYLXkyHrs3psq-ZY-bkI,692
6
+ earningscall/exports.py,sha256=YAo3vyX3PTgpKBFYwovVy-9797THrvMrdXWqLEHMtME,1425
7
+ earningscall/sectors.py,sha256=Xd6DLkAQ_fQkC2s-N9pReC8b_M3iy77OoFftoZj9FWY,5114
8
+ earningscall/symbols.py,sha256=NxabgKfZZI1YwDLUwh_MlNgyfkR9VZdcU-LqkGWwi28,6521
9
+ earningscall/transcript.py,sha256=P-CeTYhE5T78SXDHFEJ0AlVUFz2XPxDMtkeiorziBiw,1007
10
+ earningscall/utils.py,sha256=Qx8KhlumUdzyBSZRKMS6vpWlb8MGZpLKA4OffJaMdCE,1032
11
+ earningscall-1.1.1.dist-info/METADATA,sha256=3clQWAU4LnVOmlHgW4YMCXIXlxv_49niY0hZtdqjsRE,15446
12
+ earningscall-1.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ earningscall-1.1.1.dist-info/licenses/LICENSE,sha256=ktEB_UcRMg2cQlX9wiDs544xWncWizwS9mEZuGsCLrM,1069
14
+ earningscall-1.1.1.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- earningscall/__init__.py,sha256=0mANmPlE7LEWtOGzV2cmmlPfBIWBWlWRDkyqPHJ1jm8,333
2
- earningscall/api.py,sha256=SNa07842eWWvVWI6wYGXP9TRDV_0tDE4g0lV_AwnQAs,5508
3
- earningscall/company.py,sha256=Ie3LwW5GjXsy3_it5F25JjHfbU3pW8Zefhpv3IjIk4U,6609
4
- earningscall/errors.py,sha256=EA-d6qIYgQs9csp8JptQiAaYoM0M9HhCGJgKA9GAWPg,440
5
- earningscall/event.py,sha256=Jf7KPvpeaF9KkeHe46LbL_HIYLXkyHrs3psq-ZY-bkI,692
6
- earningscall/exports.py,sha256=YAo3vyX3PTgpKBFYwovVy-9797THrvMrdXWqLEHMtME,1425
7
- earningscall/sectors.py,sha256=Xd6DLkAQ_fQkC2s-N9pReC8b_M3iy77OoFftoZj9FWY,5114
8
- earningscall/symbols.py,sha256=842DO8zfglTvPKb_6QkQwAhIAnxqy_zasijBcY-ctm4,6482
9
- earningscall/transcript.py,sha256=P-CeTYhE5T78SXDHFEJ0AlVUFz2XPxDMtkeiorziBiw,1007
10
- earningscall/utils.py,sha256=Qx8KhlumUdzyBSZRKMS6vpWlb8MGZpLKA4OffJaMdCE,1032
11
- earningscall-1.0.2.dist-info/METADATA,sha256=jXMRbhV7u7vM42arV7ySlEvx1Ms6DbdIoNr-0GjP6lg,13265
12
- earningscall-1.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
- earningscall-1.0.2.dist-info/licenses/LICENSE,sha256=ktEB_UcRMg2cQlX9wiDs544xWncWizwS9mEZuGsCLrM,1069
14
- earningscall-1.0.2.dist-info/RECORD,,