earningscall 0.0.13__tar.gz → 0.0.14__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 (45) hide show
  1. earningscall-0.0.14/CHANGELOG.md +3 -0
  2. {earningscall-0.0.13 → earningscall-0.0.14}/DEVELOPMENT.md +41 -1
  3. {earningscall-0.0.13 → earningscall-0.0.14}/PKG-INFO +36 -7
  4. {earningscall-0.0.13 → earningscall-0.0.14}/README.md +22 -5
  5. earningscall-0.0.14/TODO.md +11 -0
  6. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/__init__.py +1 -0
  7. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/api.py +38 -18
  8. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/sectors.py +10 -41
  9. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/symbols.py +5 -41
  10. {earningscall-0.0.13 → earningscall-0.0.14}/pyproject.toml +31 -2
  11. earningscall-0.0.14/tests/data/msft-company-events.yaml +28 -0
  12. earningscall-0.0.14/tests/data/symbols-v2-missing-edge-cases.yaml +10 -0
  13. earningscall-0.0.14/tests/test_get_company_events.py +32 -0
  14. earningscall-0.0.14/tests/test_get_sp500_companies_api.py +62 -0
  15. {earningscall-0.0.13 → earningscall-0.0.14}/tests/test_get_transcript.py +4 -0
  16. earningscall-0.0.14/tests/test_responses_mocking.py +34 -0
  17. earningscall-0.0.14/tests/test_sectors.py +17 -0
  18. {earningscall-0.0.13 → earningscall-0.0.14}/tests/test_symbols.py +53 -1
  19. earningscall-0.0.14/tests/test_utils.py +13 -0
  20. earningscall-0.0.13/CHANGELOG.md +0 -0
  21. earningscall-0.0.13/TODO.md +0 -9
  22. {earningscall-0.0.13 → earningscall-0.0.14}/.github/workflows/release.yml +0 -0
  23. {earningscall-0.0.13 → earningscall-0.0.14}/.github/workflows/test.yml +0 -0
  24. {earningscall-0.0.13 → earningscall-0.0.14}/.gitignore +0 -0
  25. {earningscall-0.0.13 → earningscall-0.0.14}/.python-version +0 -0
  26. {earningscall-0.0.13 → earningscall-0.0.14}/LICENSE +0 -0
  27. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/company.py +0 -0
  28. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/errors.py +0 -0
  29. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/event.py +0 -0
  30. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/exports.py +0 -0
  31. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/transcript.py +0 -0
  32. {earningscall-0.0.13 → earningscall-0.0.14}/earningscall/utils.py +0 -0
  33. {earningscall-0.0.13 → earningscall-0.0.14}/hatch.toml +0 -0
  34. {earningscall-0.0.13 → earningscall-0.0.14}/scripts/get_all_company_transcripts.py +0 -0
  35. {earningscall-0.0.13 → earningscall-0.0.14}/scripts/get_single_transcript.py +0 -0
  36. {earningscall-0.0.13 → earningscall-0.0.14}/scripts/list_companies.py +0 -0
  37. {earningscall-0.0.13 → earningscall-0.0.14}/setup.cfg +0 -0
  38. {earningscall-0.0.13 → earningscall-0.0.14}/tests/data/demo-symbols-v2-alpha.yaml +0 -0
  39. {earningscall-0.0.13 → earningscall-0.0.14}/tests/data/demo-symbols-v2.yaml +0 -0
  40. {earningscall-0.0.13 → earningscall-0.0.14}/tests/data/msft-transcript-response.yaml +0 -0
  41. {earningscall-0.0.13 → earningscall-0.0.14}/tests/data/symbols-v2.yaml +0 -0
  42. {earningscall-0.0.13 → earningscall-0.0.14}/tests/data/symbols.txt +0 -0
  43. {earningscall-0.0.13 → earningscall-0.0.14}/tests/data/symbols.yaml +0 -0
  44. {earningscall-0.0.13 → earningscall-0.0.14}/tests/test_earnings_event.py +0 -0
  45. {earningscall-0.0.13 → earningscall-0.0.14}/tests/test_helper.py +0 -0
@@ -0,0 +1,3 @@
1
+ ## Release `0.0.14` - 2024-06-08
2
+ * Add caching to improve client performance.
3
+ * Add `company_info` attribute in `Company` class.
@@ -1,6 +1,46 @@
1
1
  # Development
2
2
 
3
- TODO: Add hatch installation instructions.
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.13
4
- Summary: The EarningsCall Python library.
3
+ Version: 0.0.14
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,6 +31,18 @@ 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
48
  Requires-Dist: dataclasses>=0.6
@@ -40,15 +52,20 @@ Description-Content-Type: text/markdown
40
52
 
41
53
  # EarningsCall Python Library
42
54
 
