earningscall 1.1.0__tar.gz → 1.2.0__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 (75) hide show
  1. {earningscall-1.1.0 → earningscall-1.2.0}/.gitignore +4 -1
  2. earningscall-1.2.0/.python-version +1 -0
  3. {earningscall-1.1.0 → earningscall-1.2.0}/CHANGELOG.md +10 -0
  4. {earningscall-1.1.0 → earningscall-1.2.0}/PKG-INFO +20 -7
  5. {earningscall-1.1.0 → earningscall-1.2.0}/README.md +19 -6
  6. earningscall-1.2.0/earningscall/__init__.py +17 -0
  7. {earningscall-1.1.0 → earningscall-1.2.0}/earningscall/api.py +35 -11
  8. earningscall-1.2.0/earningscall/calendar.py +30 -0
  9. {earningscall-1.1.0 → earningscall-1.2.0}/earningscall/errors.py +4 -0
  10. earningscall-1.2.0/earningscall/exports.py +86 -0
  11. {earningscall-1.1.0 → earningscall-1.2.0}/pyproject.toml +1 -1
  12. {earningscall-1.1.0 → earningscall-1.2.0}/scripts/get_all_company_transcripts.py +4 -1
  13. earningscall-1.2.0/scripts/get_all_sp500_transcript_texts.py +44 -0
  14. earningscall-1.2.0/scripts/get_calendar.py +14 -0
  15. {earningscall-1.1.0 → earningscall-1.2.0}/scripts/get_single_transcript.py +2 -3
  16. earningscall-1.2.0/tests/data/get-calendar-500-error.yaml +14 -0
  17. earningscall-1.2.0/tests/data/get-calendar-not-found-response.yaml +14 -0
  18. earningscall-1.2.0/tests/data/get-calendar-successful-response.yaml +62 -0
  19. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_download_audio_files.py +0 -8
  20. earningscall-1.2.0/tests/test_get_calendar.py +106 -0
  21. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_get_transcript.py +15 -1
  22. earningscall-1.1.0/.python-version +0 -1
  23. earningscall-1.1.0/earningscall/__init__.py +0 -14
  24. earningscall-1.1.0/earningscall/exports.py +0 -45
  25. {earningscall-1.1.0 → earningscall-1.2.0}/.github/workflows/release.yml +0 -0
  26. {earningscall-1.1.0 → earningscall-1.2.0}/.github/workflows/test.yml +0 -0
  27. {earningscall-1.1.0 → earningscall-1.2.0}/DEVELOPMENT.md +0 -0
  28. {earningscall-1.1.0 → earningscall-1.2.0}/LICENSE +0 -0
  29. {earningscall-1.1.0 → earningscall-1.2.0}/TODO.md +0 -0
  30. {earningscall-1.1.0 → earningscall-1.2.0}/earningscall/company.py +0 -0
  31. {earningscall-1.1.0 → earningscall-1.2.0}/earningscall/event.py +0 -0
  32. {earningscall-1.1.0 → earningscall-1.2.0}/earningscall/sectors.py +0 -0
  33. {earningscall-1.1.0 → earningscall-1.2.0}/earningscall/symbols.py +0 -0
  34. {earningscall-1.1.0 → earningscall-1.2.0}/earningscall/transcript.py +0 -0
  35. {earningscall-1.1.0 → earningscall-1.2.0}/earningscall/utils.py +0 -0
  36. {earningscall-1.1.0 → earningscall-1.2.0}/hatch.toml +0 -0
  37. {earningscall-1.1.0 → earningscall-1.2.0}/scripts/download_audio_files.py +0 -0
  38. {earningscall-1.1.0 → earningscall-1.2.0}/scripts/download_single_audio_file.py +0 -0
  39. {earningscall-1.1.0 → earningscall-1.2.0}/scripts/download_sp500_audio_files.py +0 -0
  40. {earningscall-1.1.0 → earningscall-1.2.0}/scripts/list_companies.py +0 -0
  41. {earningscall-1.1.0 → earningscall-1.2.0}/setup.cfg +0 -0
  42. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/aapl-q1-2022-advanced-data-level-2.yaml +0 -0
  43. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/aapl-q1-2022-advanced-data-level-3.yaml +0 -0
  44. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/aapl-q1-2022-advanced-data-level-4.yaml +0 -0
  45. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/aapl-q1-2022-speaker-name-map-v2.yaml +0 -0
  46. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/aapl-q1-2030-not-authorized-l2.yaml +0 -0
  47. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/aapl-q1-2030-not-authorized.yaml +0 -0
  48. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/aapl-q1-2030-not-found.yaml +0 -0
  49. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/aapl-q1-2030-server-error.yaml +0 -0
  50. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/demo-symbols-v2-alpha.yaml +0 -0
  51. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/demo-symbols-v2.yaml +0 -0
  52. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/meta-q3-2024-not-authorized.yaml +0 -0
  53. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/meta-q3-2024-not-found.yaml +0 -0
  54. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/meta-q3-2024-other-error.yaml +0 -0
  55. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/msft-company-events.yaml +0 -0
  56. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/msft-q1-2022-audio-file-short-clip.yaml +0 -0
  57. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/msft-transcript-response.yaml +0 -0
  58. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/sp500-company-list-failed.yaml +0 -0
  59. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/sp500-company-list.yaml +0 -0
  60. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/symbols-v2-missing-edge-cases.yaml +0 -0
  61. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/symbols-v2.yaml +0 -0
  62. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/symbols.txt +0 -0
  63. {earningscall-1.1.0 → earningscall-1.2.0}/tests/data/symbols.yaml +0 -0
  64. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_api.py +0 -0
  65. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_company.py +0 -0
  66. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_earnings_event.py +0 -0
  67. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_errors.py +0 -0
  68. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_exports.py +0 -0
  69. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_get_company_events.py +0 -0
  70. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_get_sp500_companies_api.py +0 -0
  71. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_helper.py +0 -0
  72. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_responses_mocking.py +0 -0
  73. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_sectors.py +0 -0
  74. {earningscall-1.1.0 → earningscall-1.2.0}/tests/test_symbols.py +0 -0
  75. {earningscall-1.1.0 → earningscall-1.2.0}/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.9
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## Release `1.2.0` - 2025-02-12
4
+
5
+ * Add `get_calendar` function to get the calendar for a given date.
6
+
7
+ ## Release `1.1.1` - 2025-01-26
8
+
9
+ * Modify default retry strategy to use 1s base delay and 10 max attempts (necessary for starter plan).
10
+ * Check for HTTP 401 Unauthorized status code from server and raise a helpful error message to the user.
11
+ * Update documentation and example scripts to reflect new retry configuration
12
+
3
13
  ## Release `1.1.0` - 2025-01-26
