earningscall 1.1.0__tar.gz → 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {earningscall-1.1.0 → earningscall-1.1.1}/.gitignore +4 -1
  2. {earningscall-1.1.0 → earningscall-1.1.1}/CHANGELOG.md +6 -0
  3. {earningscall-1.1.0 → earningscall-1.1.1}/PKG-INFO +6 -6
  4. {earningscall-1.1.0 → earningscall-1.1.1}/README.md +5 -5
  5. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/__init__.py +1 -5
  6. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/api.py +21 -8
  7. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/errors.py +4 -0
  8. {earningscall-1.1.0 → earningscall-1.1.1}/pyproject.toml +1 -1
  9. {earningscall-1.1.0 → earningscall-1.1.1}/scripts/get_all_company_transcripts.py +4 -1
  10. earningscall-1.1.1/scripts/get_all_sp500_transcript_texts.py +44 -0
  11. {earningscall-1.1.0 → earningscall-1.1.1}/scripts/get_single_transcript.py +2 -3
  12. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_get_transcript.py +15 -1
  13. {earningscall-1.1.0 → earningscall-1.1.1}/.github/workflows/release.yml +0 -0
  14. {earningscall-1.1.0 → earningscall-1.1.1}/.github/workflows/test.yml +0 -0
  15. {earningscall-1.1.0 → earningscall-1.1.1}/.python-version +0 -0
  16. {earningscall-1.1.0 → earningscall-1.1.1}/DEVELOPMENT.md +0 -0
  17. {earningscall-1.1.0 → earningscall-1.1.1}/LICENSE +0 -0
  18. {earningscall-1.1.0 → earningscall-1.1.1}/TODO.md +0 -0
  19. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/company.py +0 -0
  20. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/event.py +0 -0
  21. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/exports.py +0 -0
  22. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/sectors.py +0 -0
  23. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/symbols.py +0 -0
  24. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/transcript.py +0 -0
  25. {earningscall-1.1.0 → earningscall-1.1.1}/earningscall/utils.py +0 -0
  26. {earningscall-1.1.0 → earningscall-1.1.1}/hatch.toml +0 -0
  27. {earningscall-1.1.0 → earningscall-1.1.1}/scripts/download_audio_files.py +0 -0
  28. {earningscall-1.1.0 → earningscall-1.1.1}/scripts/download_single_audio_file.py +0 -0
  29. {earningscall-1.1.0 → earningscall-1.1.1}/scripts/download_sp500_audio_files.py +0 -0
  30. {earningscall-1.1.0 → earningscall-1.1.1}/scripts/list_companies.py +0 -0
  31. {earningscall-1.1.0 → earningscall-1.1.1}/setup.cfg +0 -0
  32. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-2.yaml +0 -0
  33. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-3.yaml +0 -0
  34. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/aapl-q1-2022-advanced-data-level-4.yaml +0 -0
  35. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/aapl-q1-2022-speaker-name-map-v2.yaml +0 -0
  36. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-authorized-l2.yaml +0 -0
  37. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-authorized.yaml +0 -0
  38. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/aapl-q1-2030-not-found.yaml +0 -0
  39. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/aapl-q1-2030-server-error.yaml +0 -0
  40. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/demo-symbols-v2-alpha.yaml +0 -0
  41. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/demo-symbols-v2.yaml +0 -0
  42. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/meta-q3-2024-not-authorized.yaml +0 -0
  43. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/meta-q3-2024-not-found.yaml +0 -0
  44. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/meta-q3-2024-other-error.yaml +0 -0
  45. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/msft-company-events.yaml +0 -0
  46. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/msft-q1-2022-audio-file-short-clip.yaml +0 -0
  47. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/msft-transcript-response.yaml +0 -0
  48. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/sp500-company-list-failed.yaml +0 -0
  49. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/sp500-company-list.yaml +0 -0
  50. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/symbols-v2-missing-edge-cases.yaml +0 -0
  51. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/symbols-v2.yaml +0 -0
  52. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/symbols.txt +0 -0
  53. {earningscall-1.1.0 → earningscall-1.1.1}/tests/data/symbols.yaml +0 -0
  54. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_api.py +0 -0
  55. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_company.py +0 -0
  56. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_download_audio_files.py +0 -0
  57. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_earnings_event.py +0 -0
  58. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_errors.py +0 -0
  59. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_exports.py +0 -0
  60. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_get_company_events.py +0 -0
  61. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_get_sp500_companies_api.py +0 -0
  62. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_helper.py +0 -0
  63. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_responses_mocking.py +0 -0
  64. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_sectors.py +0 -0
  65. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_symbols.py +0 -0
  66. {earningscall-1.1.0 → earningscall-1.1.1}/tests/test_utils.py +0 -0
@@ -36,4 +36,7 @@ tasks.xml
36
36
 
37
37
  # Root files
38
38
  /.coverage*
39
- /.mypy_cache/
39
+ /.mypy_cache/
40
+
41
+ # Downloaded Data from Scripts
42
+ /data
@@ -1,5 +1,11 @@
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
+
3
9
  ## Release `1.1.0` - 2025-01-26
4
10
 
5
11
  * 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.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
@@ -324,8 +324,8 @@ Depending on your specific requirements, you can adjust the retry strategy. For
324
324
  To customize the retry behavior, set the `retry_strategy` variable with the desired parameters:
325
325
 
326
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 3 seconds).
328
- - **max_attempts**: int — sets the maximum number of total request attempts (default is 5).
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
329
 
330
330
  #### Default Retry Strategy
331
331
 
@@ -336,8 +336,8 @@ import earningscall
336
336
 
337
337
  earningscall.retry_strategy = {
338
338
  "strategy": "exponential",
339
- "base_delay": 3,
340
- "max_attempts": 5,
339
+ "base_delay": 1,
340
+ "max_attempts": 10,
341
341
  }
342
342
  ```
343
343
 
@@ -350,7 +350,7 @@ import earningscall
350
350
 
351
351
  earningscall.retry_strategy = {
352
352
  "strategy": "exponential",
353
- "base_delay": 3,
353
+ "base_delay": 1,
354
354
  "max_attempts": 1,
355
355
  }
356
356
  ```
@@ -272,8 +272,8 @@ Depending on your specific requirements, you can adjust the retry strategy. For
272
272
  To customize the retry behavior, set the `retry_strategy` variable with the desired parameters:
273
273
 
274
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 3 seconds).
276
- - **max_attempts**: int — sets the maximum number of total request attempts (default is 5).
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
277
 
278
278
  #### Default Retry Strategy
279
279
 
@@ -284,8 +284,8 @@ import earningscall
284
284
 
285
285
  earningscall.retry_strategy = {
286
286
  "strategy": "exponential",
287
- "base_delay": 3,
288
- "max_attempts": 5,
287
+ "base_delay": 1,
288
+ "max_attempts": 10,
289
289
  }
290
290
  ```
291
291
 
@@ -298,7 +298,7 @@ import earningscall
298
298
 
299
299
  earningscall.retry_strategy = {
300
300
  "strategy": "exponential",
301
- "base_delay": 3,
301
+ "base_delay": 1,
302
302
  "max_attempts": 1,
303
303
  }
304
304
  ```
@@ -5,10 +5,6 @@ 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
10
  __all__ = ["get_company", "get_all_companies", "get_sp500_companies", "Symbols", "load_symbols"]
@@ -5,17 +5,23 @@ import logging
5
5
  import os
6
6
  import time
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
  )
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "earningscall"
3
- version = "1.1.0"
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
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)
@@ -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
 
@@ -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
File without changes
File without changes
File without changes
File without changes