43
- [![pypi](https://img.shields.io/pypi/v/earningscall.svg)](https://pypi.python.org/pypi/earningscall)
55
+ [![pypi](https://img.shields.io/pypi/v/earningscall.svg)](https://pypi.org/project/earningscall/)
44
56
  [![Build Status](https://github.com/EarningsCall/earningscall-python/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/EarningsCall/earningscall-python/actions?query=branch%3Amaster)
45
57
  [![Coverage Status](https://coveralls.io/repos/github/EarningsCall/earningscall-python/badge.svg?branch=master)](https://coveralls.io/github/EarningsCall/earningscall-python?branch=master)
58
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/earningscall?color=blue)](https://pypi.org/project/earningscall/)
46
59
 
47
60
  The EarningsCall Python library provides convenient access to the [EarningsCall API](https://earningscall.biz/api-guide) from
48
61
  applications written in the Python language. It includes a pre-defined set of
49
62
  classes for API resources that initialize themselves dynamically from API
50
63
  responses.
51
64
 
65
+ # Requirements
66
+
67
+ * Python 3.8+
68
+
52
69
  # Installation
53
70
 
54
71
  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 +74,6 @@ You don't need this source code unless you want to modify the package. If you ju
57
74
  pip install --upgrade earningscall
58
75
  ```
59
76
 
60
- # Requirements
61
-
62
- * Python 3.8+
63
-
64
77
  ## Get Transcript for a Single Quarter
65
78
 
66
79
  ```python
@@ -150,3 +163,19 @@ from earningscall import get_sp500_companies
150
163
  for company in get_sp500_companies():
151
164
  print(f"{company.company_info} -- {company.company_info.sector} -- {company.company_info.industry}")
152
165
  ```
166
+
167
+
168
+ ## Advanced
169
+
170
+ ### Disable Caching
171
+
172
+ When you call `get_company("aapl")` to retrieve a company, internally the library retrieves metadata
173
+ from the EarningsCall API. By default, it caches this metadata on disk in order to speed up subsequent requests.
174
+
175
+ If you prefer to disable this local caching behavior, you can do so with this code:
176
+
177
+ ```python
178
+ import earningscall
179
+
180
+ earningscall.enable_requests_cache = False
181
+ ```
@@ -1,14 +1,19 @@
1
1
  # EarningsCall Python Library
2
2
 
3
- [![pypi](https://img.shields.io/pypi/v/earningscall.svg)](https://pypi.python.org/pypi/earningscall)
3
+ [![pypi](https://img.shields.io/pypi/v/earningscall.svg)](https://pypi.org/project/earningscall/)
4
4
  [![Build Status](https://github.com/EarningsCall/earningscall-python/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/EarningsCall/earningscall-python/actions?query=branch%3Amaster)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/EarningsCall/earningscall-python/badge.svg?branch=master)](https://coveralls.io/github/EarningsCall/earningscall-python?branch=master)
6
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/earningscall?color=blue)](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 get_events(exchange: str, symbol: str):
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 = requests.get(f"{API_BASE}/events", params=params)
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 = requests.get(f"{API_BASE}/transcript", params=params)
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
- response = requests.get(f"{API_BASE}/symbols-v2.txt", params=api_key_param())
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
- response = requests.get(f"{API_BASE}/symbols/sp500.txt", params=api_key_param())
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 "UNKNOWN"
183
- return SECTORS_IN_ORDER[_index]
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 "UNKNOWN"
189
- return INDUSTRIES_IN_ORDER[_index]
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
- return EXCHANGES_IN_ORDER[_index]
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,7 +1,7 @@
1
1
  [project]
2
2
  name = "earningscall"
3
- version = "0.0.13"
4
- description = "The EarningsCall Python library."
3
+ version = "0.0.14"
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"},
@@ -14,6 +14,35 @@ dependencies = [
14
14
  "requests-cache>=1.2.0",
15
15
  ]
16
16
  license = { file = "LICENSE" }
17
+ keywords = [
18
+ "earnings calls",
19
+ "earnings call",
20
+ "earnings call transcripts",
21
+ "transcripts",
22
+ ]
23
+ classifiers = [
24
+ # How mature is this project? Common values are
25
+ # 3 - Alpha
26
+ # 4 - Beta
27
+ # 5 - Production/Stable
28
+ "Development Status :: 3 - Alpha",
29
+
30
+ # Indicate who your project is intended for
31
+ "Intended Audience :: Developers",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ "Typing :: Typed",
34
+
35
+ # Pick your license as you wish (see also "license" above)
36
+ "License :: OSI Approved :: MIT License",
37
+
38
+ # Specify the Python versions you support here.
39
+ "Programming Language :: Python :: 3",
40
+ "Programming Language :: Python :: 3.8",
41
+ "Programming Language :: Python :: 3.9",
42
+ "Programming Language :: Python :: 3.10",
43
+ "Programming Language :: Python :: 3.11",
44
+ "Programming Language :: Python :: 3.12",
45
+ ]
17
46
 
18
47
  [project.urls]
19
48
  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()
File without changes
@@ -1,9 +0,0 @@
1
- # A list of things left to do for this project
2
-
3
- * Cache symbols on disk, to avoid redundant network calls
4
-
5
-
6
-
7
- # Considerations
8
-
9
- * Consider implementing @dataclass and @dataclass_json from scratch to avoid third-party dependency
File without changes
File without changes
File without changes
File without changes