earningscall 0.0.18__py3-none-any.whl → 0.0.20__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 +29 -21
- earningscall/company.py +83 -34
- earningscall/transcript.py +13 -5
- {earningscall-0.0.18.dist-info → earningscall-0.0.20.dist-info}/METADATA +1 -1
- {earningscall-0.0.18.dist-info → earningscall-0.0.20.dist-info}/RECORD +7 -7
- {earningscall-0.0.18.dist-info → earningscall-0.0.20.dist-info}/WHEEL +0 -0
- {earningscall-0.0.18.dist-info → earningscall-0.0.20.dist-info}/licenses/LICENSE +0 -0
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
|
@@ -14,10 +15,9 @@ API_BASE = f"https://v2.api.{DOMAIN}"
|
|
14
15
|
|
15
16
|
|
16
17
|
def get_api_key():
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
return api_key
|
18
|
+
if earningscall.api_key:
|
19
|
+
return earningscall.api_key
|
20
|
+
return os.environ.get("ECALL_API_KEY", "demo")
|
21
21
|
|
22
22
|
|
23
23
|
def api_key_param():
|
@@ -46,6 +46,14 @@ def purge_cache():
|
|
46
46
|
return cache_session().cache.clear()
|
47
47
|
|
48
48
|
|
49
|
+
def get_headers():
|
50
|
+
earnings_call_version = importlib.metadata.version("earningscall")
|
51
|
+
return {
|
52
|
+
"User-Agent": f"EarningsCall Python/{earnings_call_version}",
|
53
|
+
"X-EarningsCall-Version": earnings_call_version,
|
54
|
+
}
|
55
|
+
|
56
|
+
|
49
57
|
def do_get(
|
50
58
|
path: str,
|
51
59
|
use_cache: bool = False,
|
@@ -74,6 +82,7 @@ def do_get(
|
|
74
82
|
return requests.get(
|
75
83
|
url,
|
76
84
|
params=params,
|
85
|
+
headers=get_headers(),
|
77
86
|
stream=kwargs.get("stream"),
|
78
87
|
)
|
79
88
|
|
@@ -97,20 +106,19 @@ def get_transcript(
|
|
97
106
|
year: int,
|
98
107
|
quarter: int,
|
99
108
|
level: Optional[int] = None,
|
100
|
-
) -> Optional[
|
109
|
+
) -> Optional[dict]:
|
101
110
|
"""
|
102
111
|
Get the transcript for a given exchange, symbol, year, and quarter.
|
103
112
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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.
|
109
118
|
|
110
|
-
|
111
|
-
Optional[str]: The transcript for the given exchange, symbol, year, and quarter.
|
119
|
+
:return: The transcript for the given exchange, symbol, year, and quarter.
|
112
120
|
"""
|
113
|
-
log.debug(f"get_transcript year: {year} quarter: {quarter}")
|
121
|
+
log.debug(f"get_transcript exchange: {exchange} symbol: {symbol} year: {year} quarter: {quarter} level: {level}")
|
114
122
|
params = {
|
115
123
|
**api_key_param(),
|
116
124
|
"exchange": exchange,
|
@@ -120,8 +128,7 @@ def get_transcript(
|
|
120
128
|
"level": str(level or 1),
|
121
129
|
}
|
122
130
|
response = do_get("transcript", params=params)
|
123
|
-
|
124
|
-
return None
|
131
|
+
response.raise_for_status()
|
125
132
|
return response.json()
|
126
133
|
|
127
134
|
|
@@ -151,12 +158,13 @@ def download_audio_file(
|
|
151
158
|
"""
|
152
159
|
Get the audio for a given exchange, symbol, year, and quarter.
|
153
160
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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.
|
160
168
|
"""
|
161
169
|
params = {
|
162
170
|
**api_key_param(),
|
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]
|
@@ -43,36 +49,65 @@ class Company:
|
|
43
49
|
year: Optional[int] = None,
|
44
50
|
quarter: Optional[int] = None,
|
45
51
|
event: Optional[EarningsEvent] = None,
|
52
|
+
level: Optional[int] = None,
|
46
53
|
) -> Optional[Transcript]:
|
47
54
|
"""
|
48
55
|
Get the transcript for a given year and quarter.
|
49
56
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
54
61
|
|
55
|
-
|
56
|
-
Optional[Transcript]: The transcript for the given year and quarter.
|
62
|
+
:return: The transcript for the given year and quarter.
|
57
63
|
"""
|
58
64
|
if not self.company_info.exchange or not self.company_info.symbol:
|
59
65
|
return None
|
60
66
|
if (not year or not quarter) and event:
|
61
67
|
year = event.year
|
62
68
|
quarter = event.quarter
|
63
|
-
if
|
69
|
+
if not year or not quarter:
|
64
70
|
raise ValueError("Must specify either event or year and quarter")
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
level
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
76
111
|
|
77
112
|
def download_audio_file(
|
78
113
|
self,
|
@@ -84,27 +119,41 @@ class Company:
|
|
84
119
|
"""
|
85
120
|
Download the audio file for a given year and quarter.
|
86
121
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
file_name (Optional[str]): The file name to save the audio to.
|
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.
|
92
126
|
|
93
|
-
|
94
|
-
Optional[str]: The audio for the given year and quarter.
|
127
|
+
:return: The audio for the given year and quarter.
|
95
128
|
"""
|
129
|
+
log.info(f"Downloading audio file for {self.company_info.symbol} {event}")
|
96
130
|
if not self.company_info.exchange or not self.company_info.symbol:
|
97
131
|
return None
|
98
132
|
if (not year or not quarter) and event:
|
99
133
|
year = event.year
|
100
134
|
quarter = event.quarter
|
101
|
-
if
|
135
|
+
if not year or not quarter:
|
102
136
|
raise ValueError("Must specify either event or year and quarter")
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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/transcript.py
CHANGED
@@ -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
|
-
|
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.
|
3
|
+
Version: 0.0.20
|
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
|
@@ -1,14 +1,14 @@
|
|
1
1
|
earningscall/__init__.py,sha256=0mANmPlE7LEWtOGzV2cmmlPfBIWBWlWRDkyqPHJ1jm8,333
|
2
|
-
earningscall/api.py,sha256=
|
3
|
-
earningscall/company.py,sha256=
|
2
|
+
earningscall/api.py,sha256=8Wl_JhcptjQpplvQjgODWuKgdfJ4gyj-1KET2vLTYes,4960
|
3
|
+
earningscall/company.py,sha256=aae_GjA1ffz3wYY0vGvPQ71VpYwRGXU6psurwDdgNuU,6431
|
4
4
|
earningscall/errors.py,sha256=EA-d6qIYgQs9csp8JptQiAaYoM0M9HhCGJgKA9GAWPg,440
|
5
5
|
earningscall/event.py,sha256=Jf7KPvpeaF9KkeHe46LbL_HIYLXkyHrs3psq-ZY-bkI,692
|
6
6
|
earningscall/exports.py,sha256=i9UWHY6Lq1OzZTZX_1SdNzrNd_PSlPwpB337lGMK4oM,837
|
7
7
|
earningscall/sectors.py,sha256=Xd6DLkAQ_fQkC2s-N9pReC8b_M3iy77OoFftoZj9FWY,5114
|
8
8
|
earningscall/symbols.py,sha256=39tL7oP1HT8BturwKW7mgS33dX2Y_X8cw5GKK0aV02k,6354
|
9
|
-
earningscall/transcript.py,sha256=
|
9
|
+
earningscall/transcript.py,sha256=Sm--ruKTEhkHiZSPP4I1njUgQNx_SfEIuQqjlTpLBh0,718
|
10
10
|
earningscall/utils.py,sha256=Qx8KhlumUdzyBSZRKMS6vpWlb8MGZpLKA4OffJaMdCE,1032
|
11
|
-
earningscall-0.0.
|
12
|
-
earningscall-0.0.
|
13
|
-
earningscall-0.0.
|
14
|
-
earningscall-0.0.
|
11
|
+
earningscall-0.0.20.dist-info/METADATA,sha256=H6qYvL5w8pSsiRRL-vkIqXvYRdkzSGuB8By7K9np698,7177
|
12
|
+
earningscall-0.0.20.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
13
|
+
earningscall-0.0.20.dist-info/licenses/LICENSE,sha256=ktEB_UcRMg2cQlX9wiDs544xWncWizwS9mEZuGsCLrM,1069
|
14
|
+
earningscall-0.0.20.dist-info/RECORD,,
|
File without changes
|
File without changes
|