earningscall 1.1.0__py3-none-any.whl → 1.2.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.
- earningscall/__init__.py +10 -7
- earningscall/api.py +35 -11
- earningscall/calendar.py +30 -0
- earningscall/errors.py +4 -0
- earningscall/exports.py +43 -2
- {earningscall-1.1.0.dist-info → earningscall-1.2.0.dist-info}/METADATA +20 -7
- earningscall-1.2.0.dist-info/RECORD +15 -0
- earningscall-1.1.0.dist-info/RECORD +0 -14
- {earningscall-1.1.0.dist-info → earningscall-1.2.0.dist-info}/WHEEL +0 -0
- {earningscall-1.1.0.dist-info → earningscall-1.2.0.dist-info}/licenses/LICENSE +0 -0
earningscall/__init__.py
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
from typing import Dict, Optional, Union
|
2
2
|
|
3
|
-
from earningscall.exports import get_company, get_all_companies, get_sp500_companies
|
3
|
+
from earningscall.exports import get_company, get_all_companies, get_sp500_companies, get_calendar
|
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: Dict[str, Union[str, int, float]] =
|
9
|
-
"strategy": "exponential",
|
10
|
-
"base_delay": 3,
|
11
|
-
"max_attempts": 5,
|
12
|
-
}
|
8
|
+
retry_strategy: Optional[Dict[str, Union[str, int, float]]] = None
|
13
9
|
|
14
|
-
__all__ = [
|
10
|
+
__all__ = [
|
11
|
+
"get_company",
|
12
|
+
"get_all_companies",
|
13
|
+
"get_sp500_companies",
|
14
|
+
"Symbols",
|
15
|
+
"load_symbols",
|
16
|
+
"get_calendar",
|
17
|
+
]
|
earningscall/api.py
CHANGED
@@ -1,21 +1,27 @@
|
|
1
1
|
import importlib
|
2
|
-
import platform
|
3
|
-
import urllib.parse
|
4
2
|
import logging
|
5
3
|
import os
|
4
|
+
import platform
|
6
5
|
import time
|
6
|
+
import urllib.parse
|
7
7
|
from importlib.metadata import PackageNotFoundError
|
8
|
-
from typing import Optional
|
8
|
+
from typing import Dict, Optional, Union
|
9
9
|
|
10
10
|
import requests
|
11
11
|
from requests_cache import CachedSession
|
12
12
|
|
13
13
|
import earningscall
|
14
|
+
from earningscall.errors import InvalidApiKeyError
|
14
15
|
|
15
16
|
log = logging.getLogger(__file__)
|
16
17
|
|
17
|
-
DOMAIN = os.environ.get("
|
18
|
+
DOMAIN = os.environ.get("EARNINGSCALL_DOMAIN", "earningscall.biz")
|
18
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
|
+
}
|
19
25
|
|
20
26
|
|
21
27
|
def get_api_key():
|
@@ -121,8 +127,9 @@ def do_get(
|
|
121
127
|
full_url = f"{url}?{urllib.parse.urlencode(params)}"
|
122
128
|
log.debug(f"GET: {full_url}")
|
123
129
|
|
124
|
-
|
125
|
-
|
130
|
+
retry_strategy = earningscall.retry_strategy or DEFAULT_RETRY_STRATEGY
|
131
|
+
delay = retry_strategy["base_delay"]
|
132
|
+
max_attempts = int(retry_strategy["max_attempts"])
|
126
133
|
|
127
134
|
for attempt in range(max_attempts):
|
128
135
|
if use_cache and earningscall.enable_requests_cache:
|
@@ -138,16 +145,22 @@ def do_get(
|
|
138
145
|
if is_success(response):
|
139
146
|
return response
|
140
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
|
+
|
141
153
|
if not can_retry(response):
|
142
154
|
return response
|
143
155
|
|
144
156
|
if attempt < max_attempts - 1: # Don't sleep after the last attempt
|
145
|
-
if
|
146
|
-
wait_time = delay * (2**attempt) # Exponential backoff:
|
147
|
-
elif
|
148
|
-
wait_time = delay * (attempt + 1) # Linear backoff: 3s ->
|
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
|
149
161
|
else:
|
150
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?
|
151
164
|
log.warning(
|
152
165
|
f"Rate limited (429). Retrying in {wait_time} seconds... (Attempt {attempt + 1}/{max_attempts})"
|
153
166
|
)
|
@@ -156,7 +169,18 @@ def do_get(
|
|
156
169
|
return response # Return the last response if all retries failed
|
157
170
|
|
158
171
|
|
159
|
-
def
|
172
|
+
def get_calendar_api_operation(year: int, month: int, day: int) -> dict:
|
173
|
+
params = {
|
174
|
+
"year": str(year),
|
175
|
+
"month": str(month),
|
176
|
+
"day": str(day),
|
177
|
+
}
|
178
|
+
response = do_get("calendar", params=params)
|
179
|
+
response.raise_for_status()
|
180
|
+
return response.json()
|
181
|
+
|
182
|
+
|
183
|
+
def get_events(exchange: str, symbol: str) -> Optional[dict]:
|
160
184
|
params = {
|
161
185
|
"exchange": exchange,
|
162
186
|
"symbol": symbol,
|
earningscall/calendar.py
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from dataclasses_json import config
|
6
|
+
from dataclasses_json import dataclass_json
|
7
|
+
from marshmallow import fields
|
8
|
+
|
9
|
+
|
10
|
+
@dataclass_json
|
11
|
+
@dataclass
|
12
|
+
class CalendarEvent:
|
13
|
+
"""
|
14
|
+
CalendarEvent
|
15
|
+
"""
|
16
|
+
|
17
|
+
company_name: str
|
18
|
+
exchange: str
|
19
|
+
symbol: str
|
20
|
+
year: int
|
21
|
+
quarter: int
|
22
|
+
transcript_ready: bool
|
23
|
+
conference_date: Optional[datetime] = field(
|
24
|
+
default=None,
|
25
|
+
metadata=config(
|
26
|
+
encoder=lambda date: date.isoformat() if date else None,
|
27
|
+
decoder=lambda date: datetime.fromisoformat(date) if date else None,
|
28
|
+
mm_field=fields.DateTime(format="iso"),
|
29
|
+
),
|
30
|
+
)
|
earningscall/errors.py
CHANGED
earningscall/exports.py
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
-
|
1
|
+
import datetime
|
2
|
+
from datetime import date, timedelta
|
3
|
+
from typing import List, Optional, Iterator
|
2
4
|
|
3
|
-
|
5
|
+
import requests
|
6
|
+
|
7
|
+
from earningscall import api
|
8
|
+
from earningscall.api import get_sp500_companies_txt_file, is_demo_account
|
9
|
+
from earningscall.calendar import CalendarEvent
|
4
10
|
from earningscall.company import Company
|
11
|
+
from earningscall.errors import InsufficientApiAccessError
|
5
12
|
from earningscall.symbols import get_symbols
|
6
13
|
|
7
14
|
|
@@ -43,3 +50,37 @@ def get_sp500_companies() -> Iterator[Company]:
|
|
43
50
|
company_info = get_symbols().lookup_company(ticker_symbol)
|
44
51
|
if company_info:
|
45
52
|
yield Company(company_info=company_info)
|
53
|
+
|
54
|
+
|
55
|
+
def get_calendar(input_date: date) -> List[CalendarEvent]:
|
56
|
+
"""
|
57
|
+
Get the earnings event calendar for a given input date.
|
58
|
+
|
59
|
+
:param date input_date: The date to get the calendar for.
|
60
|
+
|
61
|
+
:return: A list of CalendarEvent objects.
|
62
|
+
"""
|
63
|
+
if not input_date:
|
64
|
+
raise ValueError("Date is required")
|
65
|
+
if isinstance(input_date, datetime.datetime):
|
66
|
+
input_date = input_date.date()
|
67
|
+
if not isinstance(input_date, date):
|
68
|
+
raise ValueError("Date must be a date object")
|
69
|
+
if input_date < date(2018, 1, 1):
|
70
|
+
raise ValueError("input_date must be greater than or equal to 2018-01-01")
|
71
|
+
# Check if input_date is greater than 30 days from today
|
72
|
+
if input_date > datetime.datetime.now().date() + timedelta(days=30):
|
73
|
+
raise ValueError("input_date must be less than 30 days from today")
|
74
|
+
if is_demo_account() and input_date != date(2025, 1, 10):
|
75
|
+
raise InsufficientApiAccessError(
|
76
|
+
f"\"{input_date}\" requires an API Key for access. To get your API Key,"
|
77
|
+
f" see: https://earningscall.biz/api-pricing"
|
78
|
+
)
|
79
|
+
try:
|
80
|
+
json_data = api.get_calendar_api_operation(input_date.year, input_date.month, input_date.day)
|
81
|
+
return [CalendarEvent.from_dict(event) for event in json_data] # type: ignore
|
82
|
+
except requests.exceptions.HTTPError as error:
|
83
|
+
if error.response.status_code == 404:
|
84
|
+
# Calendar Date not found, simply return an empty list
|
85
|
+
return []
|
86
|
+
raise error
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: earningscall
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
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
|
@@ -260,6 +260,20 @@ print("Downloading audio file for Apple Inc. Q3 2021...")
|
|
260
260
|
audio_file = company.download_audio_file(year=2021, quarter=3, file_name="Apple Q3 2021.mp3")
|
261
261
|
```
|
262
262
|
|
263
|
+
## Get Earnings Event Calendar
|
264
|
+
|
265
|
+
```python
|
266
|
+
from datetime import date
|
267
|
+
|
268
|
+
from earningscall import get_calendar
|
269
|
+
|
270
|
+
calendar = get_calendar(date(2025, 1, 10))
|
271
|
+
|
272
|
+
for event in calendar:
|
273
|
+
print(f"{event.company_name} - Q{event.quarter} {event.year} on: {event.conference_date.astimezone().isoformat()} Transcript Ready: {event.transcript_ready}")
|
274
|
+
```
|
275
|
+
|
276
|
+
|
263
277
|
## List All Companies
|
264
278
|
|
265
279
|
```python
|
@@ -297,7 +311,6 @@ for company in get_sp500_companies():
|
|
297
311
|
print(f"{company.company_info} -- {company.company_info.sector} -- {company.company_info.industry}")
|
298
312
|
```
|
299
313
|
|
300
|
-
|
301
314
|
## Advanced
|
302
315
|
|
303
316
|
### Disable Caching
|
@@ -324,8 +337,8 @@ Depending on your specific requirements, you can adjust the retry strategy. For
|
|
324
337
|
To customize the retry behavior, set the `retry_strategy` variable with the desired parameters:
|
325
338
|
|
326
339
|
- **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
|
328
|
-
- **max_attempts**: int — sets the maximum number of total request attempts (default is
|
340
|
+
- **base_delay**: float (in seconds) — specifies the delay between retries (default is 1 seconds).
|
341
|
+
- **max_attempts**: int — sets the maximum number of total request attempts (default is 10).
|
329
342
|
|
330
343
|
#### Default Retry Strategy
|
331
344
|
|
@@ -336,8 +349,8 @@ import earningscall
|
|
336
349
|
|
337
350
|
earningscall.retry_strategy = {
|
338
351
|
"strategy": "exponential",
|
339
|
-
"base_delay":
|
340
|
-
"max_attempts":
|
352
|
+
"base_delay": 1,
|
353
|
+
"max_attempts": 10,
|
341
354
|
}
|
342
355
|
```
|
343
356
|
|
@@ -350,7 +363,7 @@ import earningscall
|
|
350
363
|
|
351
364
|
earningscall.retry_strategy = {
|
352
365
|
"strategy": "exponential",
|
353
|
-
"base_delay":
|
366
|
+
"base_delay": 1,
|
354
367
|
"max_attempts": 1,
|
355
368
|
}
|
356
369
|
```
|
@@ -0,0 +1,15 @@
|
|
1
|
+
earningscall/__init__.py,sha256=J1cBpSRDBivNtYDB-LIMNBZu6rQ3-_1eCY6ACFHlHhE,470
|
2
|
+
earningscall/api.py,sha256=zI7XrxC73pJXZX9Qe1te-EKVS3G-0VVz5FBiuFbi-W8,7936
|
3
|
+
earningscall/calendar.py,sha256=nQcb0UsVwCDhvvcr7dqdY0PCqdVGohB7JDON7jtbRyM,725
|
4
|
+
earningscall/company.py,sha256=Ie3LwW5GjXsy3_it5F25JjHfbU3pW8Zefhpv3IjIk4U,6609
|
5
|
+
earningscall/errors.py,sha256=aLgwrrpMmYThYEZjCGOhqS57a-GoC0xj2BdbtJ20sy8,490
|
6
|
+
earningscall/event.py,sha256=Jf7KPvpeaF9KkeHe46LbL_HIYLXkyHrs3psq-ZY-bkI,692
|
7
|
+
earningscall/exports.py,sha256=G9eZqX_QydfS5O039Wa0rl4Si3KrC_pGyBZ_cxfUrtI,3147
|
8
|
+
earningscall/sectors.py,sha256=Xd6DLkAQ_fQkC2s-N9pReC8b_M3iy77OoFftoZj9FWY,5114
|
9
|
+
earningscall/symbols.py,sha256=NxabgKfZZI1YwDLUwh_MlNgyfkR9VZdcU-LqkGWwi28,6521
|
10
|
+
earningscall/transcript.py,sha256=P-CeTYhE5T78SXDHFEJ0AlVUFz2XPxDMtkeiorziBiw,1007
|
11
|
+
earningscall/utils.py,sha256=Qx8KhlumUdzyBSZRKMS6vpWlb8MGZpLKA4OffJaMdCE,1032
|
12
|
+
earningscall-1.2.0.dist-info/METADATA,sha256=sPAakzVwnoinfbbe5Wf0QS1mDsd8qH-rotmyVljSOGw,15789
|
13
|
+
earningscall-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
14
|
+
earningscall-1.2.0.dist-info/licenses/LICENSE,sha256=ktEB_UcRMg2cQlX9wiDs544xWncWizwS9mEZuGsCLrM,1069
|
15
|
+
earningscall-1.2.0.dist-info/RECORD,,
|
@@ -1,14 +0,0 @@
|
|
1
|
-
earningscall/__init__.py,sha256=_7Xxi8zoSt98fDwzSpEfjrxgiJsc7UobZBwtsAbt3Lg,477
|
2
|
-
earningscall/api.py,sha256=H4Z_GcD6RnTCkwGE--imCOsySDLkD9qf4Rj5j-dZHkw,7088
|
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=NxabgKfZZI1YwDLUwh_MlNgyfkR9VZdcU-LqkGWwi28,6521
|
9
|
-
earningscall/transcript.py,sha256=P-CeTYhE5T78SXDHFEJ0AlVUFz2XPxDMtkeiorziBiw,1007
|
10
|
-
earningscall/utils.py,sha256=Qx8KhlumUdzyBSZRKMS6vpWlb8MGZpLKA4OffJaMdCE,1032
|
11
|
-
earningscall-1.1.0.dist-info/METADATA,sha256=U7DpkmtPIfIRGfpIB5WM77okwWH2XrLxKRI_adOhv1k,15444
|
12
|
-
earningscall-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
13
|
-
earningscall-1.1.0.dist-info/licenses/LICENSE,sha256=ktEB_UcRMg2cQlX9wiDs544xWncWizwS9mEZuGsCLrM,1069
|
14
|
-
earningscall-1.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|