earningscall 0.0.17__py3-none-any.whl → 0.0.19__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/api.py CHANGED
@@ -1,3 +1,4 @@
1
+ import importlib
1
2
  import logging
2
3
  import os
3
4
  from typing import Optional
@@ -11,13 +12,13 @@ log = logging.getLogger(__file__)
11
12
 
12
13
  DOMAIN = os.environ.get("ECALL_DOMAIN", "earningscall.biz")
13
14
  API_BASE = f"https://v2.api.{DOMAIN}"
15
+ EARNINGS_CALL_VERSION = importlib.metadata.version("earningscall")
14
16
 
15
17
 
16
18
  def get_api_key():
17
- api_key = earningscall.api_key
18
- if not api_key:
19
- return os.environ.get("ECALL_API_KEY", "demo")
20
- return api_key
19
+ if earningscall.api_key:
20
+ return earningscall.api_key
21
+ return os.environ.get("ECALL_API_KEY", "demo")
21
22
 
22
23
 
23
24
  def api_key_param():
@@ -46,7 +47,29 @@ def purge_cache():
46
47
  return cache_session().cache.clear()
47
48
 
48
49
 
49
- def do_get(path: str, use_cache: bool = False, **kwargs):
50
+ def get_headers():
51
+ return {
52
+ "User-Agent": f"EarningsCall Python/{EARNINGS_CALL_VERSION}",
53
+ "X-EarningsCall-Version": EARNINGS_CALL_VERSION,
54
+ }
55
+
56
+
57
+ def do_get(
58
+ path: str,
59
+ use_cache: bool = False,
60
+ **kwargs,
61
+ ) -> requests.Response:
62
+ """
63
+ Do a GET request to the API.
64
+
65
+ Args:
66
+ path (str): The path to request.
67
+ use_cache (bool): Whether to use the cache.
68
+ **kwargs: Additional arguments to pass to the request.
69
+
70
+ Returns:
71
+ requests.Response: The response from the API.
72
+ """
50
73
  params = {
51
74
  **api_key_param(),
52
75
  **kwargs.get("params", {}),
@@ -56,7 +79,12 @@ def do_get(path: str, use_cache: bool = False, **kwargs):
56
79
  if use_cache and earningscall.enable_requests_cache:
57
80
  return cache_session().get(url, params=params)
58
81
  else:
59
- return requests.get(url, params=params)
82
+ return requests.get(
83
+ url,
84
+ params=params,
85
+ headers=get_headers(),
86
+ stream=kwargs.get("stream"),
87
+ )
60
88
 
61
89
 
62
90
  def get_events(exchange: str, symbol: str):
@@ -72,19 +100,35 @@ def get_events(exchange: str, symbol: str):
72
100
  return response.json()
73
101
 
74
102
 
75
- def get_transcript(exchange: str, symbol: str, year: int, quarter: int) -> Optional[str]:
76
-
77
- log.debug(f"get_transcript year: {year} quarter: {quarter}")
103
+ def get_transcript(
104
+ exchange: str,
105
+ symbol: str,
106
+ year: int,
107
+ quarter: int,
108
+ level: Optional[int] = None,
109
+ ) -> Optional[dict]:
110
+ """
111
+ Get the transcript for a given exchange, symbol, year, and quarter.
112
+
113
+ :param str exchange: The exchange to get the transcript for.
114
+ :param str symbol: The symbol to get the transcript for.
115
+ :param int year: The year to get the transcript for.
116
+ :param int quarter: The quarter to get the transcript for.
117
+ :param Optional[int] level: The level to get the transcript for.
118
+
119
+ :return: The transcript for the given exchange, symbol, year, and quarter.
120
+ """
121
+ log.debug(f"get_transcript exchange: {exchange} symbol: {symbol} year: {year} quarter: {quarter} level: {level}")
78
122
  params = {
79
123
  **api_key_param(),
80
124
  "exchange": exchange,
81
125
  "symbol": symbol,
82
126
  "year": str(year),
83
127
  "quarter": str(quarter),
128
+ "level": str(level or 1),
84
129
  }
85
130
  response = do_get("transcript", params=params)
86
- if response.status_code != 200:
87
- return None
131
+ response.raise_for_status()
88
132
  return response.json()
89
133
 
90
134
 
@@ -102,3 +146,37 @@ def get_sp500_companies_txt_file():
102
146
  if response.status_code != 200:
103
147
  return None
104
148
  return response.text
149
+
150
+
151
+ def download_audio_file(
152
+ exchange: str,
153
+ symbol: str,
154
+ year: int,
155
+ quarter: int,
156
+ file_name: Optional[str] = None,
157
+ ) -> Optional[str]:
158
+ """
159
+ Get the audio for a given exchange, symbol, year, and quarter.
160
+
161
+ :param str exchange: The exchange to get the audio for.
162
+ :param str symbol: The symbol to get the audio for.
163
+ :param int year: The 4-digit year to get the audio for.
164
+ :param int quarter: The quarter to get the audio for (1, 2, 3, or 4).
165
+ :param file_name: Optionally specify the filename to save the audio to.
166
+ :return: The filename of the downloaded audio file.
167
+ :rtype Optional[str]: The filename of the downloaded audio file.
168
+ """
169
+ params = {
170
+ **api_key_param(),
171
+ "exchange": exchange,
172
+ "symbol": symbol,
173
+ "year": str(year),
174
+ "quarter": str(quarter),
175
+ }
176
+ local_filename = file_name or f"{exchange}_{symbol}_{year}_{quarter}.mp3"
177
+ with do_get("audio", params=params, stream=True) as response:
178
+ response.raise_for_status()
179
+ with open(local_filename, "wb") as f:
180
+ for chunk in response.iter_content(chunk_size=8192):
181
+ f.write(chunk)
182
+ return local_filename
earningscall/company.py CHANGED
@@ -1,7 +1,10 @@
1
1
  import logging
2
2
  from typing import Optional, List
3
3
 
4
+ import requests
5
+
4
6
  from earningscall import api
7
+ from earningscall.errors import InsufficientApiAccessError
5
8
  from earningscall.event import EarningsEvent
6
9
  from earningscall.symbols import CompanyInfo
7
10
  from earningscall.transcript import Transcript
@@ -10,6 +13,9 @@ log = logging.getLogger(__file__)
10
13
 
11
14
 
12
15
  class Company:
16
+ """
17
+ A class representing a company.
18
+ """
13
19
 
14
20
  company_info: CompanyInfo
15
21
  name: Optional[str]
@@ -39,17 +45,115 @@ class Company:
39
45
  return self._events
40
46
 
41
47
  def get_transcript(
42
- self, year: Optional[int] = None, quarter: Optional[int] = None, event: Optional[EarningsEvent] = None
48
+ self,
49
+ year: Optional[int] = None,
50
+ quarter: Optional[int] = None,
51
+ event: Optional[EarningsEvent] = None,
52
+ level: Optional[int] = None,
43
53
  ) -> Optional[Transcript]:
54
+ """
55
+ Get the transcript for a given year and quarter.
56
+
57
+ :param Optional[int] year: The year to get the transcript for.
58
+ :param Optional[int] quarter: The quarter to get the transcript for.
59
+ :param Optional[EarningsEvent] event: The event to get the transcript for.
60
+ :param Optional[int] level: The transcript level to retrieve. Default: 1
44
61
 
62
+ :return: The transcript for the given year and quarter.
63
+ """
45
64
  if not self.company_info.exchange or not self.company_info.symbol:
46
65
  return None
47
66
  if (not year or not quarter) and event:
48
67
  year = event.year
49
68
  quarter = event.quarter
50
- if (not year or not quarter) and not event:
69
+ if not year or not quarter:
51
70
  raise ValueError("Must specify either event or year and quarter")
52
- resp = api.get_transcript(self.company_info.exchange, self.company_info.symbol, year, quarter) # type: ignore
53
- if not resp:
71
+ if quarter < 1 or quarter > 4:
72
+ raise ValueError("Invalid level. Must be one of: {1,2,3,4}")
73
+ if level is None:
74
+ level = 1
75
+ if type(level) is not int or level <= 0 or level > 4:
76
+ raise ValueError(f"Invalid level: {level}. Must be one of: 1, 2, 3, or 4.")
77
+ try:
78
+ response_payload = api.get_transcript(
79
+ self.company_info.exchange,
80
+ self.company_info.symbol,
81
+ year,
82
+ quarter,
83
+ level=level,
84
+ )
85
+ # TODO: Investigate alternatives to @dataclass for level 3 transcripts, as this is
86
+ # extremely slow.
87
+ transcript = Transcript.from_dict(response_payload) # type: ignore
88
+ if level == 3:
89
+ for speaker in transcript.speakers:
90
+ speaker.text = " ".join(speaker.words)
91
+ if 2 <= level <= 3:
92
+ transcript.text = " ".join(map(lambda spk: spk.text, transcript.speakers))
93
+ elif level == 4:
94
+ transcript.text = " ".join([transcript.prepared_remarks, transcript.questions_and_answers])
95
+ return transcript
96
+ except requests.exceptions.HTTPError as error:
97
+ if error.response.status_code == 404:
98
+ return None
99
+ if error.response.status_code == 403:
100
+ plan_name = error.response.headers.get("X-Plan-Name", "unknown")
101
+ if 2 <= level <= 4:
102
+ error_message = (
103
+ f"Your plan ({plan_name}) does not include Advanced Transcript Data. "
104
+ "Upgrade your plan here: https://earningscall.biz/api-pricing"
105
+ )
106
+ else:
107
+ error_message = f"Unexpected error code was returned from the server. Your plan is: {plan_name}"
108
+ log.error(error_message)
109
+ raise InsufficientApiAccessError(error_message)
110
+ raise error
111
+
112
+ def download_audio_file(
113
+ self,
114
+ year: Optional[int] = None,
115
+ quarter: Optional[int] = None,
116
+ event: Optional[EarningsEvent] = None,
117
+ file_name: Optional[str] = None,
118
+ ) -> Optional[str]:
119
+ """
120
+ Download the audio file for a given year and quarter.
121
+
122
+ :param Optional[int] year: The year to get the audio for.
123
+ :param Optional[int] quarter: The quarter to get the audio for.
124
+ :param Optional[EarningsEvent] event: The event to get the audio for.
125
+ :param Optional[str] file_name: The file name to save the audio to.
126
+
127
+ :return: The audio for the given year and quarter.
128
+ """
129
+ log.info(f"Downloading audio file for {self.company_info.symbol} {event}")
130
+ if not self.company_info.exchange or not self.company_info.symbol:
54
131
  return None
55
- return Transcript.from_dict(resp) # type: ignore
132
+ if (not year or not quarter) and event:
133
+ year = event.year
134
+ quarter = event.quarter
135
+ if not year or not quarter:
136
+ raise ValueError("Must specify either event or year and quarter")
137
+ if quarter < 1 or quarter > 4:
138
+ raise ValueError("Invalid level. Must be one of: {1,2,3,4}")
139
+ try:
140
+ resp = api.download_audio_file(
141
+ exchange=self.company_info.exchange,
142
+ symbol=self.company_info.symbol,
143
+ year=year,
144
+ quarter=quarter,
145
+ file_name=file_name,
146
+ )
147
+ return resp
148
+ except requests.exceptions.HTTPError as error:
149
+ if error.response.status_code == 404:
150
+ return None
151
+ if error.response.status_code == 403:
152
+ plan_name = error.response.headers["X-Plan-Name"]
153
+ error_message = (
154
+ f"Your plan ({plan_name}) does not include Audio Files. "
155
+ "Upgrade your plan here: https://earningscall.biz/api-pricing"
156
+ )
157
+ log.error(error_message)
158
+ raise InsufficientApiAccessError(error_message)
159
+ raise error
earningscall/symbols.py CHANGED
@@ -8,7 +8,7 @@ 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 = ["NYSE", "NASDAQ", "AMEX", "TSX", "TSXV", "OTC"]
11
+ EXCHANGES_IN_ORDER = ["NYSE", "NASDAQ", "AMEX", "TSX", "TSXV", "OTC", "LSE"]
12
12
 
13
13
  log = logging.getLogger(__file__)
14
14
 
@@ -1,17 +1,25 @@
1
- import logging
2
1
  from dataclasses import dataclass, field
3
- from typing import Optional
2
+ from typing import List, Optional
4
3
 
5
4
  from dataclasses_json import dataclass_json
6
5
 
7
6
  from earningscall.event import EarningsEvent
8
7
 
9
- log = logging.getLogger(__file__)
8
+
9
+ @dataclass_json
10
+ @dataclass
11
+ class Speaker:
12
+ speaker: str
13
+ text: Optional[str] = field(default=None)
14
+ words: Optional[List[str]] = field(default=None)
15
+ start_times: Optional[List[float]] = field(default=None)
10
16
 
11
17
 
12
18
  @dataclass_json
13
19
  @dataclass
14
20
  class Transcript:
15
-
16
- text: str
21
+ text: Optional[str] = field(default=None)
17
22
  event: Optional[EarningsEvent] = field(default=None)
23
+ speakers: Optional[List[Speaker]] = field(default=None)
24
+ prepared_remarks: Optional[str] = field(default=None)
25
+ questions_and_answers: Optional[str] = field(default=None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: earningscall
3
- Version: 0.0.17
3
+ Version: 0.0.19
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
@@ -0,0 +1,14 @@
1
+ earningscall/__init__.py,sha256=0mANmPlE7LEWtOGzV2cmmlPfBIWBWlWRDkyqPHJ1jm8,333
2
+ earningscall/api.py,sha256=jxf2M8A0VHhr2x0IK2R3PD_enKzA-YvrJ9kBZ2YBxIM,4956
3
+ earningscall/company.py,sha256=aae_GjA1ffz3wYY0vGvPQ71VpYwRGXU6psurwDdgNuU,6431
4
+ earningscall/errors.py,sha256=EA-d6qIYgQs9csp8JptQiAaYoM0M9HhCGJgKA9GAWPg,440
5
+ earningscall/event.py,sha256=Jf7KPvpeaF9KkeHe46LbL_HIYLXkyHrs3psq-ZY-bkI,692
6
+ earningscall/exports.py,sha256=i9UWHY6Lq1OzZTZX_1SdNzrNd_PSlPwpB337lGMK4oM,837
7
+ earningscall/sectors.py,sha256=Xd6DLkAQ_fQkC2s-N9pReC8b_M3iy77OoFftoZj9FWY,5114
8
+ earningscall/symbols.py,sha256=39tL7oP1HT8BturwKW7mgS33dX2Y_X8cw5GKK0aV02k,6354
9
+ earningscall/transcript.py,sha256=Sm--ruKTEhkHiZSPP4I1njUgQNx_SfEIuQqjlTpLBh0,718
10
+ earningscall/utils.py,sha256=Qx8KhlumUdzyBSZRKMS6vpWlb8MGZpLKA4OffJaMdCE,1032
11
+ earningscall-0.0.19.dist-info/METADATA,sha256=gU0XJGI8Q0L--cF7oMSQVVlKfrVqXuPxlSAHpFFgiYU,7177
12
+ earningscall-0.0.19.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
13
+ earningscall-0.0.19.dist-info/licenses/LICENSE,sha256=ktEB_UcRMg2cQlX9wiDs544xWncWizwS9mEZuGsCLrM,1069
14
+ earningscall-0.0.19.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- earningscall/__init__.py,sha256=0mANmPlE7LEWtOGzV2cmmlPfBIWBWlWRDkyqPHJ1jm8,333
2
- earningscall/api.py,sha256=F_1h8ib7xCK0r8wMtp79D8BQgiusYmfsaRrAaPF9eic,2488
3
- earningscall/company.py,sha256=ZNF75htXg37oXtNzwrHH8DoTdyFJ3PBB9qSNBp_vo8c,1943
4
- earningscall/errors.py,sha256=EA-d6qIYgQs9csp8JptQiAaYoM0M9HhCGJgKA9GAWPg,440
5
- earningscall/event.py,sha256=Jf7KPvpeaF9KkeHe46LbL_HIYLXkyHrs3psq-ZY-bkI,692
6
- earningscall/exports.py,sha256=i9UWHY6Lq1OzZTZX_1SdNzrNd_PSlPwpB337lGMK4oM,837
7
- earningscall/sectors.py,sha256=Xd6DLkAQ_fQkC2s-N9pReC8b_M3iy77OoFftoZj9FWY,5114
8
- earningscall/symbols.py,sha256=Fsk9F2SYzn5TUQnL84AO1_xgiMg6G1DyvBGm7-_3LH4,6347
9
- earningscall/transcript.py,sha256=vuI0FOSaWDGKYaUxq1i6cnBZQJ2TAuARAWAhHlfuNRc,329
10
- earningscall/utils.py,sha256=Qx8KhlumUdzyBSZRKMS6vpWlb8MGZpLKA4OffJaMdCE,1032
11
- earningscall-0.0.17.dist-info/METADATA,sha256=Ph8kEPLaoGnl3KEEXDqN_oU1i21-vrnG6wG5Pz7V-CU,7177
12
- earningscall-0.0.17.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
13
- earningscall-0.0.17.dist-info/licenses/LICENSE,sha256=ktEB_UcRMg2cQlX9wiDs544xWncWizwS9mEZuGsCLrM,1069
14
- earningscall-0.0.17.dist-info/RECORD,,