4
14
 
5
15
  * Add backoff and retry logic to all API calls.
@@ -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
  ```
@@ -208,6 +208,20 @@ print("Downloading audio file for Apple Inc. Q3 2021...")
208
208
  audio_file = company.download_audio_file(year=2021, quarter=3, file_name="Apple Q3 2021.mp3")
209
209
  ```
210
210
 
211
+ ## Get Earnings Event Calendar
212
+
213
+ ```python
214
+ from datetime import date
215
+
216
+ from earningscall import get_calendar
217
+
218
+ calendar = get_calendar(date(2025, 1, 10))
219
+
220
+ for event in calendar:
221
+ print(f"{event.company_name} - Q{event.quarter} {event.year} on: {event.conference_date.astimezone().isoformat()} Transcript Ready: {event.transcript_ready}")
222
+ ```
223
+
224
+
211
225
  ## List All Companies
212
226
 
213
227
  ```python
@@ -245,7 +259,6 @@ for company in get_sp500_companies():
245
259
  print(f"{company.company_info} -- {company.company_info.sector} -- {company.company_info.industry}")
246
260
  ```
247
261
 
248
-
249
262
  ## Advanced
250
263
 
251
264
  ### Disable Caching
@@ -272,8 +285,8 @@ Depending on your specific requirements, you can adjust the retry strategy. For
272
285
  To customize the retry behavior, set the `retry_strategy` variable with the desired parameters:
273
286
 
274
287
  - **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 3 seconds).
