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 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__ = ["get_company", "get_all_companies", "get_sp500_companies", "Symbols", "load_symbols"]
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("ECALL_DOMAIN", "earningscall.biz")
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
- delay = earningscall.retry_strategy["base_delay"]
125
- max_attempts = int(earningscall.retry_strategy["max_attempts"])
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 earningscall.retry_strategy["strategy"] == "exponential":
146
- wait_time = delay * (2**attempt) # Exponential backoff: 3s -> 6s -> 12s -> 24s -> 48s
147
- elif earningscall.retry_strategy["strategy"] == "linear":
148
- wait_time = delay * (attempt + 1) # Linear backoff: 3s -> 6s -> 9s -> 12s -> 15s
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 get_events(exchange: str, symbol: str):
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,
@@ -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
@@ -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/exports.py CHANGED
@@ -1,7 +1,14 @@
1
- from typing import Optional, Iterator
1
+ import datetime
2
+ from datetime import date, timedelta
3
+ from typing import List, Optional, Iterator
2
4
 
3
- from earningscall.api import get_sp500_companies_txt_file
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.1.0
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 3 seconds).
328
- - **max_attempts**: int — sets the maximum number of total request attempts (default is 5).
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": 3,
340
- "max_attempts": 5,
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": 3,
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,,