earningscall 0.0.13__tar.gz → 0.0.16__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.
- {earningscall-0.0.13 → earningscall-0.0.16}/.github/workflows/test.yml +1 -1
- earningscall-0.0.16/CHANGELOG.md +9 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/DEVELOPMENT.md +41 -1
- {earningscall-0.0.13 → earningscall-0.0.16}/PKG-INFO +36 -8
- {earningscall-0.0.13 → earningscall-0.0.16}/README.md +22 -5
- earningscall-0.0.16/TODO.md +11 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/__init__.py +1 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/api.py +38 -18
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/sectors.py +10 -41
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/symbols.py +5 -41
- {earningscall-0.0.13 → earningscall-0.0.16}/pyproject.toml +31 -3
- earningscall-0.0.16/tests/data/msft-company-events.yaml +28 -0
- earningscall-0.0.16/tests/data/symbols-v2-missing-edge-cases.yaml +10 -0
- earningscall-0.0.16/tests/test_get_company_events.py +32 -0
- earningscall-0.0.16/tests/test_get_sp500_companies_api.py +62 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/test_get_transcript.py +4 -0
- earningscall-0.0.16/tests/test_responses_mocking.py +34 -0
- earningscall-0.0.16/tests/test_sectors.py +17 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/test_symbols.py +53 -1
- earningscall-0.0.16/tests/test_utils.py +13 -0
- earningscall-0.0.13/CHANGELOG.md +0 -0
- earningscall-0.0.13/TODO.md +0 -9
- {earningscall-0.0.13 → earningscall-0.0.16}/.github/workflows/release.yml +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/.gitignore +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/.python-version +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/LICENSE +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/company.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/errors.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/event.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/exports.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/transcript.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/earningscall/utils.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/hatch.toml +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/scripts/get_all_company_transcripts.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/scripts/get_single_transcript.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/scripts/list_companies.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/setup.cfg +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/data/demo-symbols-v2-alpha.yaml +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/data/demo-symbols-v2.yaml +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/data/msft-transcript-response.yaml +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/data/symbols-v2.yaml +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/data/symbols.txt +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/data/symbols.yaml +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/test_earnings_event.py +0 -0
- {earningscall-0.0.13 → earningscall-0.0.16}/tests/test_helper.py +0 -0
@@ -35,7 +35,7 @@ jobs:
|
|
35
35
|
python-version: ${{ matrix.python-version }}
|
36
36
|
|
37
37
|
- name: Install Hatch
|
38
|
-
run: pip install --upgrade hatch
|
38
|
+
run: pip install --upgrade hatch filelock==3.14.0 # https://github.com/pypa/virtualenv/issues/2735
|
39
39
|
|
40
40
|
- if: matrix.python-version == '3.9' && runner.os == 'Linux'
|
41
41
|
name: Lint
|
@@ -0,0 +1,9 @@
|
|
1
|
+
## Release `0.0.16` - 2024-06-12
|
2
|
+
* Bump version number.
|
3
|
+
|
4
|
+
## Release `0.0.15` - 2024-06-12
|
5
|
+
* Remove `dataclasses` as a project dependency. It is built directly into Python.
|
6
|
+
|
7
|
+
## Release `0.0.14` - 2024-06-08
|
8
|
+
* Add caching to improve client performance.
|
9
|
+
* Add `company_info` attribute in `Company` class.
|
@@ -1,6 +1,46 @@
|
|
1
1
|
# Development
|
2
2
|
|
3
|
-
|
3
|
+
First, install Hatch. See the Hatch [installation instructions](https://hatch.pypa.io/latest/install/).
|
4
|
+
|
5
|
+
|
6
|
+
## Run Build
|
7
|
+
|
8
|
+
```shell
|
9
|
+
hatch build
|
10
|
+
```
|
11
|
+
|
12
|
+
|
13
|
+
### Running Unit Tests
|
14
|
+
|
15
|
+
```shell
|
16
|
+
hatch run test
|
17
|
+
```
|
18
|
+
|
19
|
+
### Run Test Coverage Report
|
20
|
+
|
21
|
+
|
22
|
+
```shell
|
23
|
+
hatch run cov
|
24
|
+
```
|
25
|
+
|
26
|
+
Or, generate HTML report locally:
|
27
|
+
|
28
|
+
```shell
|
29
|
+
coverage html
|
30
|
+
```
|
31
|
+
|
32
|
+
### Run Linter
|
33
|
+
|
34
|
+
```shell
|
35
|
+
hatch run lint:all
|
36
|
+
```
|
37
|
+
|
38
|
+
If you get linter errors, you can automatically fix them by running this command:
|
39
|
+
|
40
|
+
```shell
|
41
|
+
hatch run lint:fmt
|
42
|
+
```
|
43
|
+
|
4
44
|
|
5
45
|
|
6
46
|
### Saving Server-Side Responses for a Mocked Unit test
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: earningscall
|
3
|
-
Version: 0.0.
|
4
|
-
Summary: The EarningsCall Python library.
|
3
|
+
Version: 0.0.16
|
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
|
7
7
|
Project-URL: Repository, https://github.com/EarningsCall/earningscall-python
|
@@ -31,24 +31,40 @@ License: MIT License
|
|
31
31
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
32
32
|
SOFTWARE.
|
33
33
|
License-File: LICENSE
|
34
|
+
Keywords: earnings call,earnings call transcripts,earnings calls,transcripts
|
35
|
+
Classifier: Development Status :: 3 - Alpha
|
36
|
+
Classifier: Intended Audience :: Developers
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
39
|
+
Classifier: Programming Language :: Python :: 3.8
|
40
|
+
Classifier: Programming Language :: Python :: 3.9
|
41
|
+
Classifier: Programming Language :: Python :: 3.10
|
42
|
+
Classifier: Programming Language :: Python :: 3.11
|
43
|
+
Classifier: Programming Language :: Python :: 3.12
|
44
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
45
|
+
Classifier: Typing :: Typed
|
34
46
|
Requires-Python: >=3.8
|
35
47
|
Requires-Dist: dataclasses-json>=0.6.4
|
36
|
-
Requires-Dist: dataclasses>=0.6
|
37
48
|
Requires-Dist: requests-cache>=1.2.0
|
38
49
|
Requires-Dist: requests>=2.30.0
|
39
50
|
Description-Content-Type: text/markdown
|
40
51
|
|
41
52
|
# EarningsCall Python Library
|
42
53
|
|
43
|
-
[](https://pypi.
|
54
|
+
[](https://pypi.org/project/earningscall/)
|
44
55
|
[](https://github.com/EarningsCall/earningscall-python/actions?query=branch%3Amaster)
|
45
56
|
[](https://coveralls.io/github/EarningsCall/earningscall-python?branch=master)
|
57
|
+
[](https://pypi.org/project/earningscall/)
|
46
58
|
|
47
59
|
The EarningsCall Python library provides convenient access to the [EarningsCall API](https://earningscall.biz/api-guide) from
|
48
60
|
applications written in the Python language. It includes a pre-defined set of
|
49
61
|
classes for API resources that initialize themselves dynamically from API
|
50
62
|
responses.
|
51
63
|
|
64
|
+
# Requirements
|
65
|
+
|
66
|
+
* Python 3.8+
|
67
|
+
|
52
68
|
# Installation
|
53
69
|
|
54
70
|
You don't need this source code unless you want to modify the package. If you just want to use the package, just run:
|
@@ -57,10 +73,6 @@ You don't need this source code unless you want to modify the package. If you ju
|
|
57
73
|
pip install --upgrade earningscall
|
58
74
|
```
|
59
75
|
|
60
|
-
# Requirements
|
61
|
-
|
62
|
-
* Python 3.8+
|
63
|
-
|
64
76
|
## Get Transcript for a Single Quarter
|
65
77
|
|
66
78
|
```python
|
@@ -150,3 +162,19 @@ from earningscall import get_sp500_companies
|
|
150
162
|
for company in get_sp500_companies():
|
151
163
|
print(f"{company.company_info} -- {company.company_info.sector} -- {company.company_info.industry}")
|
152
164
|
```
|
165
|
+
|
166
|
+
|
167
|
+
## Advanced
|
168
|
+
|
169
|
+
### Disable Caching
|
170
|
+
|
171
|
+
When you call `get_company("aapl")` to retrieve a company, internally the library retrieves metadata
|
172
|
+
from the EarningsCall API. By default, it caches this metadata on disk in order to speed up subsequent requests.
|
173
|
+
|
174
|
+
If you prefer to disable this local caching behavior, you can do so with this code:
|
175
|
+
|
176
|
+
```python
|
177
|
+
import earningscall
|
178
|
+
|
179
|
+
earningscall.enable_requests_cache = False
|
180
|
+
```
|
@@ -1,14 +1,19 @@
|
|
1
1
|
# EarningsCall Python Library
|
2
2
|
|
3
|
-
[](https://pypi.
|
3
|
+
[](https://pypi.org/project/earningscall/)
|
4
4
|
[](https://github.com/EarningsCall/earningscall-python/actions?query=branch%3Amaster)
|
5
5
|
[](https://coveralls.io/github/EarningsCall/earningscall-python?branch=master)
|
6
|
+
[](https://pypi.org/project/earningscall/)
|
6
7
|
|
7
8
|
The EarningsCall Python library provides convenient access to the [EarningsCall API](https://earningscall.biz/api-guide) from
|
8
9
|
applications written in the Python language. It includes a pre-defined set of
|
9
10
|
classes for API resources that initialize themselves dynamically from API
|
10
11
|
responses.
|
11
12
|
|
13
|
+
# Requirements
|
14
|
+
|
15
|
+
* Python 3.8+
|
16
|
+
|
12
17
|
# Installation
|
13
18
|
|
14
19
|
You don't need this source code unless you want to modify the package. If you just want to use the package, just run:
|
@@ -17,10 +22,6 @@ You don't need this source code unless you want to modify the package. If you ju
|
|
17
22
|
pip install --upgrade earningscall
|
18
23
|
```
|
19
24
|
|
20
|
-
# Requirements
|
21
|
-
|
22
|
-
* Python 3.8+
|
23
|
-
|
24
25
|
## Get Transcript for a Single Quarter
|
25
26
|
|
26
27
|
```python
|
@@ -110,3 +111,19 @@ from earningscall import get_sp500_companies
|
|
110
111
|
for company in get_sp500_companies():
|
111
112
|
print(f"{company.company_info} -- {company.company_info.sector} -- {company.company_info.industry}")
|
112
113
|
```
|
114
|
+
|
115
|
+
|
116
|
+
## Advanced
|
117
|
+
|
118
|
+
### Disable Caching
|
119
|
+
|
120
|
+
When you call `get_company("aapl")` to retrieve a company, internally the library retrieves metadata
|
121
|
+
from the EarningsCall API. By default, it caches this metadata on disk in order to speed up subsequent requests.
|
122
|
+
|
123
|
+
If you prefer to disable this local caching behavior, you can do so with this code:
|
124
|
+
|
125
|
+
```python
|
126
|
+
import earningscall
|
127
|
+
|
128
|
+
earningscall.enable_requests_cache = False
|
129
|
+
```
|
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
# ToDos
|
3
|
+
|
4
|
+
* Add ability to retrieve advanced transcript data
|
5
|
+
* Raise helpful error if the user tries to make a call that the current plan doesn't support
|
6
|
+
* Add documentation on how to disable caching
|
7
|
+
|
8
|
+
|
9
|
+
# Considerations
|
10
|
+
|
11
|
+
* Consider implementing @dataclass and @dataclass_json from scratch to avoid third-party dependency
|
@@ -4,5 +4,6 @@ from earningscall.exports import get_company, get_all_companies, get_sp500_compa
|
|
4
4
|
from earningscall.symbols import Symbols, load_symbols
|
5
5
|
|
6
6
|
api_key: Optional[str] = None
|
7
|
+
enable_requests_cache: bool = True
|
7
8
|
|
8
9
|
__all__ = ["get_company", "get_all_companies", "get_sp500_companies", "Symbols", "load_symbols"]
|
@@ -3,6 +3,7 @@ import os
|
|
3
3
|
from typing import Optional
|
4
4
|
|
5
5
|
import requests
|
6
|
+
from requests_cache import CachedSession
|
6
7
|
|
7
8
|
import earningscall
|
8
9
|
|
@@ -27,15 +28,45 @@ def is_demo_account():
|
|
27
28
|
return get_api_key() == "demo"
|
28
29
|
|
29
30
|
|
30
|
-
def
|
31
|
+
def cache_session() -> CachedSession:
|
32
|
+
return CachedSession(
|
33
|
+
".earningscall_cache",
|
34
|
+
backend="sqlite",
|
35
|
+
cache_control=True,
|
36
|
+
use_temp=True,
|
37
|
+
ignored_parameters=['apikey'],
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
def cached_urls():
|
42
|
+
return cache_session().cache.urls()
|
43
|
+
|
44
|
+
|
45
|
+
def purge_cache():
|
46
|
+
return cache_session().cache.clear()
|
47
|
+
|
48
|
+
|
49
|
+
def do_get(path: str, use_cache: bool = False, **kwargs):
|
50
|
+
params = {
|
51
|
+
**api_key_param(),
|
52
|
+
**kwargs.get("params", {}),
|
53
|
+
}
|
54
|
+
url = f"{API_BASE}/{path}"
|
55
|
+
log.debug(f"do_get url: {url} params: {params}")
|
56
|
+
if use_cache and earningscall.enable_requests_cache:
|
57
|
+
return cache_session().get(url, params=params)
|
58
|
+
else:
|
59
|
+
return requests.get(url, params=params)
|
31
60
|
|
61
|
+
|
62
|
+
def get_events(exchange: str, symbol: str):
|
32
63
|
log.debug(f"get_events exchange: {exchange} symbol: {symbol}")
|
33
64
|
params = {
|
34
65
|
**api_key_param(),
|
35
66
|
"exchange": exchange,
|
36
67
|
"symbol": symbol,
|
37
68
|
}
|
38
|
-
response =
|
69
|
+
response = do_get("events", params=params)
|
39
70
|
if response.status_code != 200:
|
40
71
|
return None
|
41
72
|
return response.json()
|
@@ -51,34 +82,23 @@ def get_transcript(exchange: str, symbol: str, year: int, quarter: int) -> Optio
|
|
51
82
|
"year": str(year),
|
52
83
|
"quarter": str(quarter),
|
53
84
|
}
|
54
|
-
response =
|
85
|
+
response = do_get("transcript", params=params)
|
55
86
|
if response.status_code != 200:
|
56
87
|
return None
|
57
88
|
return response.json()
|
58
89
|
|
59
90
|
|
60
|
-
def get_symbols_v1():
|
61
|
-
response = requests.get(f"{API_BASE}/symbols.txt")
|
62
|
-
if response.status_code != 200:
|
63
|
-
return None
|
64
|
-
return response.text
|
65
|
-
|
66
|
-
|
67
91
|
def get_symbols_v2():
|
68
|
-
|
92
|
+
log.debug("get_symbols_v2")
|
93
|
+
response = do_get("symbols-v2.txt", use_cache=True)
|
69
94
|
if response.status_code != 200:
|
70
95
|
return None
|
71
96
|
return response.text
|
72
97
|
|
73
98
|
|
74
99
|
def get_sp500_companies_txt_file():
|
75
|
-
|
100
|
+
log.debug("get_sp500_companies_txt_file")
|
101
|
+
response = do_get("symbols/sp500.txt", use_cache=True)
|
76
102
|
if response.status_code != 200:
|
77
103
|
return None
|
78
104
|
return response.text
|
79
|
-
|
80
|
-
|
81
|
-
# def do_something():
|
82
|
-
# session = CachedSession('demo_cache', cache_control=True)
|
83
|
-
#
|
84
|
-
# # CachedSession()
|
@@ -1,6 +1,4 @@
|
|
1
|
-
import json
|
2
1
|
import logging
|
3
|
-
from typing import Optional
|
4
2
|
|
5
3
|
log = logging.getLogger(__file__)
|
6
4
|
sectors_file_name = "sectors.json"
|
@@ -179,14 +177,20 @@ def sector_to_index(_sector: str) -> int:
|
|
179
177
|
|
180
178
|
def index_to_sector(_index: int) -> str:
|
181
179
|
if _index == -1:
|
182
|
-
return "
|
183
|
-
|
180
|
+
return "Unknown"
|
181
|
+
try:
|
182
|
+
return SECTORS_IN_ORDER[_index]
|
183
|
+
except IndexError:
|
184
|
+
return "Unknown"
|
184
185
|
|
185
186
|
|
186
187
|
def index_to_industry(_index: int) -> str:
|
187
188
|
if _index == -1:
|
188
|
-
return "
|
189
|
-
|
189
|
+
return "Unknown"
|
190
|
+
try:
|
191
|
+
return INDUSTRIES_IN_ORDER[_index]
|
192
|
+
except IndexError:
|
193
|
+
return "Unknown"
|
190
194
|
|
191
195
|
|
192
196
|
def industry_to_index(_industry: str) -> int:
|
@@ -194,38 +198,3 @@ def industry_to_index(_industry: str) -> int:
|
|
194
198
|
return INDUSTRIES_IN_ORDER.index(_industry)
|
195
199
|
except ValueError:
|
196
200
|
return -1
|
197
|
-
|
198
|
-
|
199
|
-
class Sectors:
|
200
|
-
|
201
|
-
def __init__(self, sectors: Optional[set] = None, industries: Optional[set] = None):
|
202
|
-
if sectors:
|
203
|
-
self.sectors = sectors
|
204
|
-
else:
|
205
|
-
self.sectors = set()
|
206
|
-
if industries:
|
207
|
-
self.industries = industries
|
208
|
-
else:
|
209
|
-
self.industries = set()
|
210
|
-
|
211
|
-
def add_sector(self, sector: str):
|
212
|
-
if sector is not None:
|
213
|
-
self.sectors.add(sector)
|
214
|
-
|
215
|
-
def add_industry(self, industry: str):
|
216
|
-
if industry is not None:
|
217
|
-
self.industries.add(industry)
|
218
|
-
|
219
|
-
def to_dicts(self) -> dict:
|
220
|
-
return {
|
221
|
-
"sectors": list(self.sectors),
|
222
|
-
"industries": list(self.industries),
|
223
|
-
}
|
224
|
-
|
225
|
-
def to_json(self) -> str:
|
226
|
-
return json.dumps(self.to_dicts())
|
227
|
-
|
228
|
-
@staticmethod
|
229
|
-
def from_json(json_str):
|
230
|
-
data = json.loads(json_str)
|
231
|
-
return Sectors(set(data["sectors"]), set(data["industries"]))
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
|
-
import re
|
4
3
|
from collections import defaultdict
|
5
4
|
from typing import Optional, Iterator, List
|
6
5
|
|
@@ -26,7 +25,10 @@ def exchange_to_index(_exchange: Optional[str]) -> int:
|
|
26
25
|
def index_to_exchange(_index: int) -> str:
|
27
26
|
if _index == -1:
|
28
27
|
return "UNKNOWN"
|
29
|
-
|
28
|
+
try:
|
29
|
+
return EXCHANGES_IN_ORDER[_index]
|
30
|
+
except IndexError:
|
31
|
+
return "UNKNOWN"
|
30
32
|
|
31
33
|
|
32
34
|
security_type_pattern = {
|
@@ -47,21 +49,12 @@ class CompanyInfo:
|
|
47
49
|
|
48
50
|
def __init__(self, **kwargs):
|
49
51
|
self.exchange = None
|
52
|
+
self.symbol = None
|
50
53
|
self.name = None
|
51
|
-
self.security_name = None
|
52
54
|
self.sector = None
|
53
55
|
self.industry = None
|
54
56
|
for key, value in kwargs.items():
|
55
57
|
self.__setattr__(key, value)
|
56
|
-
# If name is not set upon initialization, we'll set it from the security name. Also, include
|
57
|
-
# sanitization (removing "Common Stock" from the security name for example)
|
58
|
-
if not self.name:
|
59
|
-
if self.exchange == "OTC":
|
60
|
-
self.security_name = self.security_name.title()
|
61
|
-
if self.exchange in security_type_pattern:
|
62
|
-
self.name = re.sub(security_type_pattern[self.exchange], "", self.security_name)
|
63
|
-
else:
|
64
|
-
self.name = self.security_name
|
65
58
|
|
66
59
|
def __str__(self):
|
67
60
|
return f"({self.exchange}: {self.symbol} - {self.name})"
|
@@ -160,20 +153,6 @@ class Symbols:
|
|
160
153
|
return json.dumps(self.without_security_names())
|
161
154
|
return json.dumps(self.to_dicts())
|
162
155
|
|
163
|
-
# TODO: Test this
|
164
|
-
# def to_json_v2(self) -> str:
|
165
|
-
# return json.dumps(
|
166
|
-
# [
|
167
|
-
# [exchange_to_index(__symbol.exchange), __symbol.company_info, __symbol.name]
|
168
|
-
# for __symbol in self.get_all()
|
169
|
-
# ]
|
170
|
-
# )
|
171
|
-
|
172
|
-
def to_txt(self) -> str:
|
173
|
-
exchange_symbol_names = [__symbol.to_txt_row() for __symbol in self.get_all()]
|
174
|
-
sorted_rows = sorted(exchange_symbol_names, key=lambda row: row[1])
|
175
|
-
return "\n".join(["\t".join(row) for row in sorted_rows])
|
176
|
-
|
177
156
|
def to_txt_v2(self) -> str:
|
178
157
|
exchange_symbol_names = [__symbol.to_txt_v2_row() for __symbol in self.get_all()]
|
179
158
|
sorted_rows = sorted(exchange_symbol_names, key=lambda row: row[1])
|
@@ -186,21 +165,6 @@ class Symbols:
|
|
186
165
|
__symbols.add(CompanyInfo(**item))
|
187
166
|
return __symbols
|
188
167
|
|
189
|
-
@staticmethod
|
190
|
-
def from_json_v2(json_str):
|
191
|
-
__symbols = Symbols()
|
192
|
-
for [_exchange_index, _symbol, _name] in json.loads(json_str):
|
193
|
-
__symbols.add(CompanyInfo(exchange=index_to_exchange(_exchange_index), symbol=_symbol, name=_name))
|
194
|
-
return __symbols
|
195
|
-
|
196
|
-
@staticmethod
|
197
|
-
def from_txt(txt_str):
|
198
|
-
__symbols = Symbols()
|
199
|
-
for line in txt_str.split("\n"):
|
200
|
-
_exchange_index, _symbol, _name = line.split("\t")
|
201
|
-
__symbols.add(CompanyInfo(exchange=index_to_exchange(int(_exchange_index)), symbol=_symbol, name=_name))
|
202
|
-
return __symbols
|
203
|
-
|
204
168
|
@staticmethod
|
205
169
|
def from_txt_v2(txt_str):
|
206
170
|
__symbols = Symbols()
|
@@ -1,19 +1,47 @@
|
|
1
1
|
[project]
|
2
2
|
name = "earningscall"
|
3
|
-
version = "0.0.
|
4
|
-
description = "The EarningsCall Python library."
|
3
|
+
version = "0.0.16"
|
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 = [
|
7
7
|
{name = "EarningsCall", email = "dev@earningscall.biz"},
|
8
8
|
]
|
9
9
|
requires-python = ">= 3.8"
|
10
10
|
dependencies = [
|
11
|
-
"dataclasses>=0.6",
|
12
11
|
"dataclasses-json>=0.6.4",
|
13
12
|
"requests>=2.30.0",
|
14
13
|
"requests-cache>=1.2.0",
|
15
14
|
]
|
16
15
|
license = { file = "LICENSE" }
|
16
|
+
keywords = [
|
17
|
+
"earnings calls",
|
18
|
+
"earnings call",
|
19
|
+
"earnings call transcripts",
|
20
|
+
"transcripts",
|
21
|
+
]
|
22
|
+
classifiers = [
|
23
|
+
# How mature is this project? Common values are
|
24
|
+
# 3 - Alpha
|
25
|
+
# 4 - Beta
|
26
|
+
# 5 - Production/Stable
|
27
|
+
"Development Status :: 3 - Alpha",
|
28
|
+
|
29
|
+
# Indicate who your project is intended for
|
30
|
+
"Intended Audience :: Developers",
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
32
|
+
"Typing :: Typed",
|
33
|
+
|
34
|
+
# Pick your license as you wish (see also "license" above)
|
35
|
+
"License :: OSI Approved :: MIT License",
|
36
|
+
|
37
|
+
# Specify the Python versions you support here.
|
38
|
+
"Programming Language :: Python :: 3",
|
39
|
+
"Programming Language :: Python :: 3.8",
|
40
|
+
"Programming Language :: Python :: 3.9",
|
41
|
+
"Programming Language :: Python :: 3.10",
|
42
|
+
"Programming Language :: Python :: 3.11",
|
43
|
+
"Programming Language :: Python :: 3.12",
|
44
|
+
]
|
17
45
|
|
18
46
|
[project.urls]
|
19
47
|
Homepage = "https://earningscall.biz"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
responses:
|
2
|
+
- response:
|
3
|
+
auto_calculate_content_length: false
|
4
|
+
body: '{"company_name": "Microsoft Corporation", "events": [{"year": 2024, "quarter":
|
5
|
+
3, "conference_date": "2024-04-25T17:30:00.000-04:00"}, {"year": 2024, "quarter":
|
6
|
+
2, "conference_date": "2024-01-30T17:30:00.000-05:00"}, {"year": 2024, "quarter":
|
7
|
+
1, "conference_date": "2023-10-24T14:30:00.000-07:00"}, {"year": 2023, "quarter":
|
8
|
+
4, "conference_date": "2023-07-25T14:30:00.000-07:00"}, {"year": 2023, "quarter":
|
9
|
+
3, "conference_date": "2023-04-25T14:30:00.000-07:00"}, {"year": 2023, "quarter":
|
10
|
+
2, "conference_date": "2023-01-24T14:30:00.000-08:00"}, {"year": 2023, "quarter":
|
11
|
+
1, "conference_date": "2022-10-25T09:00:00.000-04:00"}, {"year": 2022, "quarter":
|
12
|
+
4, "conference_date": "2022-07-26T09:00:00.000-04:00"}, {"year": 2022, "quarter":
|
13
|
+
3, "conference_date": "2022-04-26T14:30:00.000-07:00"}, {"year": 2022, "quarter":
|
14
|
+
2, "conference_date": "2022-01-25T14:30:00.000-08:00"}, {"year": 2022, "quarter":
|
15
|
+
1, "conference_date": "2021-10-26T14:30:00.000-07:00"}, {"year": 2021, "quarter":
|
16
|
+
4, "conference_date": "2021-07-27T14:30:00.000-07:00"}, {"year": 2021, "quarter":
|
17
|
+
3, "conference_date": "2021-04-29T14:30:00.000-07:00"}, {"year": 2021, "quarter":
|
18
|
+
2, "conference_date": "2021-01-26T14:30:00.000-08:00"}, {"year": 2021, "quarter":
|
19
|
+
1, "conference_date": "2020-10-27T14:30:00.000-07:00"}, {"year": 2020, "quarter":
|
20
|
+
4, "conference_date": "2020-07-22T14:30:00.000-07:00"}, {"year": 2020, "quarter":
|
21
|
+
3, "conference_date": "2020-04-29T14:30:00.000-05:00"}, {"year": 2020, "quarter":
|
22
|
+
2, "conference_date": "2020-01-29T14:30:00.000-08:00"}, {"year": 2020, "quarter":
|
23
|
+
1, "conference_date": "2019-10-23T14:30:00.000-07:00"}, {"year": 2019, "quarter":
|
24
|
+
4, "conference_date": "2019-07-18T14:30:00.000-07:00"}]}'
|
25
|
+
content_type: text/plain
|
26
|
+
method: GET
|
27
|
+
status: 200
|
28
|
+
url: https://v2.api.earningscall.biz/events?apikey=demo&exchange=NASDAQ&symbol=MSFT
|
@@ -0,0 +1,10 @@
|
|
1
|
+
responses:
|
2
|
+
- response:
|
3
|
+
auto_calculate_content_length: false
|
4
|
+
body: "99999\tA\tAgilent Technologies, Inc.\t9999\t99999\n-1\tAA\tAlcoa Corporation\t-1\t\
|
5
|
+
-1\n-1\tAAB\tAberdeen International Inc.\t-1\t-1\n1\tAACG\tATA Creativity Global\t\
|
6
|
+
3\t38"
|
7
|
+
content_type: text/plain
|
8
|
+
method: GET
|
9
|
+
status: 200
|
10
|
+
url: https://v2.api.earningscall.biz/symbols-v2.txt
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import responses
|
2
|
+
|
3
|
+
from earningscall import get_company
|
4
|
+
from earningscall.api import purge_cache
|
5
|
+
from earningscall.symbols import clear_symbols
|
6
|
+
from earningscall.utils import data_path
|
7
|
+
|
8
|
+
|
9
|
+
# Uncomment and run following code to generate msft-transcript-response.yaml file
|
10
|
+
#
|
11
|
+
# from responses import _recorder
|
12
|
+
# @_recorder.record(file_path="msft-company-events.yaml")
|
13
|
+
# def test_save_symbols_v1():
|
14
|
+
# requests.get("https://v2.api.earningscall.biz/events?apikey=demo&exchange=NASDAQ&symbol=MSFT")
|
15
|
+
#
|
16
|
+
|
17
|
+
|
18
|
+
@responses.activate
|
19
|
+
def test_get_demo_company():
|
20
|
+
##
|
21
|
+
purge_cache()
|
22
|
+
clear_symbols()
|
23
|
+
responses._add_from_file(file_path=data_path("symbols-v2.yaml"))
|
24
|
+
responses._add_from_file(file_path=data_path("msft-transcript-response.yaml"))
|
25
|
+
responses._add_from_file(file_path=data_path("msft-company-events.yaml"))
|
26
|
+
##
|
27
|
+
company = get_company("msft")
|
28
|
+
events = company.events()
|
29
|
+
##
|
30
|
+
assert len(events) == 20
|
31
|
+
assert events[0].year == 2024
|
32
|
+
assert events[0].quarter == 3
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
import responses
|
4
|
+
|
5
|
+
from earningscall.api import get_sp500_companies_txt_file, cached_urls, purge_cache
|
6
|
+
|
7
|
+
|
8
|
+
@responses.activate
|
9
|
+
def test_load_sp500_tickers():
|
10
|
+
purge_cache()
|
11
|
+
assert len(cached_urls()) == 0
|
12
|
+
##
|
13
|
+
responses.add(
|
14
|
+
responses.GET,
|
15
|
+
"https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=demo",
|
16
|
+
body="AAPL\nMSFT\nTSLA",
|
17
|
+
status=200,
|
18
|
+
adding_headers={
|
19
|
+
"Cache-Control": "public, max-age=3", # Allow client to cache for 3 seconds
|
20
|
+
},
|
21
|
+
)
|
22
|
+
responses.add(
|
23
|
+
responses.GET,
|
24
|
+
"https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=demo",
|
25
|
+
body="AAPL\nMSFT\nTSLA\nNEWCOMPANY",
|
26
|
+
status=200,
|
27
|
+
adding_headers={
|
28
|
+
"cache-control": "public, max-age=30", # Allow client to cache for 30 seconds
|
29
|
+
},
|
30
|
+
)
|
31
|
+
##
|
32
|
+
sp500_raw_text = get_sp500_companies_txt_file()
|
33
|
+
##
|
34
|
+
assert len(responses.calls) == 1
|
35
|
+
assert responses.calls[0].request.url == "https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=demo"
|
36
|
+
assert cached_urls() == ["https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=REDACTED"]
|
37
|
+
tickers = [ticker_symbol for ticker_symbol in sp500_raw_text.split("\n")]
|
38
|
+
assert tickers == ["AAPL", "MSFT", "TSLA"]
|
39
|
+
##
|
40
|
+
# Request a second time, should serve out of cache
|
41
|
+
##
|
42
|
+
sp500_raw_text = get_sp500_companies_txt_file()
|
43
|
+
##
|
44
|
+
assert len(responses.calls) == 1
|
45
|
+
assert responses.calls[0].request.url == "https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=demo"
|
46
|
+
urls = cached_urls()
|
47
|
+
assert urls == ["https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=REDACTED"]
|
48
|
+
tickers = [ticker_symbol for ticker_symbol in sp500_raw_text.split("\n")]
|
49
|
+
assert tickers == ["AAPL", "MSFT", "TSLA"]
|
50
|
+
##
|
51
|
+
# Wait enough time for cache entry to expire
|
52
|
+
##
|
53
|
+
time.sleep(4)
|
54
|
+
##
|
55
|
+
sp500_raw_text = get_sp500_companies_txt_file()
|
56
|
+
##
|
57
|
+
assert len(responses.calls) == 2
|
58
|
+
assert responses.calls[0].request.url == "https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=demo"
|
59
|
+
assert responses.calls[1].request.url == "https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=demo"
|
60
|
+
assert cached_urls() == ["https://v2.api.earningscall.biz/symbols/sp500.txt?apikey=REDACTED"]
|
61
|
+
tickers = [ticker_symbol for ticker_symbol in sp500_raw_text.split("\n")]
|
62
|
+
assert tickers == ["AAPL", "MSFT", "TSLA", "NEWCOMPANY"]
|
@@ -2,6 +2,7 @@ import pytest
|
|
2
2
|
import responses
|
3
3
|
|
4
4
|
from earningscall import get_company
|
5
|
+
from earningscall.api import purge_cache
|
5
6
|
from earningscall.errors import InsufficientApiAccessError
|
6
7
|
from earningscall.symbols import clear_symbols
|
7
8
|
from earningscall.utils import data_path
|
@@ -18,6 +19,7 @@ from earningscall.utils import data_path
|
|
18
19
|
@responses.activate
|
19
20
|
def test_get_demo_company():
|
20
21
|
##
|
22
|
+
purge_cache()
|
21
23
|
clear_symbols()
|
22
24
|
responses._add_from_file(file_path=data_path("symbols-v2.yaml"))
|
23
25
|
responses._add_from_file(file_path=data_path("msft-transcript-response.yaml"))
|
@@ -33,6 +35,7 @@ def test_get_demo_company():
|
|
33
35
|
@responses.activate
|
34
36
|
def test_get_demo_company_with_event_populated():
|
35
37
|
##
|
38
|
+
purge_cache()
|
36
39
|
clear_symbols()
|
37
40
|
responses._add_from_file(file_path=data_path("symbols-v2.yaml"))
|
38
41
|
responses._add_from_file(file_path=data_path("demo-symbols-v2-alpha.yaml"))
|
@@ -75,6 +78,7 @@ def test_get_demo_company_with_event_populated():
|
|
75
78
|
@responses.activate
|
76
79
|
def test_get_non_demo_company():
|
77
80
|
##
|
81
|
+
purge_cache()
|
78
82
|
clear_symbols()
|
79
83
|
responses._add_from_file(file_path=data_path("demo-symbols-v2.yaml"))
|
80
84
|
##
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import requests
|
2
|
+
import responses
|
3
|
+
|
4
|
+
|
5
|
+
# NOTE: this test is only testing the behavior of the responses library, and therefore it is not testing any
|
6
|
+
# code in earningscall at all.
|
7
|
+
|
8
|
+
|
9
|
+
@responses.activate
|
10
|
+
def test_load_sp500_tickers():
|
11
|
+
##
|
12
|
+
responses.add(
|
13
|
+
responses.GET,
|
14
|
+
"http://www.example.com",
|
15
|
+
body="first response",
|
16
|
+
status=200,
|
17
|
+
)
|
18
|
+
responses.add(
|
19
|
+
responses.GET,
|
20
|
+
"http://www.example.com",
|
21
|
+
body="second response",
|
22
|
+
status=200,
|
23
|
+
)
|
24
|
+
##
|
25
|
+
first_response = requests.get("http://www.example.com").text
|
26
|
+
assert first_response == "first response"
|
27
|
+
second_response = requests.get("http://www.example.com").text
|
28
|
+
assert second_response == "second response"
|
29
|
+
# It just returns last response over and over again.
|
30
|
+
second_response = requests.get("http://www.example.com").text
|
31
|
+
assert second_response == "second response"
|
32
|
+
second_response = requests.get("http://www.example.com").text
|
33
|
+
assert second_response == "second response"
|
34
|
+
##
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from earningscall.sectors import index_to_sector, index_to_industry, sector_to_index, industry_to_index
|
2
|
+
|
3
|
+
|
4
|
+
def test_unknown_sector():
|
5
|
+
assert sector_to_index("Bad Sector") == -1
|
6
|
+
|
7
|
+
|
8
|
+
def test_unknown_sector_index():
|
9
|
+
assert index_to_sector(-1) == "Unknown"
|
10
|
+
|
11
|
+
|
12
|
+
def test_unknown_industry():
|
13
|
+
assert industry_to_index("Bad Industry") == -1
|
14
|
+
|
15
|
+
|
16
|
+
def test_unknown_industry_index():
|
17
|
+
assert index_to_industry(-1) == "Unknown"
|
@@ -1,6 +1,7 @@
|
|
1
|
+
import pytest
|
1
2
|
import responses
|
2
3
|
|
3
|
-
from earningscall.api import API_BASE
|
4
|
+
from earningscall.api import API_BASE, purge_cache
|
4
5
|
from earningscall.symbols import Symbols, CompanyInfo
|
5
6
|
from earningscall.utils import data_path
|
6
7
|
|
@@ -8,6 +9,7 @@ from earningscall.utils import data_path
|
|
8
9
|
@responses.activate
|
9
10
|
def test_load_symbols_txt_v2():
|
10
11
|
##
|
12
|
+
purge_cache()
|
11
13
|
responses.patch(API_BASE)
|
12
14
|
responses._add_from_file(file_path=data_path("symbols-v2.yaml"))
|
13
15
|
##
|
@@ -21,10 +23,39 @@ def test_load_symbols_txt_v2():
|
|
21
23
|
assert _symbol.industry == "Consumer Electronics"
|
22
24
|
assert len(responses.calls) == 1
|
23
25
|
assert responses.calls[0].request.url == "https://v2.api.earningscall.biz/symbols-v2.txt?apikey=demo"
|
26
|
+
##
|
27
|
+
symbols.remove_exchange_symbol("NASDAQ_AAPL")
|
28
|
+
with pytest.raises(KeyError):
|
29
|
+
symbols.get_exchange_symbol("NASDAQ_AAPL")
|
30
|
+
|
31
|
+
|
32
|
+
@responses.activate
|
33
|
+
def test_load_symbols_txt_v2_missing_edge_cases():
|
34
|
+
##
|
35
|
+
purge_cache()
|
36
|
+
responses.patch(API_BASE)
|
37
|
+
responses._add_from_file(file_path=data_path("symbols-v2-missing-edge-cases.yaml"))
|
38
|
+
##
|
39
|
+
symbols = Symbols.load_txt_v2()
|
40
|
+
##
|
41
|
+
assert len(symbols) == 4
|
42
|
+
_symbol = symbols.get_exchange_symbol("UNKNOWN_A")
|
43
|
+
assert _symbol.name == "Agilent Technologies, Inc."
|
44
|
+
assert _symbol.exchange == "UNKNOWN"
|
45
|
+
assert _symbol.sector == "Unknown"
|
46
|
+
assert _symbol.industry == "Unknown"
|
47
|
+
_symbol = symbols.get_exchange_symbol("NASDAQ_AACG")
|
48
|
+
assert _symbol.name == "ATA Creativity Global"
|
49
|
+
assert _symbol.exchange == "NASDAQ"
|
50
|
+
assert _symbol.sector == "Consumer Defensive"
|
51
|
+
assert _symbol.industry == "Education & Training Services"
|
52
|
+
assert len(responses.calls) == 1
|
53
|
+
assert responses.calls[0].request.url == "https://v2.api.earningscall.biz/symbols-v2.txt?apikey=demo"
|
24
54
|
|
25
55
|
|
26
56
|
def test_symbols_serialization_to_text_v2():
|
27
57
|
##
|
58
|
+
purge_cache()
|
28
59
|
_symbols = Symbols()
|
29
60
|
_symbols.add(
|
30
61
|
CompanyInfo(
|
@@ -66,3 +97,24 @@ def test_symbols_serialization_to_text_v2():
|
|
66
97
|
assert _deserialized_symbols.get_exchange_symbol("TSX_ACB").sector == "Technology"
|
67
98
|
assert _deserialized_symbols.get_exchange_symbol("TSX_ACB").industry == "Electronic Gaming & Multimedia"
|
68
99
|
assert _deserialized_symbols.get_exchange_symbol("NASDAQ_HITI").name == "High Tide Inc."
|
100
|
+
|
101
|
+
|
102
|
+
def test_symbols_serialization_v1():
|
103
|
+
_symbols = Symbols()
|
104
|
+
_symbols.add(CompanyInfo(exchange="TSX", symbol="TLRY", name="Tilray, Inc", sector="Energy", industry="Uranium"))
|
105
|
+
_symbols.add(
|
106
|
+
CompanyInfo(exchange="TSX", symbol="ACB", name="Aurora Cannabis Inc.", sector="Energy", industry="Uranium")
|
107
|
+
)
|
108
|
+
_symbols.add(
|
109
|
+
CompanyInfo(exchange="NASDAQ", symbol="HITI", name="High Tide Inc.", sector="Energy", industry="Uranium")
|
110
|
+
)
|
111
|
+
##
|
112
|
+
result = _symbols.to_json()
|
113
|
+
##
|
114
|
+
_deserialized_symbols = Symbols.from_json(result)
|
115
|
+
assert len(_deserialized_symbols) == 3
|
116
|
+
assert _deserialized_symbols.get_exchange_symbol("TSX_TLRY").name == "Tilray, Inc"
|
117
|
+
assert _deserialized_symbols.get_exchange_symbol("TSX_TLRY").sector == "Energy"
|
118
|
+
assert _deserialized_symbols.get_exchange_symbol("TSX_TLRY").industry == "Uranium"
|
119
|
+
assert _deserialized_symbols.get_exchange_symbol("TSX_ACB").name == "Aurora Cannabis Inc."
|
120
|
+
assert _deserialized_symbols.get_exchange_symbol("NASDAQ_HITI").name == "High Tide Inc."
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from earningscall.utils import configure_sane_logging, enable_debug_logs, project_file_path
|
2
|
+
|
3
|
+
|
4
|
+
def test_project_file_path_tests_dir():
|
5
|
+
project_file_path("tests", None)
|
6
|
+
|
7
|
+
|
8
|
+
def test_enable_debug_logs():
|
9
|
+
enable_debug_logs()
|
10
|
+
|
11
|
+
|
12
|
+
def test_configure_sane_logging():
|
13
|
+
configure_sane_logging()
|
earningscall-0.0.13/CHANGELOG.md
DELETED
File without changes
|
earningscall-0.0.13/TODO.md
DELETED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|