276
- - **max_attempts**: int — sets the maximum number of total request attempts (default is 5).
288
+ - **base_delay**: float (in seconds) — specifies the delay between retries (default is 1 seconds).
289
+ - **max_attempts**: int — sets the maximum number of total request attempts (default is 10).
277
290
 
278
291
  #### Default Retry Strategy
279
292
 
@@ -284,8 +297,8 @@ import earningscall
284
297
 
285
298
  earningscall.retry_strategy = {
286
299
  "strategy": "exponential",
287
- "base_delay": 3,
288
- "max_attempts": 5,
300
+ "base_delay": 1,
301
+ "max_attempts": 10,
289
302
  }
290
303
  ```
291
304
 
@@ -298,7 +311,7 @@ import earningscall
298
311
 
299
312
  earningscall.retry_strategy = {
300
313
  "strategy": "exponential",
301
- "base_delay": 3,
314
+ "base_delay": 1,
302
315
  "max_attempts": 1,
303
316
  }
304
317
  ```
@@ -0,0 +1,17 @@
1
+ from typing import Dict, Optional, Union
2
+
3
+ from earningscall.exports import get_company, get_all_companies, get_sp500_companies, get_calendar
4
+ from earningscall.symbols import Symbols, load_symbols
5
+
6
+ api_key: Optional[str] = None
7
+ enable_requests_cache: bool = True
8
+ retry_strategy: Optional[Dict[str, Union[str, int, float]]] = None
9
+
10
+ __all__ = [
11
+ "get_company",
12
+ "get_all_companies",
13
+ "get_sp500_companies",
14
+ "Symbols",
15
+ "load_symbols",
16
+ "get_calendar",
17
+ ]
@@ -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
+ )
@@ -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
@@ -0,0 +1,86 @@
1
+ import datetime
2
+ from datetime import date, timedelta
3
+ from typing import List, Optional, Iterator
4
+
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
10
+ from earningscall.company import Company
11
+ from earningscall.errors import InsufficientApiAccessError
12
+ from earningscall.symbols import get_symbols
13
+
14
+
15
+ def get_company(symbol: str, exchange: Optional[str] = None) -> Optional[Company]:
16
+ """
17
+ Get a company by symbol and optionally an exchange.
18
+
19
+ :param str symbol: The symbol to get the company for.
20
+ :param Optional[str] exchange: The exchange to get the company for.
21
+
22
+ :return: The company for the given symbol and exchange.
23
+ """
24
+ company_info = get_symbols().lookup_company(symbol=symbol, exchange=exchange)
25
+ if company_info:
26
+ return Company(company_info=company_info)
27
+ return None
28
+
29
+
30
+ def get_all_companies() -> Iterator[Company]:
31
+ """
32
+ Get all companies.
33
+
34
+ :return: An iterator of all companies that is available to the current API plan.
35
+ """
36
+ for company_info in get_symbols().get_all():
37
+ yield Company(company_info=company_info)
38
+
39
+
40
+ def get_sp500_companies() -> Iterator[Company]:
41
+ """
42
+ Get all S&P 500 companies.
43
+
44
+ :return: An iterator of all S&P 500 companies that is available to the current API plan.
45
+ """
46
+ resp = get_sp500_companies_txt_file()
47
+ if not resp:
48
+ return
49
+ for ticker_symbol in resp.split("\n"):
50
+ company_info = get_symbols().lookup_company(ticker_symbol)
51
+ if company_info:
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
  [project]
2
2
  name = "earningscall"
3
- version = "1.1.0"
3
+ version = "1.2.0"
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
6
  authors = [{ name = "EarningsCall", email = "dev@earningscall.biz" }]
@@ -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)
@@ -0,0 +1,14 @@
1
+ from datetime import date
2
+ import earningscall # noqa: F401
3
+
4
+ from earningscall import get_calendar
5
+
6
+ # TODO: Set your API key here:
7
+ # earningscall.api_key = "YOUR API KEY HERE"
8
+
9
+ events = get_calendar(date(2025, 1, 10))
10
+
11
+ for event in events:
12
+ print(
13
+ f"{event.company_name} - Q{event.quarter} {event.year} on: {event.conference_date.astimezone().isoformat()} Transcript Ready: {event.transcript_ready}"
14
+ )
@@ -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
 
