earningscall 1.0.2__tar.gz → 1.1.1__tar.gz

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 (67) hide show
  1. {earningscall-1.0.2 → earningscall-1.1.1}/.gitignore +4 -1
  2. earningscall-1.1.1/.python-version +1 -0
  3. {earningscall-1.0.2 → earningscall-1.1.1}/CHANGELOG.md +11 -0
  4. {earningscall-1.0.2 → earningscall-1.1.1}/DEVELOPMENT.md +13 -1
  5. {earningscall-1.0.2 → earningscall-1.1.1}/PKG-INFO +61 -2
  6. {earningscall-1.0.2 → earningscall-1.1.1}/README.md +58 -0
  7. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/__init__.py +2 -1
  8. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/api.py +65 -12
  9. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/errors.py +4 -0
  10. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/symbols.py +11 -1
  11. {earningscall-1.0.2 → earningscall-1.1.1}/pyproject.toml +49 -55
  12. {earningscall-1.0.2 → earningscall-1.1.1}/scripts/get_all_company_transcripts.py +4 -1
  13. earningscall-1.1.1/scripts/get_all_sp500_transcript_texts.py +44 -0
  14. {earningscall-1.0.2 → earningscall-1.1.1}/scripts/get_single_transcript.py +2 -3
  15. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_download_audio_files.py +7 -3
  16. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_get_transcript.py +113 -3
  17. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_symbols.py +15 -1
  18. earningscall-1.0.2/.python-version +0 -1
  19. {earningscall-1.0.2 → earningscall-1.1.1}/.github/workflows/release.yml +0 -0
  20. {earningscall-1.0.2 → earningscall-1.1.1}/.github/workflows/test.yml +0 -0
  21. {earningscall-1.0.2 → earningscall-1.1.1}/LICENSE +0 -0
  22. {earningscall-1.0.2 → earningscall-1.1.1}/TODO.md +0 -0
  23. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/company.py +0 -0
  24. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/event.py +0 -0
  25. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/exports.py +0 -0
  26. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/sectors.py +0 -0
  27. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/transcript.py +0 -0
  28. {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/utils.py +0 -0
  29. {earningscall-1.0.2 → earningscall-1.1.1}/hatch.toml +0 -0
  30. {earningscall-1.0.2 → earningscall-1.1.1}/scripts/download_audio_files.py +0 -0
  31. {earningscall-1.0.2 → earningscall-1.1.1}/scripts/download_single_audio_file.py +0 -0
  32. {earningscall-1.0.2 → earningscall-1.1.1}/scripts/download_sp500_audio_files.py +0 -0
  33. {earningscall-1.0.2 → earningscall-1.1.1}/scripts/list_companies.py +0 -0
  34. {earningscall-1.0.2 → earningscall-1.1.1}/setup.cfg +0 -0
  35. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-2.yaml +0 -0
  36. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-3.yaml +0 -0
  37. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-4.yaml +0 -0
  38. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-speaker-name-map-v2.yaml +0 -0
  39. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-authorized-l2.yaml +0 -0
  40. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-authorized.yaml +0 -0
  41. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-found.yaml +0 -0
  42. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2030-server-error.yaml +0 -0
  43. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/demo-symbols-v2-alpha.yaml +0 -0
  44. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/demo-symbols-v2.yaml +0 -0
  45. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/meta-q3-2024-not-authorized.yaml +0 -0
  46. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/meta-q3-2024-not-found.yaml +0 -0
  47. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/meta-q3-2024-other-error.yaml +0 -0
  48. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/msft-company-events.yaml +0 -0
  49. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/msft-q1-2022-audio-file-short-clip.yaml +0 -0
  50. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/msft-transcript-response.yaml +0 -0
  51. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/sp500-company-list-failed.yaml +0 -0
  52. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/sp500-company-list.yaml +0 -0
  53. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/symbols-v2-missing-edge-cases.yaml +0 -0
  54. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/symbols-v2.yaml +0 -0
  55. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/symbols.txt +0 -0
  56. {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/symbols.yaml +0 -0
  57. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_api.py +0 -0
  58. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_company.py +0 -0
  59. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_earnings_event.py +0 -0
  60. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_errors.py +0 -0
  61. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_exports.py +0 -0
  62. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_get_company_events.py +0 -0
  63. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_get_sp500_companies_api.py +0 -0
  64. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_helper.py +0 -0
  65. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_responses_mocking.py +0 -0
  66. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_sectors.py +0 -0
  67. {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_utils.py +0 -0
@@ -36,4 +36,7 @@ tasks.xml
36
36
 
37
37
  # Root files
38
38
  /.coverage*
39
- /.mypy_cache/
39
+ /.mypy_cache/
40
+
41
+ # Downloaded Data from Scripts
42
+ /data
@@ -0,0 +1 @@
1
+ 3.12.8
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## Release `1.1.1` - 2025-01-26
4
+
5
+ * Modify default retry strategy to use 1s base delay and 10 max attempts (necessary for starter plan).
6
+ * Check for HTTP 401 Unauthorized status code from server and raise a helpful error message to the user.
7
+ * Update documentation and example scripts to reflect new retry configuration
8
+
9
+ ## Release `1.1.0` - 2025-01-26
10
+
11
+ * Add backoff and retry logic to all API calls.
12
+ * Add tests for rate limiting.
13
+
3
14
  ## Release `1.0.2` - 2024-12-24
4
15
 
5
16
  * Add STO exchange to the list of exchanges.
@@ -13,7 +13,7 @@ First, install Hatch plus other build dependencies. See the Hatch [installation
13
13
  My preferred way to install it is to use `pip`:
14
14
 
15
15
  ```shell
16
- pip install hatch coverage black
16
+ pip install hatch hatch-containers coverage black responses
17
17
  ```
18
18
 
19
19
  ## Run Build
@@ -41,6 +41,18 @@ Or, generate HTML report locally:
41
41
  coverage html
42
42
  ```
43
43
 
44
+ ### Run Container Tests
45
+
46
+ The container tests are run in GitHub Actions. Normally, you don't need to run them locally.
47
+
48
+ The container tests run all unit tests in a containerized environment.
49
+
50
+ Each environment is a different Python version.
51
+
52
+ ```shell
53
+ hatch run all:test
54
+ ```
55
+
44
56
  ### Run Linter
45
57
 
46
58
  ```shell
@@ -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
+
@@ -4,6 +4,7 @@
4
4
  [![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)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/EarningsCall/earningscall-python/badge.svg?branch=master)](https://coveralls.io/github/EarningsCall/earningscall-python?branch=master)
6
6
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/earningscall?color=blue)](https://pypi.org/project/earningscall/)
7
+ [![GitHub Stars](https://img.shields.io/github/stars/EarningsCall/earningscall-python.svg?style=social&label=Star)](https://github.com/EarningsCall/earningscall-python)
7
8
 
8
9
  [![Python](https://img.shields.io/badge/Python-14354C?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/)
9
10
 
@@ -259,3 +260,60 @@ import earningscall
259
260
 
260
261
  earningscall.enable_requests_cache = False
261
262
  ```
263
+
264
+ ### Retry Strategy
265
+
266
+ 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.
267
+
268
+ #### Customizing the Retry Strategy
269
+
270
+ 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.
271
+
272
+ To customize the retry behavior, set the `retry_strategy` variable with the desired parameters:
273
+
274
+ - **strategy**: "exponential" | "linear" — defines the type of retry strategy (default is "exponential").
275
+ - **base_delay**: float (in seconds) — specifies the delay between retries (default is 1 seconds).
276
+ - **max_attempts**: int — sets the maximum number of total request attempts (default is 10).
277
+
278
+ #### Default Retry Strategy
279
+
280
+ Below is the default retry configuration:
281
+
282
+ ```python
283
+ import earningscall
284
+
285
+ earningscall.retry_strategy = {
286
+ "strategy": "exponential",
287
+ "base_delay": 1,
288
+ "max_attempts": 10,
289
+ }
290
+ ```
291
+
292
+ #### Disabling Retries
293
+
294
+ To disable retries entirely and limit the request to a single attempt, set `max_attempts` to `1`:
295
+
296
+ ```python
297
+ import earningscall
298
+
299
+ earningscall.retry_strategy = {
300
+ "strategy": "exponential",
301
+ "base_delay": 1,
302
+ "max_attempts": 1,
303
+ }
304
+ ```
305
+
306
+ #### Linear Retry Strategy
307
+
308
+ You can switch to a linear retry strategy by setting the `strategy` parameter to "linear":
309
+
310
+ ```python
311
+ import earningscall
312
+
313
+ earningscall.retry_strategy = {
314
+ "strategy": "linear",
315
+ "base_delay": 1,
316
+ "max_attempts": 3,
317
+ }
318
+ ```
319
+
@@ -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"]
@@ -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):
@@ -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
@@ -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,54 +1,53 @@
1
1
  [project]
2
2
  name = "earningscall"
3
- version = "1.0.2"
3
+ version = "1.1.1"
4
4
  description = "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
  readme = "README.md"
6
- authors = [
7
- {name = "EarningsCall", email = "dev@earningscall.biz"},
8
- ]
6
+ authors = [{ name = "EarningsCall", email = "dev@earningscall.biz" }]
9
7
  requires-python = ">= 3.8"
10
8
  dependencies = [
11
- "dataclasses-json>=0.6.4",
12
- "requests>=2.30.0",
13
- "requests-cache>=1.2.0",
9
+ "dataclasses-json>=0.6.4",
10
+ "requests>=2.30.0",
11
+ "requests-cache>=1.2.0",
14
12
  ]
15
13
  license = { file = "LICENSE" }
16
14
  keywords = [
17
- "earnings calls",
18
- "earnings call",
19
- "earnings call api",
20
- "earnings call transcripts",
21
- "earnings call transcripts api",
22
- "earnings call transcript api",
23
- "earnings call app",
24
- "earning call app",
25
- "listen to earnings calls",
26
- "where to listen to earnings calls",
27
- "earnings transcript api",
28
- "transcripts",
15
+ "earnings calls",
16
+ "earnings call",
17
+ "earnings call api",
18
+ "earnings call transcripts",
19
+ "earnings call transcripts api",
20
+ "earnings call transcript api",
21
+ "earnings call app",
22
+ "earning call app",
23
+ "listen to earnings calls",
24
+ "where to listen to earnings calls",
25
+ "earnings transcript api",
26
+ "transcripts",
29
27
  ]
30
28
  classifiers = [
31
- # How mature is this project? Common values are
32
- # 3 - Alpha
33
- # 4 - Beta
34
- # 5 - Production/Stable
35
- "Development Status :: 3 - Alpha",
36
-
37
- # Indicate who your project is intended for
38
- "Intended Audience :: Developers",
39
- "Topic :: Software Development :: Libraries :: Python Modules",
40
- "Typing :: Typed",
41
-
42
- # Pick your license as you wish (see also "license" above)
43
- "License :: OSI Approved :: MIT License",
44
-
45
- # Specify the Python versions you support here.
46
- "Programming Language :: Python :: 3",
47
- "Programming Language :: Python :: 3.8",
48
- "Programming Language :: Python :: 3.9",
49
- "Programming Language :: Python :: 3.10",
50
- "Programming Language :: Python :: 3.11",
51
- "Programming Language :: Python :: 3.12",
29
+ # How mature is this project? Common values are
30
+ # 3 - Alpha
31
+ # 4 - Beta
32
+ # 5 - Production/Stable
33
+ "Development Status :: 5 - Production/Stable",
34
+
35
+ # Indicate who your project is intended for
36
+ "Intended Audience :: Developers",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ "Typing :: Typed",
39
+
40
+ # Pick your license as you wish (see also "license" above)
41
+ "License :: OSI Approved :: MIT License",
42
+
43
+ # Specify the Python versions you support here.
44
+ "Programming Language :: Python :: 3",
45
+ "Programming Language :: Python :: 3.8",
46
+ "Programming Language :: Python :: 3.9",
47
+ "Programming Language :: Python :: 3.10",
48
+ "Programming Language :: Python :: 3.11",
49
+ "Programming Language :: Python :: 3.12",
50
+ "Programming Language :: Python :: 3.13",
52
51
  ]
53
52
 
54
53
  [project.urls]
@@ -75,25 +74,20 @@ packages = ["earningscall"]
75
74
  path = "hatch_init/__about__.py"
76
75
 
77
76
  [tool.hatch.envs.default]
78
- dependencies = [
79
- "pytest",
80
- "pytest-cov",
81
- ]
77
+ dependencies = ["pytest", "pytest-cov", "responses"]
82
78
  [tool.hatch.envs.default.scripts]
83
79
  cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=hatch_init --cov=tests"
84
80
  no-cov = "cov --no-cov"
85
81
 
86
82
  [[tool.hatch.envs.test.matrix]]
87
- python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
83
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
88
84
 
89
85
 
90
86
  [tool.hatch.build.targets.wheel.hooks.mypyc]
91
87
  enable-by-default = false
92
88
  dependencies = ["hatch-mypyc>=0.14.1"]
93
89
  require-runtime-dependencies = true
94
- mypy-args = [
95
- "--no-warn-unused-ignores",
96
- ]
90
+ mypy-args = ["--no-warn-unused-ignores"]
97
91
 
98
92
  [tool.mypy]
99
93
  disallow_untyped_defs = false
@@ -121,10 +115,14 @@ ignore = [
121
115
  "FBT003",
122
116
  # Ignore checks for possible passwords
123
117
  # Ignore complexity
124
- "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915",
118
+ "C901",
119
+ "PLR0911",
120
+ "PLR0912",
121
+ "PLR0913",
122
+ "PLR0915",
125
123
  "PLC1901", # empty string comparisons
126
124
  "PLW2901", # `for` loop variable overwritten
127
- "SIM114", # Combine `if` branches using logical `or` operator
125
+ "SIM114", # Combine `if` branches using logical `or` operator
128
126
  ]
129
127
  unfixable = [
130
128
  # Don't touch unused imports
@@ -156,11 +154,7 @@ earningscall = ["earningscall"]
156
154
  tests = ["tests"]
157
155
 
158
156
  [tool.coverage.report]
159
- exclude_lines = [
160
- "no cov",
161
- "if __name__ == .__main__.:",
162
- "if TYPE_CHECKING:",
163
- ]
157
+ exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
164
158
 
165
159
  [tool.pytest.ini_options]
166
160
  log_level = "DEBUG"
@@ -1,7 +1,10 @@
1
+ import earningscall # noqa: F401
1
2
  from earningscall import get_company
2
3
 
4
+ # earningscall.api_key = "YOUR API KEY HERE"
3
5
 
4
- company = get_company("aapl") # Lookup Apple, Inc by its ticker symbol, "AAPL"
6
+
7
+ company = get_company("AAPL") # Lookup Apple, Inc by its ticker symbol, "AAPL"
5
8
 
6
9
  print(f"Getting all transcripts for: {company}..")
7
10
 
@@ -0,0 +1,44 @@
1
+ from datetime import datetime
2
+ import os
3
+
4
+ import earningscall # noqa: F401
5
+ from earningscall import get_sp500_companies
6
+ from earningscall.company import Company
7
+
8
+ # TODO: Set your API key here:
9
+ # earningscall.api_key = "YOUR SECRET API KEY GOES HERE"
10
+
11
+ directory = "data/transcript_texts"
12
+ os.makedirs(directory, exist_ok=True)
13
+
14
+
15
+ def download_transcript_texts(company: Company):
16
+ print(f"Downloading all transcript texts for: {company}..")
17
+ for event in company.events():
18
+ if datetime.now().timestamp() < event.conference_date.timestamp():
19
+ print(
20
+ f"* {company.company_info.symbol} Q{event.quarter} {event.year} -- skipping, conference date in the future"
21
+ )
22
+ continue
23
+ file_name = os.path.join(
24
+ directory,
25
+ f"{company.company_info.exchange}_{company.company_info.symbol}_{event.year}_Q{event.quarter}.text",
26
+ )
27
+ if os.path.exists(file_name):
28
+ print(f"* {company.company_info.symbol} Q{event.quarter} {event.year} -- already downloaded")
29
+ else:
30
+ print(f"* Downloading transcript text for {company.company_info.symbol} Q{event.quarter} {event.year}...")
31
+ transcript = company.get_transcript(event=event)
32
+ if transcript:
33
+ # Save transcript text to file
34
+ with open(file_name, "w") as fd:
35
+ fd.write(transcript.text)
36
+ print(
37
+ f" Downloaded transcript text for {company.company_info.symbol} Q{event.quarter} {event.year} to: {file_name}"
38
+ )
39
+ else:
40
+ print(f" No transcript text found for {company.company_info.symbol} Q{event.quarter} {event.year}")
41
+
42
+
43
+ for company in get_sp500_companies():
44
+ download_transcript_texts(company)
@@ -4,10 +4,9 @@ from earningscall import get_company
4
4
 
5
5
 
6
6
  # TODO: Set your API key here:
7
- # earningscall.api_key = "YOUR SECRET API KEY GOES HERE"
7
+ # earningscall.api_key = "YOUR API KEY HERE"
8
8
 
9
-
10
- company = get_company("aapl")
9
+ company = get_company("AAPL")
11
10
 
12
11
  transcript = company.get_transcript(year=2021, quarter=3, level=2)
13
12
 
@@ -1,11 +1,10 @@
1
1
  import os
2
2
 
3
- from requests import HTTPError
4
-
5
- import earningscall
6
3
  import pytest
7
4
  import responses
5
+ from requests import HTTPError
8
6
 
7
+ import earningscall
9
8
  from earningscall import get_company
10
9
  from earningscall.api import purge_cache
11
10
  from earningscall.company import Company
@@ -28,6 +27,11 @@ def run_before_and_after_tests():
28
27
  """Fixture to execute asserts before and after a test is run"""
29
28
  # Setup: fill with any logic you want
30
29
  earningscall.api_key = None
30
+ earningscall.retry_strategy = {
31
+ "strategy": "exponential",
32
+ "base_delay": 0.001,
33
+ "max_attempts": 3,
34
+ }
31
35
  purge_cache()
32
36
  clear_symbols()
33
37
  yield # this is where the testing happens
@@ -1,13 +1,14 @@
1
- from requests import HTTPError
1
+ import json
2
2
 
3
- import earningscall
4
3
  import pytest
5
4
  import responses
5
+ from requests import HTTPError
6
6
 
7
+ import earningscall
7
8
  from earningscall import get_company
8
9
  from earningscall.api import purge_cache
9
10
  from earningscall.company import Company
10
- from earningscall.errors import InsufficientApiAccessError
11
+ from earningscall.errors import InsufficientApiAccessError, InvalidApiKeyError
11
12
  from earningscall.event import EarningsEvent
12
13
  from earningscall.symbols import clear_symbols, CompanyInfo
13
14
  from earningscall.transcript import Transcript
@@ -27,6 +28,11 @@ def run_before_and_after_tests():
27
28
  """Fixture to execute asserts before and after a test is run"""
28
29
  # Setup: fill with any logic you want
29
30
  earningscall.api_key = None
31
+ earningscall.retry_strategy = {
32
+ "strategy": "exponential",
33
+ "base_delay": 0.01,
34
+ "max_attempts": 3,
35
+ }
30
36
  purge_cache()
31
37
  clear_symbols()
32
38
  yield # this is where the testing happens
@@ -276,6 +282,110 @@ def test_data_class_for_transcript():
276
282
  assert transcript.speaker_name_map_v2["spk01"].title == "CEO"
277
283
 
278
284
 
285
+ @responses.activate
286
+ def test_get_transcript_with_rate_limit_backoff_and_retry():
287
+ ##
288
+ responses._add_from_file(file_path=data_path("symbols-v2.yaml"))
289
+ # First response
290
+ responses.add(
291
+ responses.GET,
292
+ "https://v2.api.earningscall.biz/transcript?apikey=demo&exchange=NASDAQ&symbol=AAPL&year=2023&quarter=1&level=1",
293
+ body=json.dumps({"error": "Rate limit exceeded"}),
294
+ status=429,
295
+ )
296
+ # Second response
297
+ responses.add(
298
+ responses.GET,
299
+ "https://v2.api.earningscall.biz/transcript?apikey=demo&exchange=NASDAQ&symbol=AAPL&year=2023&quarter=1&level=1",
300
+ body=json.dumps({"text": "Hello, world!"}),
301
+ status=200,
302
+ )
303
+ ##
304
+ company = get_company("aapl")
305
+ ##
306
+ transcript = company.get_transcript(year=2023, quarter=1, level=1)
307
+ # ##
308
+ assert transcript.text == "Hello, world!"
309
+
310
+
311
+ @responses.activate
312
+ def test_get_transcript_fails_all_attempts():
313
+ ##
314
+ responses._add_from_file(file_path=data_path("symbols-v2.yaml"))
315
+ # Always throttle the caller
316
+ responses.add(
317
+ responses.GET,
318
+ "https://v2.api.earningscall.biz/transcript?apikey=demo&exchange=NASDAQ&symbol=AAPL&year=2023&quarter=1&level=1",
319
+ body=json.dumps({"error": "Rate limit exceeded"}),
320
+ status=429,
321
+ )
322
+ ##
323
+ company = get_company("aapl")
324
+ ##
325
+ with pytest.raises(HTTPError):
326
+ company.get_transcript(year=2023, quarter=1, level=1)
327
+
328
+
329
+ @responses.activate
330
+ def test_get_transcript_fails_all_attempts_linear_retry_strategy():
331
+ earningscall.retry_strategy = {
332
+ "strategy": "linear",
333
+ "base_delay": 0.01,
334
+ "max_attempts": 3,
335
+ }
336
+ ##
337
+ responses._add_from_file(file_path=data_path("symbols-v2.yaml"))
338
+ # Always throttle the caller
339
+ responses.add(
340
+ responses.GET,
341
+ "https://v2.api.earningscall.biz/transcript?apikey=demo&exchange=NASDAQ&symbol=AAPL&year=2023&quarter=1&level=1",
342
+ body=json.dumps({"error": "Rate limit exceeded"}),
343
+ status=429,
344
+ )
345
+ ##
346
+ company = get_company("aapl")
347
+ ##
348
+ with pytest.raises(HTTPError):
349
+ company.get_transcript(year=2023, quarter=1, level=1)
350
+
351
+
352
+ @responses.activate
353
+ def test_get_transcript_fails_all_attempts_invalid_retry_strategy():
354
+ earningscall.retry_strategy = {
355
+ "strategy": "invalid",
356
+ "base_delay": 0.01,
357
+ "max_attempts": 3,
358
+ }
359
+ ##
360
+ responses._add_from_file(file_path=data_path("symbols-v2.yaml"))
361
+ # Always throttle the caller
362
+ responses.add(
363
+ responses.GET,
364
+ "https://v2.api.earningscall.biz/transcript?apikey=demo&exchange=NASDAQ&symbol=AAPL&year=2023&quarter=1&level=1",
365
+ body=json.dumps({"error": "Rate limit exceeded"}),
366
+ status=429,
367
+ )
368
+ ##
369
+ company = get_company("aapl")
370
+ ##
371
+ with pytest.raises(ValueError):
372
+ company.get_transcript(year=2023, quarter=1, level=1)
373
+
374
+
375
+ @responses.activate
376
+ def test_get_company_fails_not_authorized():
377
+ # Always throttle the caller
378
+ responses.add(
379
+ responses.GET,
380
+ "https://v2.api.earningscall.biz/symbols-v2.txt",
381
+ body=json.dumps({"error": "Not authorized"}),
382
+ status=401,
383
+ )
384
+ ##
385
+ with pytest.raises(InvalidApiKeyError):
386
+ get_company("aapl")
387
+
388
+
279
389
  # Uncomment and run following code to generate demo-symbols-v2.yaml file
280
390
  #
281
391
  # import requests
@@ -1,7 +1,7 @@
1
- import earningscall
2
1
  import pytest
3
2
  import responses
4
3
 
4
+ import earningscall
5
5
  from earningscall.api import API_BASE, purge_cache
6
6
  from earningscall.symbols import Symbols, CompanyInfo
7
7
  from earningscall.utils import data_path
@@ -127,3 +127,17 @@ def test_symbols_serialization_v1():
127
127
  assert _deserialized_symbols.get_exchange_symbol("TSX_TLRY").industry == "Uranium"
128
128
  assert _deserialized_symbols.get_exchange_symbol("TSX_ACB").name == "Aurora Cannabis Inc."
129
129
  assert _deserialized_symbols.get_exchange_symbol("NASDAQ_HITI").name == "High Tide Inc."
130
+
131
+
132
+ def test_exchanges_in_order():
133
+ assert earningscall.symbols.EXCHANGES_IN_ORDER == [
134
+ "NYSE",
135
+ "NASDAQ",
136
+ "AMEX",
137
+ "TSX",
138
+ "TSXV",
139
+ "OTC",
140
+ "LSE",
141
+ "CBOE",
142
+ "STO",
143
+ ]
@@ -1 +0,0 @@
1
- 3.12.6
File without changes
File without changes
File without changes
File without changes