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.
- {earningscall-1.0.2 → earningscall-1.1.1}/.gitignore +4 -1
- earningscall-1.1.1/.python-version +1 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/CHANGELOG.md +11 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/DEVELOPMENT.md +13 -1
- {earningscall-1.0.2 → earningscall-1.1.1}/PKG-INFO +61 -2
- {earningscall-1.0.2 → earningscall-1.1.1}/README.md +58 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/__init__.py +2 -1
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/api.py +65 -12
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/errors.py +4 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/symbols.py +11 -1
- {earningscall-1.0.2 → earningscall-1.1.1}/pyproject.toml +49 -55
- {earningscall-1.0.2 → earningscall-1.1.1}/scripts/get_all_company_transcripts.py +4 -1
- earningscall-1.1.1/scripts/get_all_sp500_transcript_texts.py +44 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/scripts/get_single_transcript.py +2 -3
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_download_audio_files.py +7 -3
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_get_transcript.py +113 -3
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_symbols.py +15 -1
- earningscall-1.0.2/.python-version +0 -1
- {earningscall-1.0.2 → earningscall-1.1.1}/.github/workflows/release.yml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/.github/workflows/test.yml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/LICENSE +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/TODO.md +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/company.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/event.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/exports.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/sectors.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/transcript.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/earningscall/utils.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/hatch.toml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/scripts/download_audio_files.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/scripts/download_single_audio_file.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/scripts/download_sp500_audio_files.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/scripts/list_companies.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/setup.cfg +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-2.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-3.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-4.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-speaker-name-map-v2.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-authorized-l2.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-authorized.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-found.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2030-server-error.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/demo-symbols-v2-alpha.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/demo-symbols-v2.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/meta-q3-2024-not-authorized.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/meta-q3-2024-not-found.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/meta-q3-2024-other-error.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/msft-company-events.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/msft-q1-2022-audio-file-short-clip.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/msft-transcript-response.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/sp500-company-list-failed.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/sp500-company-list.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/symbols-v2-missing-edge-cases.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/symbols-v2.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/symbols.txt +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/data/symbols.yaml +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_api.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_company.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_earnings_event.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_errors.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_exports.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_get_company_events.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_get_sp500_companies_api.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_helper.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_responses_mocking.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_sectors.py +0 -0
- {earningscall-1.0.2 → earningscall-1.1.1}/tests/test_utils.py +0 -0
@@ -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.
|
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 ::
|
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
|
[](https://github.com/EarningsCall/earningscall-python/actions?query=branch%3Amaster)
|
56
57
|
[](https://coveralls.io/github/EarningsCall/earningscall-python?branch=master)
|
57
58
|
[](https://pypi.org/project/earningscall/)
|
59
|
+
[](https://github.com/EarningsCall/earningscall-python)
|
58
60
|
|
59
61
|
[](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
|
[](https://github.com/EarningsCall/earningscall-python/actions?query=branch%3Amaster)
|
5
5
|
[](https://coveralls.io/github/EarningsCall/earningscall-python?branch=master)
|
6
6
|
[](https://pypi.org/project/earningscall/)
|
7
|
+
[](https://github.com/EarningsCall/earningscall-python)
|
7
8
|
|
8
9
|
[](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("
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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):
|
@@ -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 = [
|
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.
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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",
|
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",
|
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
|
-
|
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
|
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
|
-
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-2.yaml
RENAMED
File without changes
|
{earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-3.yaml
RENAMED
File without changes
|
{earningscall-1.0.2 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-4.yaml
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{earningscall-1.0.2 → earningscall-1.1.1}/tests/data/msft-q1-2022-audio-file-short-clip.yaml
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|