@@ -0,0 +1,14 @@
1
+ responses:
2
+ - response:
3
+ auto_calculate_content_length: false
4
+ body: ''
5
+ content_type: text/plain
6
+ headers:
7
+ Via: 1.1 1cae0bb0106fc058447f3b32dee7b228.cloudfront.net (CloudFront)
8
+ X-Amz-Cf-Id: GJG7c5roSXmd3WXEWTNzyrl8VN9fWmIm3OR8oYxEZg8bWpoiZE6XvA==
9
+ X-Amz-Cf-Pop: IAH50-C4
10
+ X-Cache: FunctionGeneratedResponse from cloudfront
11
+ X-Plan-Name: demo
12
+ method: GET
13
+ status: 500
14
+ url: https://v2.api.earningscall.biz/calendar?apikey=XXXXXXXXXXX&year=2020&month=1&day=1
@@ -0,0 +1,14 @@
1
+ responses:
2
+ - response:
3
+ auto_calculate_content_length: false
4
+ body: ''
5
+ content_type: text/plain
6
+ headers:
7
+ Via: 1.1 1cae0bb0106fc058447f3b32dee7b228.cloudfront.net (CloudFront)
8
+ X-Amz-Cf-Id: GJG7c5roSXmd3WXEWTNzyrl8VN9fWmIm3OR8oYxEZg8bWpoiZE6XvA==
9
+ X-Amz-Cf-Pop: IAH50-C4
10
+ X-Cache: FunctionGeneratedResponse from cloudfront
11
+ X-Plan-Name: starter
12
+ method: GET
13
+ status: 404
14
+ url: https://v2.api.earningscall.biz/calendar?apikey=XXXXXXXXXXX&year=2018&month=1&day=1
@@ -0,0 +1,62 @@
1
+ responses:
2
+ - response:
3
+ auto_calculate_content_length: false
4
+ body: '[{"exchange": "NASDAQ", "symbol": "MPAA", "year": 2025, "quarter": 3, "conference_date":
5
+ "2025-02-10T13:00:00.000-05:00", "company_name": "Motorcar Parts of America,
6
+ Inc.", "transcript_ready": true}, {"exchange": "NASDAQ", "symbol": "SPSC", "year":
7
+ 2024, "quarter": 4, "conference_date": "2025-02-10T16:30:00-05:00", "company_name":
8
+ "SPS Commerce, Inc.", "transcript_ready": true}, {"exchange": "NASDAQ", "symbol":
9
+ "RICK", "year": 2025, "quarter": 1, "conference_date": "2025-02-10T16:30:00-05:00",
10
+ "company_name": "RCI Hospitality Holdings, Inc.", "transcript_ready": true},
11
+ {"exchange": "NASDAQ", "symbol": "ALAB", "year": 2024, "quarter": 4, "conference_date":
12
+ "2025-02-10T16:30:00-05:00", "company_name": "Astera Labs, Inc.", "transcript_ready":
13
+ true}, {"exchange": "NASDAQ", "symbol": "PETS", "year": 2025, "quarter": 3,
14
+ "conference_date": "2025-02-10T16:30:00-05:00", "company_name": "PetMed Express,
15
+ Inc.", "transcript_ready": true}, {"exchange": "NASDAQ", "symbol": "XAIR", "year":
16
+ 2025, "quarter": 3, "conference_date": "2025-02-10T16:30:00-05:00", "company_name":
17
+ "Beyond Air, Inc.", "transcript_ready": true}, {"exchange": "NASDAQ", "symbol":
18
+ "BLFS", "year": 2024, "quarter": 4, "conference_date": "2025-02-10T16:30:00.000-05:00",
19
+ "company_name": "BioLife Solutions, Inc.", "transcript_ready": true}, {"exchange":
20
+ "NASDAQ", "symbol": "VRTX", "year": 2024, "quarter": 4, "conference_date": "2025-02-10T16:30:00.000-05:00",
21
+ "company_name": "Vertex Pharmaceuticals Incorporated", "transcript_ready": true},
22
+ {"exchange": "NYSE", "symbol": "TAK", "year": 2025, "quarter": 1, "conference_date":
23
+ "2025-02-10T16:30:00-05:00", "company_name": "Takeda Pharmaceutical Company
24
+ Limited", "transcript_ready": true}, {"exchange": "OTC", "symbol": "PRKA", "year":
25
+ 2025, "quarter": 1, "conference_date": "2025-02-10T16:30:00.000-05:00", "company_name":
26
+ "Parks! America Inc", "transcript_ready": true}, {"exchange": "NASDAQ", "symbol":
27
+ "AMKR", "year": 2024, "quarter": 4, "conference_date": "2025-02-10T17:00:00.000-05:00",
28
+ "company_name": "Amkor Technology, Inc.", "transcript_ready": true}, {"exchange":
29
+ "NASDAQ", "symbol": "MITK", "year": 2025, "quarter": 1, "conference_date": "2025-02-10T17:00:00-05:00",
30
+ "company_name": "Mitek Systems, Inc.", "transcript_ready": true}, {"exchange":
31
+ "NYSE", "symbol": "SLQT", "year": 2025, "quarter": 2, "conference_date": "2025-02-10T17:00:00-05:00",
32
+ "company_name": "SelectQuote, Inc.", "transcript_ready": true}, {"exchange":
33
+ "NYSE", "symbol": "NGL", "year": 2025, "quarter": 3, "conference_date": "2025-02-10T16:00:00-06:00",
34
+ "company_name": "NGL Energy Partners LP", "transcript_ready": true}, {"exchange":
35
+ "NASDAQ", "symbol": "LSCC", "year": 2024, "quarter": 4, "conference_date": "2025-02-10T17:00:00-05:00",
36
+ "company_name": "Lattice Semiconductor Corporation", "transcript_ready": true},
37
+ {"exchange": "TSX", "symbol": "CVO", "year": 2025, "quarter": 3, "conference_date":
38
+ "2025-02-10T17:00:00-05:00", "company_name": "Coveo Solutions Inc.", "transcript_ready":
39
+ true}, {"exchange": "NYSE", "symbol": "INSP", "year": 2024, "quarter": 4, "conference_date":
40
+ "2025-02-10T17:00:00-05:00", "company_name": "Inspire Medical Systems, Inc.",
41
+ "transcript_ready": true}, {"exchange": "NASDAQ", "symbol": "CMCO", "year":
42
+ 2025, "quarter": 3, "conference_date": "2025-02-10T17:00:00-05:00", "company_name":
43
+ "Columbus McKinnon Corporation", "transcript_ready": true}, {"exchange": "NASDAQ",
44
+ "symbol": "HLIT", "year": 2024, "quarter": 4, "conference_date": "2025-02-10T14:00:00-08:00",
45
+ "company_name": "Harmonic Inc.", "transcript_ready": true}, {"exchange": "NYSE",
46
+ "symbol": "SSD", "year": 2024, "quarter": 4, "conference_date": "2025-02-10T17:00:00-05:00",
47
+ "company_name": "Simpson Manufacturing Company, Inc.", "transcript_ready": true}]'
48
+ content_type: text/plain
49
+ headers:
50
+ Cache-Control: public, max-age=500
51
+ ETag: W/"ff349ae06e2284df57d342fd9e3c9d4b"
52
+ Last-Modified: Wed, 12 Feb 2025 15:44:12 GMT
53
+ Transfer-Encoding: chunked
54
+ Vary: accept-encoding
55
+ Via: 1.1 dab9621fb9e60d4beae799f308450f86.cloudfront.net (CloudFront)
56
+ X-Amz-Cf-Id: o-89nbe8YAinJi-F0rXkAr8qIUAcTkW6Mmvk4vQeDH-9ob1MZ7QVfg==
57
+ X-Amz-Cf-Pop: IAH50-C4
58
+ X-Cache: Miss from cloudfront
59
+ x-amz-server-side-encryption: AES256
60
+ method: GET
61
+ status: 200
62
+ url: https://v2.api.earningscall.biz/calendar?apikey=XXXXXXXXXXX&year=2025&month=2&day=10
@@ -14,14 +14,6 @@ from earningscall.symbols import CompanyInfo, clear_symbols
14
14
  from earningscall.utils import data_path
