kensho-kfinance 2.2.4__py3-none-any.whl → 2.3.0__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.
Potentially problematic release.
This version of kensho-kfinance might be problematic. Click here for more details.
- {kensho_kfinance-2.2.4.dist-info → kensho_kfinance-2.3.0.dist-info}/METADATA +14 -4
- {kensho_kfinance-2.2.4.dist-info → kensho_kfinance-2.3.0.dist-info}/RECORD +22 -16
- kfinance/CHANGELOG.md +9 -0
- kfinance/fetch.py +26 -10
- kfinance/kfinance.py +227 -11
- kfinance/meta_classes.py +26 -8
- kfinance/pydantic_models.py +33 -0
- kfinance/tests/test_example_notebook.py +4 -2
- kfinance/tests/test_fetch.py +74 -1
- kfinance/tests/test_objects.py +165 -2
- kfinance/tests/test_tools.py +217 -1
- kfinance/tool_calling/__init__.py +8 -0
- kfinance/tool_calling/get_earnings.py +30 -0
- kfinance/tool_calling/get_latest_earnings.py +27 -0
- kfinance/tool_calling/get_next_earnings.py +27 -0
- kfinance/tool_calling/get_transcript.py +23 -0
- kfinance/tool_calling/prompts.py +16 -0
- kfinance/version.py +2 -2
- {kensho_kfinance-2.2.4.dist-info → kensho_kfinance-2.3.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-2.2.4.dist-info → kensho_kfinance-2.3.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.2.4.dist-info → kensho_kfinance-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.2.4.dist-info → kensho_kfinance-2.3.0.dist-info}/top_level.txt +0 -0
kfinance/tests/test_fetch.py
CHANGED
|
@@ -2,9 +2,17 @@ from unittest import TestCase
|
|
|
2
2
|
from unittest.mock import MagicMock
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
+
from requests_mock import Mocker
|
|
5
6
|
|
|
6
|
-
from kfinance.constants import Periodicity, PeriodType
|
|
7
|
+
from kfinance.constants import BusinessRelationshipType, Periodicity, PeriodType
|
|
7
8
|
from kfinance.fetch import KFinanceApiClient
|
|
9
|
+
from kfinance.kfinance import Client
|
|
10
|
+
from kfinance.pydantic_models import (
|
|
11
|
+
CompanyIdAndName,
|
|
12
|
+
RelationshipResponse,
|
|
13
|
+
RelationshipResponseNoName,
|
|
14
|
+
)
|
|
15
|
+
from kfinance.tests.conftest import SPGI_COMPANY_ID
|
|
8
16
|
|
|
9
17
|
|
|
10
18
|
def build_mock_api_client() -> KFinanceApiClient:
|
|
@@ -130,6 +138,18 @@ class TestFetchItem(TestCase):
|
|
|
130
138
|
self.kfinance_api_client.fetch_earnings_dates(company_id=company_id)
|
|
131
139
|
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
132
140
|
|
|
141
|
+
def test_fetch_earnings(self) -> None:
|
|
142
|
+
company_id = 21719
|
|
143
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}earnings/{company_id}"
|
|
144
|
+
self.kfinance_api_client.fetch_earnings(company_id=company_id)
|
|
145
|
+
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
146
|
+
|
|
147
|
+
def test_fetch_transcript(self) -> None:
|
|
148
|
+
key_dev_id = 12345
|
|
149
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}transcript/{key_dev_id}"
|
|
150
|
+
self.kfinance_api_client.fetch_transcript(key_dev_id=key_dev_id)
|
|
151
|
+
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
152
|
+
|
|
133
153
|
def test_fetch_ticker_geography_groups(self) -> None:
|
|
134
154
|
country_iso_code = "USA"
|
|
135
155
|
expected_fetch_url = (
|
|
@@ -279,3 +299,56 @@ class TestMarketCap:
|
|
|
279
299
|
expected_fetch_url = f"{client.url_base}users/permissions"
|
|
280
300
|
client.fetch_permissions()
|
|
281
301
|
client.fetch.assert_called_with(expected_fetch_url)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class TestFetchCompaniesFromBusinessRelationship:
|
|
305
|
+
def test_old_response_format(self, requests_mock: Mocker, mock_client: Client) -> None:
|
|
306
|
+
"""
|
|
307
|
+
GIVEN a business relationship request
|
|
308
|
+
WHEN the api returns a response in the old (no name) format
|
|
309
|
+
THEN the response can successfully be parsed.
|
|
310
|
+
"""
|
|
311
|
+
http_resp = {"current": [883103], "previous": [472898, 8182358]}
|
|
312
|
+
expected_result = RelationshipResponseNoName(current=[883103], previous=[472898, 8182358])
|
|
313
|
+
requests_mock.get(
|
|
314
|
+
url=f"{mock_client.kfinance_api_client.url_base}relationship/{SPGI_COMPANY_ID}/{BusinessRelationshipType.supplier}",
|
|
315
|
+
json=http_resp,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
resp = mock_client.kfinance_api_client.fetch_companies_from_business_relationship(
|
|
319
|
+
company_id=SPGI_COMPANY_ID, relationship_type=BusinessRelationshipType.supplier
|
|
320
|
+
)
|
|
321
|
+
assert resp == expected_result
|
|
322
|
+
|
|
323
|
+
def test_new_response_format(self, requests_mock: Mocker, mock_client: Client) -> None:
|
|
324
|
+
"""
|
|
325
|
+
GIVEN a business relationship request
|
|
326
|
+
WHEN the api returns a response in the new (with name) format
|
|
327
|
+
THEN the response can successfully be parsed.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
http_resp = {
|
|
331
|
+
"current": [{"company_name": "foo", "company_id": 883103}],
|
|
332
|
+
"previous": [
|
|
333
|
+
{"company_name": "bar", "company_id": 472898},
|
|
334
|
+
{"company_name": "baz", "company_id": 8182358},
|
|
335
|
+
],
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
expected_result = RelationshipResponse(
|
|
339
|
+
current=[CompanyIdAndName(company_name="foo", company_id=883103)],
|
|
340
|
+
previous=[
|
|
341
|
+
CompanyIdAndName(company_name="bar", company_id=472898),
|
|
342
|
+
CompanyIdAndName(company_name="baz", company_id=8182358),
|
|
343
|
+
],
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
requests_mock.get(
|
|
347
|
+
url=f"{mock_client.kfinance_api_client.url_base}relationship/{SPGI_COMPANY_ID}/{BusinessRelationshipType.supplier}",
|
|
348
|
+
json=http_resp,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
resp = mock_client.kfinance_api_client.fetch_companies_from_business_relationship(
|
|
352
|
+
company_id=SPGI_COMPANY_ID, relationship_type=BusinessRelationshipType.supplier
|
|
353
|
+
)
|
|
354
|
+
assert resp == expected_result
|
kfinance/tests/test_objects.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import datetime, timezone
|
|
1
|
+
from datetime import date, datetime, timezone
|
|
2
2
|
from io import BytesIO
|
|
3
3
|
import re
|
|
4
4
|
from typing import Optional
|
|
@@ -7,8 +7,9 @@ from unittest import TestCase
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
9
|
from PIL.Image import open as image_open
|
|
10
|
+
import time_machine
|
|
10
11
|
|
|
11
|
-
from kfinance.kfinance import Company, Security, Ticker, TradingItem
|
|
12
|
+
from kfinance.kfinance import Company, Earnings, Security, Ticker, TradingItem, Transcript
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
msft_company_id = "21835"
|
|
@@ -54,6 +55,25 @@ MOCK_COMPANY_DB = {
|
|
|
54
55
|
"iso_country": "USA",
|
|
55
56
|
},
|
|
56
57
|
"earnings_call_dates": {"earnings": ["2004-07-22T21:30:00"]},
|
|
58
|
+
"earnings": {
|
|
59
|
+
"earnings": [
|
|
60
|
+
{
|
|
61
|
+
"name": "Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024",
|
|
62
|
+
"key_dev_id": 1916266380,
|
|
63
|
+
"datetime": "2024-07-25T21:30:00",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "Microsoft Corporation, Q1 2025 Earnings Call, Oct 24, 2024",
|
|
67
|
+
"keydevid": 1916266381,
|
|
68
|
+
"datetime": "2024-10-24T21:30:00",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "Microsoft Corporation, Q2 2025 Earnings Call, Jan 25, 2025",
|
|
72
|
+
"keydevid": 1916266382,
|
|
73
|
+
"datetime": "2025-01-25T21:30:00",
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
},
|
|
57
77
|
"statements": {
|
|
58
78
|
"income_statement": {
|
|
59
79
|
"statements": {
|
|
@@ -91,6 +111,32 @@ MOCK_COMPANY_DB = {
|
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
MOCK_TRANSCRIPT_DB = {
|
|
115
|
+
1916266380: {
|
|
116
|
+
"transcript": [
|
|
117
|
+
{
|
|
118
|
+
"component_type": "Presentation Operator Message",
|
|
119
|
+
"person_name": "Operator",
|
|
120
|
+
"text": "Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"component_type": "Presenter Speech",
|
|
124
|
+
"person_name": "Satya Nadella",
|
|
125
|
+
"text": "Thank you for joining us today. We had an exceptional quarter with strong growth across all segments.",
|
|
126
|
+
},
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
1916266381: {
|
|
130
|
+
"transcript": [
|
|
131
|
+
{
|
|
132
|
+
"component_type": "Presentation Operator Message",
|
|
133
|
+
"person_name": "Operator",
|
|
134
|
+
"text": "Good morning, and welcome to Microsoft's First Quarter 2025 Earnings Conference Call.",
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
94
140
|
|
|
95
141
|
MOCK_SECURITY_DB = {msft_security_id: {"isin": msft_isin, "cusip": msft_cusip}}
|
|
96
142
|
|
|
@@ -215,6 +261,14 @@ class MockKFinanceApiClient:
|
|
|
215
261
|
"""Get a segment"""
|
|
216
262
|
return MOCK_COMPANY_DB[company_id]
|
|
217
263
|
|
|
264
|
+
def fetch_earnings(self, company_id: int) -> dict:
|
|
265
|
+
"""Get the earnings for a company."""
|
|
266
|
+
return MOCK_COMPANY_DB[company_id]["earnings"]
|
|
267
|
+
|
|
268
|
+
def fetch_transcript(self, key_dev_id: int) -> dict:
|
|
269
|
+
"""Get the transcript for an earnings item."""
|
|
270
|
+
return MOCK_TRANSCRIPT_DB[key_dev_id]
|
|
271
|
+
|
|
218
272
|
|
|
219
273
|
class TestTradingItem(TestCase):
|
|
220
274
|
def setUp(self):
|
|
@@ -619,3 +673,112 @@ class TestTicker(TestCase):
|
|
|
619
673
|
expected_dataframe.index.name = "date"
|
|
620
674
|
market_caps = self.msft_ticker_from_ticker.market_cap()
|
|
621
675
|
pd.testing.assert_frame_equal(expected_dataframe, market_caps)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
class TestTranscript(TestCase):
|
|
679
|
+
def setUp(self):
|
|
680
|
+
"""setup tests"""
|
|
681
|
+
self.transcript_components = [
|
|
682
|
+
{
|
|
683
|
+
"component_type": "Presentation Operator Message",
|
|
684
|
+
"person_name": "Operator",
|
|
685
|
+
"text": "Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.",
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
"component_type": "Presenter Speech",
|
|
689
|
+
"person_name": "Satya Nadella",
|
|
690
|
+
"text": "Thank you for joining us today. We had an exceptional quarter with strong growth across all segments.",
|
|
691
|
+
},
|
|
692
|
+
]
|
|
693
|
+
self.transcript = Transcript(self.transcript_components)
|
|
694
|
+
|
|
695
|
+
def test_transcript_length(self):
|
|
696
|
+
"""test transcript length"""
|
|
697
|
+
self.assertEqual(len(self.transcript), 2)
|
|
698
|
+
|
|
699
|
+
def test_transcript_indexing(self):
|
|
700
|
+
"""test transcript indexing"""
|
|
701
|
+
self.assertEqual(
|
|
702
|
+
self.transcript[0].person_name, self.transcript_components[0]["person_name"]
|
|
703
|
+
)
|
|
704
|
+
self.assertEqual(self.transcript[0].text, self.transcript_components[0]["text"])
|
|
705
|
+
self.assertEqual(
|
|
706
|
+
self.transcript[0].component_type, self.transcript_components[0]["component_type"]
|
|
707
|
+
)
|
|
708
|
+
self.assertEqual(
|
|
709
|
+
self.transcript[1].person_name, self.transcript_components[1]["person_name"]
|
|
710
|
+
)
|
|
711
|
+
self.assertEqual(self.transcript[1].text, self.transcript_components[1]["text"])
|
|
712
|
+
self.assertEqual(
|
|
713
|
+
self.transcript[1].component_type, self.transcript_components[1]["component_type"]
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
def test_transcript_raw(self):
|
|
717
|
+
"""test transcript raw property"""
|
|
718
|
+
expected_raw = "Operator: Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.\n\nSatya Nadella: Thank you for joining us today. We had an exceptional quarter with strong growth across all segments."
|
|
719
|
+
self.assertEqual(self.transcript.raw, expected_raw)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class TestEarnings(TestCase):
|
|
723
|
+
def setUp(self):
|
|
724
|
+
"""setup tests"""
|
|
725
|
+
self.kfinance_api_client = MockKFinanceApiClient()
|
|
726
|
+
self.earnings = Earnings(
|
|
727
|
+
kfinance_api_client=self.kfinance_api_client,
|
|
728
|
+
name="Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024",
|
|
729
|
+
datetime=datetime.fromisoformat("2024-07-25T21:30:00").replace(tzinfo=timezone.utc),
|
|
730
|
+
key_dev_id=1916266380,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def test_earnings_attributes(self):
|
|
734
|
+
"""test earnings attributes"""
|
|
735
|
+
self.assertEqual(
|
|
736
|
+
self.earnings.name, "Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024"
|
|
737
|
+
)
|
|
738
|
+
self.assertEqual(self.earnings.key_dev_id, 1916266380)
|
|
739
|
+
expected_datetime = datetime.fromisoformat("2024-07-25T21:30:00").replace(
|
|
740
|
+
tzinfo=timezone.utc
|
|
741
|
+
)
|
|
742
|
+
self.assertEqual(self.earnings.datetime, expected_datetime)
|
|
743
|
+
|
|
744
|
+
def test_earnings_transcript(self):
|
|
745
|
+
"""test earnings transcript property"""
|
|
746
|
+
transcript = self.earnings.transcript
|
|
747
|
+
self.assertIsInstance(transcript, Transcript)
|
|
748
|
+
self.assertEqual(len(transcript), 2)
|
|
749
|
+
self.assertEqual(transcript[0].person_name, "Operator")
|
|
750
|
+
self.assertEqual(transcript[1].person_name, "Satya Nadella")
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class TestCompanyEarnings(TestCase):
|
|
754
|
+
def setUp(self):
|
|
755
|
+
"""setup tests"""
|
|
756
|
+
self.kfinance_api_client = MockKFinanceApiClient()
|
|
757
|
+
self.msft_company = Company(self.kfinance_api_client, msft_company_id)
|
|
758
|
+
|
|
759
|
+
def test_company_earnings(self):
|
|
760
|
+
"""test company earnings method"""
|
|
761
|
+
earnings_list = self.msft_company.earnings()
|
|
762
|
+
self.assertEqual(len(earnings_list), 3)
|
|
763
|
+
self.assertIsInstance(earnings_list[0], Earnings)
|
|
764
|
+
self.assertEqual(earnings_list[0].key_dev_id, 1916266380)
|
|
765
|
+
|
|
766
|
+
def test_company_earnings_with_date_filter(self):
|
|
767
|
+
"""test company earnings method with date filtering"""
|
|
768
|
+
start_date = date(2024, 8, 1)
|
|
769
|
+
end_date = date(2024, 12, 31)
|
|
770
|
+
earnings_list = self.msft_company.earnings(start_date=start_date, end_date=end_date)
|
|
771
|
+
self.assertEqual(len(earnings_list), 1)
|
|
772
|
+
self.assertEqual(earnings_list[0].key_dev_id, 1916266381)
|
|
773
|
+
|
|
774
|
+
@time_machine.travel(datetime(2025, 2, 1, 12, tzinfo=timezone.utc))
|
|
775
|
+
def test_company_latest_earnings(self):
|
|
776
|
+
"""test company latest_earnings property"""
|
|
777
|
+
latest_earnings = self.msft_company.latest_earnings
|
|
778
|
+
self.assertEqual(latest_earnings.key_dev_id, 1916266382)
|
|
779
|
+
|
|
780
|
+
@time_machine.travel(datetime(2024, 6, 1, 12, tzinfo=timezone.utc))
|
|
781
|
+
def test_company_next_earnings(self):
|
|
782
|
+
"""test company next_earnings property"""
|
|
783
|
+
next_earnings = self.msft_company.next_earnings
|
|
784
|
+
self.assertEqual(next_earnings.key_dev_id, 1916266380)
|
kfinance/tests/test_tools.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from datetime import date, datetime
|
|
2
2
|
|
|
3
3
|
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
4
|
+
from pytest import raises
|
|
4
5
|
from requests_mock import Mocker
|
|
5
6
|
import time_machine
|
|
6
7
|
|
|
7
8
|
from kfinance.constants import BusinessRelationshipType, Capitalization, SegmentType, StatementType
|
|
8
|
-
from kfinance.kfinance import Client
|
|
9
|
+
from kfinance.kfinance import Client, NoEarningsDataError
|
|
9
10
|
from kfinance.tests.conftest import SPGI_COMPANY_ID, SPGI_SECURITY_ID, SPGI_TRADING_ITEM_ID
|
|
10
11
|
from kfinance.tool_calling import (
|
|
12
|
+
GetEarnings,
|
|
11
13
|
GetEarningsCallDatetimesFromIdentifier,
|
|
12
14
|
GetFinancialLineItemFromIdentifier,
|
|
13
15
|
GetFinancialStatementFromIdentifier,
|
|
@@ -15,8 +17,11 @@ from kfinance.tool_calling import (
|
|
|
15
17
|
GetInfoFromIdentifier,
|
|
16
18
|
GetIsinFromTicker,
|
|
17
19
|
GetLatest,
|
|
20
|
+
GetLatestEarnings,
|
|
21
|
+
GetNextEarnings,
|
|
18
22
|
GetNQuartersAgo,
|
|
19
23
|
GetPricesFromIdentifier,
|
|
24
|
+
GetTranscript,
|
|
20
25
|
ResolveIdentifier,
|
|
21
26
|
)
|
|
22
27
|
from kfinance.tool_calling.get_business_relationship_from_identifier import (
|
|
@@ -42,6 +47,7 @@ from kfinance.tool_calling.get_segments_from_identifier import (
|
|
|
42
47
|
GetSegmentsFromIdentifier,
|
|
43
48
|
GetSegmentsFromIdentifierArgs,
|
|
44
49
|
)
|
|
50
|
+
from kfinance.tool_calling.get_transcript import GetTranscriptArgs
|
|
45
51
|
from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier
|
|
46
52
|
|
|
47
53
|
|
|
@@ -422,3 +428,213 @@ class TestResolveIdentifier:
|
|
|
422
428
|
"security_id": SPGI_SECURITY_ID,
|
|
423
429
|
"trading_item_id": SPGI_TRADING_ITEM_ID,
|
|
424
430
|
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class TestGetLatestEarnings:
|
|
434
|
+
def test_get_latest_earnings(self, requests_mock: Mocker, mock_client: Client):
|
|
435
|
+
"""
|
|
436
|
+
GIVEN the GetLatestEarnings tool
|
|
437
|
+
WHEN we request the latest earnings for SPGI
|
|
438
|
+
THEN we get back the latest SPGI earnings
|
|
439
|
+
"""
|
|
440
|
+
earnings_data = {
|
|
441
|
+
"earnings": [
|
|
442
|
+
{
|
|
443
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
444
|
+
"datetime": "2025-02-11T13:30:00Z",
|
|
445
|
+
"keydevid": 12345,
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"name": "SPGI Q3 2024 Earnings Call",
|
|
449
|
+
"datetime": "2024-10-30T12:30:00Z",
|
|
450
|
+
"keydevid": 12344,
|
|
451
|
+
},
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
requests_mock.get(
|
|
456
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
457
|
+
json=earnings_data,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
expected_response = {
|
|
461
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
462
|
+
"key_dev_id": 12345,
|
|
463
|
+
"datetime": "2025-02-11T13:30:00+00:00",
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
tool = GetLatestEarnings(kfinance_client=mock_client)
|
|
467
|
+
response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
468
|
+
assert response == expected_response
|
|
469
|
+
|
|
470
|
+
def test_get_latest_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
|
|
471
|
+
"""
|
|
472
|
+
GIVEN the GetLatestEarnings tool
|
|
473
|
+
WHEN we request the latest earnings for a company with no data
|
|
474
|
+
THEN we get a NoEarningsDataError exception
|
|
475
|
+
"""
|
|
476
|
+
earnings_data = {"earnings": []}
|
|
477
|
+
|
|
478
|
+
requests_mock.get(
|
|
479
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
480
|
+
json=earnings_data,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
tool = GetLatestEarnings(kfinance_client=mock_client)
|
|
484
|
+
with raises(NoEarningsDataError, match="Latest earnings for SPGI not found"):
|
|
485
|
+
tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class TestGetNextEarnings:
|
|
489
|
+
def test_get_next_earnings_(self, requests_mock: Mocker, mock_client: Client):
|
|
490
|
+
"""
|
|
491
|
+
GIVEN the GetNextEarnings tool
|
|
492
|
+
WHEN we request the next earnings for SPGI
|
|
493
|
+
THEN we get back the next SPGI earnings
|
|
494
|
+
"""
|
|
495
|
+
earnings_data = {
|
|
496
|
+
"earnings": [
|
|
497
|
+
{
|
|
498
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
499
|
+
"datetime": "2025-04-29T12:30:00Z",
|
|
500
|
+
"keydevid": 12346,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
504
|
+
"datetime": "2025-02-11T13:30:00Z",
|
|
505
|
+
"keydevid": 12345,
|
|
506
|
+
},
|
|
507
|
+
]
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
requests_mock.get(
|
|
511
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
512
|
+
json=earnings_data,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
expected_response = {
|
|
516
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
517
|
+
"key_dev_id": 12346,
|
|
518
|
+
"datetime": "2025-04-29T12:30:00+00:00",
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
with time_machine.travel("2025-03-01T00:00:00+00:00"):
|
|
522
|
+
tool = GetNextEarnings(kfinance_client=mock_client)
|
|
523
|
+
response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
524
|
+
assert response == expected_response
|
|
525
|
+
|
|
526
|
+
def test_get_next_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
|
|
527
|
+
"""
|
|
528
|
+
GIVEN the GetNextEarnings tool
|
|
529
|
+
WHEN we request the next earnings for a company with no data
|
|
530
|
+
THEN we get a NoEarningsDataError exception
|
|
531
|
+
"""
|
|
532
|
+
earnings_data = {"earnings": []}
|
|
533
|
+
|
|
534
|
+
requests_mock.get(
|
|
535
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
536
|
+
json=earnings_data,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
with time_machine.travel("2025-03-01T00:00:00+00:00"):
|
|
540
|
+
tool = GetNextEarnings(kfinance_client=mock_client)
|
|
541
|
+
with raises(NoEarningsDataError, match="Next earnings for SPGI not found"):
|
|
542
|
+
tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class TestGetEarnings:
|
|
546
|
+
def test_get_earnings(self, requests_mock: Mocker, mock_client: Client):
|
|
547
|
+
"""
|
|
548
|
+
GIVEN the GetEarnings tool
|
|
549
|
+
WHEN we request all earnings for SPGI
|
|
550
|
+
THEN we get back all SPGI earnings
|
|
551
|
+
"""
|
|
552
|
+
earnings_data = {
|
|
553
|
+
"earnings": [
|
|
554
|
+
{
|
|
555
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
556
|
+
"datetime": "2025-04-29T12:30:00Z",
|
|
557
|
+
"keydevid": 12346,
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
561
|
+
"datetime": "2025-02-11T13:30:00Z",
|
|
562
|
+
"keydevid": 12345,
|
|
563
|
+
},
|
|
564
|
+
]
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
requests_mock.get(
|
|
568
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
569
|
+
json=earnings_data,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
expected_response = [
|
|
573
|
+
{
|
|
574
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
575
|
+
"key_dev_id": 12346,
|
|
576
|
+
"datetime": "2025-04-29T12:30:00+00:00",
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
580
|
+
"key_dev_id": 12345,
|
|
581
|
+
"datetime": "2025-02-11T13:30:00+00:00",
|
|
582
|
+
},
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
tool = GetEarnings(kfinance_client=mock_client)
|
|
586
|
+
response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
587
|
+
assert response == expected_response
|
|
588
|
+
|
|
589
|
+
def test_get_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
|
|
590
|
+
"""
|
|
591
|
+
GIVEN the GetEarnings tool
|
|
592
|
+
WHEN we request all earnings for a company with no data
|
|
593
|
+
THEN we get a NoEarningslDataError exception
|
|
594
|
+
"""
|
|
595
|
+
earnings_data = {"earnings": []}
|
|
596
|
+
|
|
597
|
+
requests_mock.get(
|
|
598
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
599
|
+
json=earnings_data,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
tool = GetEarnings(kfinance_client=mock_client)
|
|
603
|
+
with raises(NoEarningsDataError, match="Earnings for SPGI not found"):
|
|
604
|
+
tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class TestGetTranscript:
|
|
608
|
+
def test_get_transcript(self, requests_mock: Mocker, mock_client: Client):
|
|
609
|
+
"""
|
|
610
|
+
GIVEN the GetTranscript tool
|
|
611
|
+
WHEN we request a transcript by key_dev_id
|
|
612
|
+
THEN we get back the transcript text
|
|
613
|
+
"""
|
|
614
|
+
transcript_data = {
|
|
615
|
+
"transcript": [
|
|
616
|
+
{
|
|
617
|
+
"person_name": "Operator",
|
|
618
|
+
"text": "Good morning, everyone.",
|
|
619
|
+
"component_type": "speech",
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
"person_name": "CEO",
|
|
623
|
+
"text": "Thank you for joining us today.",
|
|
624
|
+
"component_type": "speech",
|
|
625
|
+
},
|
|
626
|
+
]
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
requests_mock.get(
|
|
630
|
+
url="https://kfinance.kensho.com/api/v1/transcript/12345",
|
|
631
|
+
json=transcript_data,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
expected_response = (
|
|
635
|
+
"Operator: Good morning, everyone.\n\nCEO: Thank you for joining us today."
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
tool = GetTranscript(kfinance_client=mock_client)
|
|
639
|
+
response = tool.run(GetTranscriptArgs(key_dev_id=12345).model_dump(mode="json"))
|
|
640
|
+
assert response == expected_response
|
|
@@ -5,6 +5,7 @@ from kfinance.tool_calling.get_business_relationship_from_identifier import (
|
|
|
5
5
|
)
|
|
6
6
|
from kfinance.tool_calling.get_capitalization_from_identifier import GetCapitalizationFromIdentifier
|
|
7
7
|
from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker
|
|
8
|
+
from kfinance.tool_calling.get_earnings import GetEarnings
|
|
8
9
|
from kfinance.tool_calling.get_earnings_call_datetimes_from_identifier import (
|
|
9
10
|
GetEarningsCallDatetimesFromIdentifier,
|
|
10
11
|
)
|
|
@@ -20,11 +21,14 @@ from kfinance.tool_calling.get_history_metadata_from_identifier import (
|
|
|
20
21
|
from kfinance.tool_calling.get_info_from_identifier import GetInfoFromIdentifier
|
|
21
22
|
from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTicker
|
|
22
23
|
from kfinance.tool_calling.get_latest import GetLatest
|
|
24
|
+
from kfinance.tool_calling.get_latest_earnings import GetLatestEarnings
|
|
23
25
|
from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgo
|
|
26
|
+
from kfinance.tool_calling.get_next_earnings import GetNextEarnings
|
|
24
27
|
from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifier
|
|
25
28
|
from kfinance.tool_calling.get_segments_from_identifier import (
|
|
26
29
|
GetSegmentsFromIdentifier,
|
|
27
30
|
)
|
|
31
|
+
from kfinance.tool_calling.get_transcript import GetTranscript
|
|
28
32
|
from kfinance.tool_calling.resolve_identifier import ResolveIdentifier
|
|
29
33
|
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
30
34
|
|
|
@@ -36,6 +40,10 @@ ALL_TOOLS: list[Type[KfinanceTool]] = [
|
|
|
36
40
|
GetCusipFromTicker,
|
|
37
41
|
GetInfoFromIdentifier,
|
|
38
42
|
GetEarningsCallDatetimesFromIdentifier,
|
|
43
|
+
GetEarnings,
|
|
44
|
+
GetLatestEarnings,
|
|
45
|
+
GetNextEarnings,
|
|
46
|
+
GetTranscript,
|
|
39
47
|
GetHistoryMetadataFromIdentifier,
|
|
40
48
|
GetPricesFromIdentifier,
|
|
41
49
|
GetCapitalizationFromIdentifier,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.kfinance import NoEarningsDataError
|
|
7
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GetEarnings(KfinanceTool):
|
|
11
|
+
name: str = "get_earnings"
|
|
12
|
+
description: str = "Get all earnings for a given identifier. Returns a list of dictionaries, each with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
|
|
13
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
14
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
15
|
+
|
|
16
|
+
def _run(self, identifier: str) -> list[dict]:
|
|
17
|
+
ticker = self.kfinance_client.ticker(identifier)
|
|
18
|
+
earnings = ticker.company.earnings()
|
|
19
|
+
|
|
20
|
+
if not earnings:
|
|
21
|
+
raise NoEarningsDataError(f"Earnings for {identifier} not found")
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
"name": earnings_item.name,
|
|
26
|
+
"key_dev_id": earnings_item.key_dev_id,
|
|
27
|
+
"datetime": earnings_item.datetime.isoformat(),
|
|
28
|
+
}
|
|
29
|
+
for earnings_item in earnings
|
|
30
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.kfinance import NoEarningsDataError
|
|
7
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GetLatestEarnings(KfinanceTool):
|
|
11
|
+
name: str = "get_latest_earnings"
|
|
12
|
+
description: str = "Get the latest earnings for a given identifier. Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
|
|
13
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
14
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
15
|
+
|
|
16
|
+
def _run(self, identifier: str) -> dict:
|
|
17
|
+
ticker = self.kfinance_client.ticker(identifier)
|
|
18
|
+
latest_earnings = ticker.company.latest_earnings
|
|
19
|
+
|
|
20
|
+
if latest_earnings is None:
|
|
21
|
+
raise NoEarningsDataError(f"Latest earnings for {identifier} not found")
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"name": latest_earnings.name,
|
|
25
|
+
"key_dev_id": latest_earnings.key_dev_id,
|
|
26
|
+
"datetime": latest_earnings.datetime.isoformat(),
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.kfinance import NoEarningsDataError
|
|
7
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GetNextEarnings(KfinanceTool):
|
|
11
|
+
name: str = "get_next_earnings"
|
|
12
|
+
description: str = "Get the next earnings for a given identifier. Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
|
|
13
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
14
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
15
|
+
|
|
16
|
+
def _run(self, identifier: str) -> dict:
|
|
17
|
+
ticker = self.kfinance_client.ticker(identifier)
|
|
18
|
+
next_earnings = ticker.company.next_earnings
|
|
19
|
+
|
|
20
|
+
if next_earnings is None:
|
|
21
|
+
raise NoEarningsDataError(f"Next earnings for {identifier} not found")
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"name": next_earnings.name,
|
|
25
|
+
"key_dev_id": next_earnings.key_dev_id,
|
|
26
|
+
"datetime": next_earnings.datetime.isoformat(),
|
|
27
|
+
}
|