15
15
 
16
16
 
17
- # Uncomment and run following code to generate msft-transcript-response.yaml file
18
- #
19
- # from responses import _recorder
20
- # @_recorder.record(file_path="meta-q3-2024-not-found.yaml")
21
- # def test_save_symbols_v1():
22
- # requests.get("https://v2.api.alpha.earningscall.biz/audio?apikey=brocktest&exchange=NASDAQ&symbol=META&year=2024&quarter=3")
23
-
24
-
25
17
  @pytest.fixture(autouse=True)
26
18
  def run_before_and_after_tests():
27
19
  """Fixture to execute asserts before and after a test is run"""
@@ -0,0 +1,106 @@
1
+ from datetime import datetime, date, timedelta
2
+
3
+ import pytest
4
+ import requests
5
+ import responses
6
+
7
+ import earningscall
8
+ from earningscall import get_calendar
9
+ from earningscall.api import purge_cache
10
+ from earningscall.errors import InsufficientApiAccessError
11
+ from earningscall.utils import data_path
12
+
13
+
14
+ # Uncomment and run following code to generate data/get-calendar-not-found-response.yaml file
15
+ #
16
+
17
+ # import earningscall
18
+ # @_recorder.record(file_path="data/get-calendar-not-found-response.yaml")
19
+ # def test_save_symbols_v1():
20
+ # requests.get("https://v2.api.earningscall.biz/calendar?apikey=demo&year=1980&month=1&day=1")
21
+
22
+
23
+ @pytest.fixture(autouse=True)
24
+ def run_before_and_after_tests():
25
+ """Fixture to execute asserts before and after a test is run"""
26
+ # Setup
27
+ earningscall.api_key = None
28
+ earningscall.retry_strategy = {
29
+ "strategy": "exponential",
30
+ "base_delay": 0.01,
31
+ "max_attempts": 3,
32
+ }
33
+ purge_cache()
34
+ yield # this is where the testing happens
35
+ # Teardown
36
+ earningscall.api_key = None
37
+
38
+
39
+ @responses.activate
40
+ def test_get_calendar_invalid_inputs():
41
+ with pytest.raises(ValueError):
42
+ get_calendar(None)
43
+ with pytest.raises(ValueError):
44
+ get_calendar("2025-02-10")
45
+ with pytest.raises(ValueError):
46
+ get_calendar(123456)
47
+ with pytest.raises(ValueError):
48
+ get_calendar(date(2017, 12, 31))
49
+ with pytest.raises(ValueError):
50
+ get_calendar(datetime.now() + timedelta(days=31))
51
+
52
+
53
+ @responses.activate
54
+ def test_get_non_demo_date():
55
+ with pytest.raises(InsufficientApiAccessError):
56
+ get_calendar(date(2020, 1, 1))
57
+
58
+
59
+ @responses.activate
60
+ def test_get_calendar_success():
61
+ earningscall.api_key = "XXXXXXXXXXX"
62
+ responses._add_from_file(file_path=data_path("get-calendar-successful-response.yaml"))
63
+ calendar = get_calendar(date(2025, 2, 10))
64
+ assert len(calendar) == 20
65
+ assert calendar[0].exchange == "NASDAQ"
66
+ assert calendar[0].symbol == "MPAA"
67
+ assert calendar[0].year == 2025
68
+ assert calendar[0].quarter == 3
69
+ assert calendar[0].conference_date.year == 2025
70
+ assert calendar[0].conference_date.month == 2
71
+ assert calendar[0].conference_date.day == 10
72
+ assert calendar[0].transcript_ready
73
+ assert calendar[0].company_name == "Motorcar Parts of America, Inc."
74
+
75
+
76
+ @responses.activate
77
+ def test_get_calendar_success_with_datetime():
78
+ earningscall.api_key = "XXXXXXXXXXX"
79
+ responses._add_from_file(file_path=data_path("get-calendar-successful-response.yaml"))
80
+ calendar = get_calendar(datetime(2025, 2, 10))
81
+ assert len(calendar) == 20
82
+ assert calendar[0].exchange == "NASDAQ"
83
+ assert calendar[0].symbol == "MPAA"
84
+ assert calendar[0].year == 2025
85
+ assert calendar[0].quarter == 3
86
+ assert calendar[0].conference_date.year == 2025
87
+ assert calendar[0].conference_date.month == 2
88
+ assert calendar[0].conference_date.day == 10
89
+ assert calendar[0].transcript_ready
90
+ assert calendar[0].company_name == "Motorcar Parts of America, Inc."
91
+
92
+
93
+ @responses.activate
94
+ def test_get_calendar_server_error():
95
+ earningscall.api_key = "XXXXXXXXXXX"
96
+ responses._add_from_file(file_path=data_path("get-calendar-500-error.yaml"))
97
+ with pytest.raises(requests.exceptions.HTTPError):
98
+ get_calendar(date(2020, 1, 1))
99
+
100
+
101
+ @responses.activate
102
+ def test_get_calendar_not_found():
103
+ earningscall.api_key = "XXXXXXXXXXX"
104
+ responses._add_from_file(file_path=data_path("get-calendar-not-found-response.yaml"))
105
+ calendar = get_calendar(date(2018, 1, 1))
106
+ assert len(calendar) == 0
@@ -8,7 +8,7 @@ import earningscall
8
8
  from earningscall import get_company
9
9
  from earningscall.api import purge_cache
10
10
  from earningscall.company import Company
11
- from earningscall.errors import InsufficientApiAccessError
11
+ from earningscall.errors import InsufficientApiAccessError, InvalidApiKeyError
12
12
  from earningscall.event import EarningsEvent
13
13
  from earningscall.symbols import clear_symbols, CompanyInfo
14
14
  from earningscall.transcript import Transcript
@@ -372,6 +372,20 @@ def test_get_transcript_fails_all_attempts_invalid_retry_strategy():
372
372
  company.get_transcript(year=2023, quarter=1, level=1)
373
373
 
374
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
+
375
389
  # Uncomment and run following code to generate demo-symbols-v2.yaml file
376
390
  #
377
391
  # import requests
@@ -1 +0,0 @@
1
- 3.12.8
@@ -1,14 +0,0 @@
1
- from typing import Dict, Optional, Union
2
-
3
- from earningscall.exports import get_company, get_all_companies, get_sp500_companies
4
- from earningscall.symbols import Symbols, load_symbols
5
-
6
- api_key: Optional[str] = None
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
- }
13
-
14
- __all__ = ["get_company", "get_all_companies", "get_sp500_companies", "Symbols", "load_symbols"]
@@ -1,45 +0,0 @@
1
- from typing import Optional, Iterator
2
-
3
- from earningscall.api import get_sp500_companies_txt_file
4
- from earningscall.company import Company
5
- from earningscall.symbols import get_symbols
6
-
7
-
8
- def get_company(symbol: str, exchange: Optional[str] = None) -> Optional[Company]:
9
- """
10
- Get a company by symbol and optionally an exchange.
11
-
12
- :param str symbol: The symbol to get the company for.
13
- :param Optional[str] exchange: The exchange to get the company for.
14
-
15
- :return: The company for the given symbol and exchange.
16
- """
17
- company_info = get_symbols().lookup_company(symbol=symbol, exchange=exchange)
18
- if company_info:
19
- return Company(company_info=company_info)
20
- return None
21
-
22
-
23
- def get_all_companies() -> Iterator[Company]:
24
- """
25
- Get all companies.
26
-
27
- :return: An iterator of all companies that is available to the current API plan.
28
- """
29
- for company_info in get_symbols().get_all():
30
- yield Company(company_info=company_info)
31
-
32
-
33
- def get_sp500_companies() -> Iterator[Company]:
34
- """
35
- Get all S&P 500 companies.
36
-
37
- :return: An iterator of all S&P 500 companies that is available to the current API plan.
38
- """
39
- resp = get_sp500_companies_txt_file()
40
- if not resp:
41
- return
42
- for ticker_symbol in resp.split("\n"):
43
- company_info = get_symbols().lookup_company(ticker_symbol)
44
- if company_info:
45
- yield Company(company_info=company_info)
File without changes
File without changes
File without changes
File without changes