kensho-kfinance 2.9.0__py3-none-any.whl → 3.0.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.

Files changed (134) hide show
  1. {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/METADATA +1 -1
  2. kensho_kfinance-3.0.0.dist-info/RECORD +110 -0
  3. kfinance/CHANGELOG.md +3 -0
  4. kfinance/__init__.py +1 -0
  5. kfinance/client/README.md +9 -0
  6. kfinance/{batch_request_handling.py → client/batch_request_handling.py} +63 -27
  7. kfinance/{fetch.py → client/fetch.py} +23 -29
  8. kfinance/{kfinance.py → client/kfinance.py} +37 -33
  9. kfinance/{meta_classes.py → client/meta_classes.py} +26 -35
  10. kfinance/{decimal_with_unit.py → client/models/decimal_with_unit.py} +1 -1
  11. kfinance/{tests → client/models/tests}/test_decimal_with_unit.py +1 -1
  12. kfinance/client/tests/__init__.py +0 -0
  13. kfinance/{tests → client/tests}/test_batch_requests.py +8 -6
  14. kfinance/{tests → client/tests}/test_client.py +25 -19
  15. kfinance/{tests → client/tests}/test_fetch.py +11 -29
  16. kfinance/{tests → client/tests}/test_group_objects.py +1 -1
  17. kfinance/{tests → client/tests}/test_objects.py +33 -29
  18. kfinance/{tests/conftest.py → conftest.py} +14 -2
  19. kfinance/domains/README.md +14 -0
  20. kfinance/domains/__init__.py +0 -0
  21. kfinance/domains/business_relationships/__init__.py +0 -0
  22. kfinance/{models → domains/business_relationships}/business_relationship_models.py +10 -0
  23. kfinance/domains/business_relationships/business_relationship_tools.py +74 -0
  24. kfinance/domains/business_relationships/tests/__init__.py +0 -0
  25. kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +55 -0
  26. kfinance/domains/capitalizations/__init__.py +0 -0
  27. kfinance/{models → domains/capitalizations}/capitalization_models.py +24 -17
  28. kfinance/domains/capitalizations/capitalization_tools.py +89 -0
  29. kfinance/domains/capitalizations/tests/__init__.py +0 -0
  30. kfinance/{tests/test_models → domains/capitalizations/tests}/test_capitalization_models.py +8 -10
  31. kfinance/domains/capitalizations/tests/test_capitalization_tools.py +85 -0
  32. kfinance/domains/companies/__init__.py +0 -0
  33. kfinance/domains/companies/company_identifiers.py +175 -0
  34. kfinance/domains/companies/company_models.py +27 -0
  35. kfinance/domains/companies/company_tools.py +66 -0
  36. kfinance/domains/companies/tests/__init__.py +0 -0
  37. kfinance/domains/companies/tests/test_company_tools.py +26 -0
  38. kfinance/domains/competitors/__init__.py +0 -0
  39. kfinance/{models → domains/competitors}/competitor_models.py +7 -0
  40. kfinance/domains/competitors/competitor_tools.py +62 -0
  41. kfinance/domains/competitors/tests/__init__.py +0 -0
  42. kfinance/domains/competitors/tests/test_competitor_tools.py +45 -0
  43. kfinance/domains/cusip_and_isin/__init__.py +0 -0
  44. kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +80 -0
  45. kfinance/domains/cusip_and_isin/tests/__init__.py +0 -0
  46. kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +57 -0
  47. kfinance/domains/earnings/__init__.py +0 -0
  48. kfinance/domains/earnings/earning_models.py +41 -0
  49. kfinance/domains/earnings/earning_tools.py +174 -0
  50. kfinance/domains/earnings/tests/__init__.py +0 -0
  51. kfinance/domains/earnings/tests/test_earnings_tools.py +195 -0
  52. kfinance/domains/line_items/__init__.py +0 -0
  53. kfinance/domains/line_items/line_item_tools.py +114 -0
  54. kfinance/domains/line_items/tests/__init__.py +0 -0
  55. kfinance/domains/line_items/tests/test_line_item_tools.py +86 -0
  56. kfinance/domains/mergers_and_acquisitions/__init__.py +0 -0
  57. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +176 -0
  58. kfinance/domains/mergers_and_acquisitions/tests/__init__.py +0 -0
  59. kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +124 -0
  60. kfinance/domains/prices/__init__.py +0 -0
  61. kfinance/{models → domains/prices}/price_models.py +1 -1
  62. kfinance/domains/prices/price_tools.py +165 -0
  63. kfinance/domains/prices/tests/__init__.py +0 -0
  64. kfinance/{tests/test_models → domains/prices/tests}/test_price_models.py +2 -2
  65. kfinance/domains/prices/tests/test_price_tools.py +141 -0
  66. kfinance/domains/segments/__init__.py +0 -0
  67. kfinance/domains/segments/segment_tools.py +91 -0
  68. kfinance/domains/segments/tests/__init__.py +0 -0
  69. kfinance/domains/segments/tests/test_segment_tools.py +80 -0
  70. kfinance/domains/statements/__init__.py +0 -0
  71. kfinance/domains/statements/statement_tools.py +113 -0
  72. kfinance/domains/statements/tests/__init__.py +0 -0
  73. kfinance/domains/statements/tests/test_statement_tools.py +73 -0
  74. kfinance/integrations/README.md +8 -0
  75. kfinance/integrations/__init__.py +0 -0
  76. kfinance/integrations/mcp/__init__.py +0 -0
  77. kfinance/{mcp.py → integrations/mcp/mcp.py} +2 -2
  78. kfinance/integrations/tests/__init__.py +0 -0
  79. kfinance/{tests → integrations/tests}/test_example_notebook.py +4 -4
  80. kfinance/{tool_calling → integrations/tool_calling}/README.md +2 -2
  81. kfinance/integrations/tool_calling/__init__.py +0 -0
  82. kfinance/integrations/tool_calling/all_tools.py +55 -0
  83. kfinance/{tool_calling → integrations/tool_calling}/prompts.py +3 -2
  84. kfinance/integrations/tool_calling/static_tools/README.md +4 -0
  85. kfinance/integrations/tool_calling/static_tools/__init__.py +0 -0
  86. kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_latest.py +3 -3
  87. kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_n_quarters_ago.py +3 -3
  88. kfinance/integrations/tool_calling/static_tools/tests/__init__.py +0 -0
  89. kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +30 -0
  90. kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +24 -0
  91. kfinance/integrations/tool_calling/tests/__init__.py +0 -0
  92. kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +69 -0
  93. kfinance/{tool_calling/shared_models.py → integrations/tool_calling/tool_calling_models.py} +37 -7
  94. kfinance/version.py +2 -2
  95. kensho_kfinance-2.9.0.dist-info/RECORD +0 -70
  96. kfinance/models/id_models.py +0 -7
  97. kfinance/prompt.py +0 -526
  98. kfinance/pydantic_models.py +0 -33
  99. kfinance/tests/test_tools.py +0 -804
  100. kfinance/tool_calling/__init__.py +0 -53
  101. kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +0 -42
  102. kfinance/tool_calling/get_business_relationship_from_identifier.py +0 -30
  103. kfinance/tool_calling/get_capitalization_from_identifier.py +0 -35
  104. kfinance/tool_calling/get_competitors_from_identifier.py +0 -25
  105. kfinance/tool_calling/get_cusip_from_ticker.py +0 -20
  106. kfinance/tool_calling/get_earnings.py +0 -33
  107. kfinance/tool_calling/get_financial_line_item_from_identifier.py +0 -48
  108. kfinance/tool_calling/get_financial_statement_from_identifier.py +0 -44
  109. kfinance/tool_calling/get_history_metadata_from_identifier.py +0 -17
  110. kfinance/tool_calling/get_info_from_identifier.py +0 -16
  111. kfinance/tool_calling/get_isin_from_ticker.py +0 -20
  112. kfinance/tool_calling/get_latest_earnings.py +0 -30
  113. kfinance/tool_calling/get_merger_info_from_transaction_id.py +0 -69
  114. kfinance/tool_calling/get_mergers_from_identifier.py +0 -44
  115. kfinance/tool_calling/get_next_earnings.py +0 -30
  116. kfinance/tool_calling/get_prices_from_identifier.py +0 -46
  117. kfinance/tool_calling/get_segments_from_identifier.py +0 -44
  118. kfinance/tool_calling/get_transcript.py +0 -23
  119. kfinance/tool_calling/resolve_identifier.py +0 -18
  120. {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/WHEEL +0 -0
  121. {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/AUTHORS.md +0 -0
  122. {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/LICENSE +0 -0
  123. {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/top_level.txt +0 -0
  124. /kfinance/{models → client}/__init__.py +0 -0
  125. /kfinance/{models → client}/industry_models.py +0 -0
  126. /kfinance/{tests → client/models}/__init__.py +0 -0
  127. /kfinance/{models → client/models}/currency_models.py +0 -0
  128. /kfinance/{models → client/models}/date_and_period_models.py +0 -0
  129. /kfinance/{tests/test_models → client/models/tests}/__init__.py +0 -0
  130. /kfinance/{models → client}/permission_models.py +0 -0
  131. /kfinance/{server_thread.py → client/server_thread.py} +0 -0
  132. /kfinance/{models → domains/line_items}/line_item_models.py +0 -0
  133. /kfinance/{models → domains/segments}/segment_models.py +0 -0
  134. /kfinance/{models → domains/statements}/statement_models.py +0 -0
@@ -2,18 +2,20 @@ from unittest.mock import Mock
2
2
 
3
3
  import pytest
4
4
 
5
- from kfinance.kfinance import Client
6
- from kfinance.models.permission_models import Permission
7
- from kfinance.tool_calling import (
8
- GetBusinessRelationshipFromIdentifier,
9
- GetEarnings,
10
- GetFinancialStatementFromIdentifier,
11
- GetLatest,
12
- GetLatestEarnings,
13
- GetNextEarnings,
14
- GetTranscript,
5
+ from kfinance.client.kfinance import Client
6
+ from kfinance.client.permission_models import Permission
7
+ from kfinance.domains.business_relationships.business_relationship_tools import (
8
+ GetBusinessRelationshipFromIdentifiers,
15
9
  )
16
- from kfinance.tool_calling.shared_models import KfinanceTool
10
+ from kfinance.domains.earnings.earning_tools import (
11
+ GetEarningsFromIdentifiers,
12
+ GetLatestEarningsFromIdentifiers,
13
+ GetNextEarningsFromIdentifiers,
14
+ GetTranscriptFromKeyDevId,
15
+ )
16
+ from kfinance.domains.statements.statement_tools import GetFinancialStatementFromIdentifiers
17
+ from kfinance.integrations.tool_calling.static_tools.get_latest import GetLatest
18
+ from kfinance.integrations.tool_calling.tool_calling_models import KfinanceTool
17
19
 
18
20
 
19
21
  class TestLangchainTools:
@@ -52,19 +54,23 @@ class TestLangchainTools:
52
54
  mock_client.kfinance_api_client._user_permissions = {Permission.RelationshipPermission} # noqa: SLF001
53
55
  tool_classes = [type(t) for t in mock_client.langchain_tools]
54
56
  # User should have access to GetBusinessRelationshipFromIdentifier
55
- assert GetBusinessRelationshipFromIdentifier in tool_classes
57
+ assert GetBusinessRelationshipFromIdentifiers in tool_classes
56
58
  # User should have access to functions that don't require permissions
57
59
  assert GetLatest in tool_classes
58
60
  # User should not have access to functions that require statement permissions
59
- assert GetFinancialStatementFromIdentifier not in tool_classes
61
+ assert GetFinancialStatementFromIdentifiers not in tool_classes
60
62
 
61
63
  @pytest.mark.parametrize(
62
64
  "user_permission, expected_tool",
63
65
  [
64
66
  pytest.param(
65
- Permission.TranscriptsPermission, GetTranscript, id="Transcript Permissions"
67
+ Permission.TranscriptsPermission,
68
+ GetTranscriptFromKeyDevId,
69
+ id="Transcript Permissions",
70
+ ),
71
+ pytest.param(
72
+ Permission.EarningsPermission, GetEarningsFromIdentifiers, id="Earnings Permissions"
66
73
  ),
67
- pytest.param(Permission.EarningsPermission, GetEarnings, id="Earnings Permissions"),
68
74
  ],
69
75
  )
70
76
  def test_permission_set_handling(
@@ -79,11 +85,11 @@ class TestLangchainTools:
79
85
  mock_client.kfinance_api_client._user_permissions = {user_permission} # noqa: SLF001
80
86
  tool_classes = [type(t) for t in mock_client.langchain_tools]
81
87
  # User should have access to GetEarnings, GetNextEarnings, GetLatestEarnings, GetTranscript
82
- assert GetEarnings in tool_classes
83
- assert GetNextEarnings in tool_classes
84
- assert GetLatestEarnings in tool_classes
88
+ assert GetEarningsFromIdentifiers in tool_classes
89
+ assert GetNextEarningsFromIdentifiers in tool_classes
90
+ assert GetLatestEarningsFromIdentifiers in tool_classes
85
91
  assert expected_tool in tool_classes
86
92
  # User should have access to functions that don't require permissions
87
93
  assert GetLatest in tool_classes
88
94
  # User should not have access to functions that require statement permissions
89
- assert GetFinancialStatementFromIdentifier not in tool_classes
95
+ assert GetFinancialStatementFromIdentifiers not in tool_classes
@@ -5,16 +5,15 @@ from pydantic import ValidationError
5
5
  import pytest
6
6
  from requests_mock import Mocker
7
7
 
8
- from kfinance.fetch import KFinanceApiClient
9
- from kfinance.kfinance import Client
10
- from kfinance.models.business_relationship_models import BusinessRelationshipType
11
- from kfinance.models.date_and_period_models import Periodicity, PeriodType
12
- from kfinance.pydantic_models import (
13
- CompanyIdAndName,
8
+ from kfinance.client.fetch import KFinanceApiClient
9
+ from kfinance.client.kfinance import Client
10
+ from kfinance.client.models.date_and_period_models import Periodicity, PeriodType
11
+ from kfinance.conftest import SPGI_COMPANY_ID
12
+ from kfinance.domains.business_relationships.business_relationship_models import (
13
+ BusinessRelationshipType,
14
14
  RelationshipResponse,
15
- RelationshipResponseNoName,
16
15
  )
17
- from kfinance.tests.conftest import SPGI_COMPANY_ID
16
+ from kfinance.domains.companies.company_models import CompanyIdAndName
18
17
 
19
18
 
20
19
  def build_mock_api_client() -> KFinanceApiClient:
@@ -150,7 +149,8 @@ class TestFetchItem(TestCase):
150
149
  def test_fetch_earnings(self) -> None:
151
150
  company_id = 21719
152
151
  expected_fetch_url = f"{self.kfinance_api_client.url_base}earnings/{company_id}"
153
- self.kfinance_api_client.fetch_earnings(company_id=company_id)
152
+ with pytest.raises(ValidationError):
153
+ self.kfinance_api_client.fetch_earnings(company_id=company_id)
154
154
  self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
155
155
 
156
156
  def test_fetch_transcript(self) -> None:
@@ -334,28 +334,10 @@ class TestMarketCap:
334
334
 
335
335
 
336
336
  class TestFetchCompaniesFromBusinessRelationship:
337
- def test_old_response_format(self, requests_mock: Mocker, mock_client: Client) -> None:
338
- """
339
- GIVEN a business relationship request
340
- WHEN the api returns a response in the old (no name) format
341
- THEN the response can successfully be parsed.
342
- """
343
- http_resp = {"current": [883103], "previous": [472898, 8182358]}
344
- expected_result = RelationshipResponseNoName(current=[883103], previous=[472898, 8182358])
345
- requests_mock.get(
346
- url=f"{mock_client.kfinance_api_client.url_base}relationship/{SPGI_COMPANY_ID}/{BusinessRelationshipType.supplier}",
347
- json=http_resp,
348
- )
349
-
350
- resp = mock_client.kfinance_api_client.fetch_companies_from_business_relationship(
351
- company_id=SPGI_COMPANY_ID, relationship_type=BusinessRelationshipType.supplier
352
- )
353
- assert resp == expected_result
354
-
355
- def test_new_response_format(self, requests_mock: Mocker, mock_client: Client) -> None:
337
+ def test_fetch_business_relationships(self, requests_mock: Mocker, mock_client: Client) -> None:
356
338
  """
357
339
  GIVEN a business relationship request
358
- WHEN the api returns a response in the new (with name) format
340
+ WHEN the api returns a response
359
341
  THEN the response can successfully be parsed.
360
342
  """
361
343
 
@@ -1,6 +1,6 @@
1
1
  from unittest.mock import Mock
2
2
 
3
- from kfinance.kfinance import Client, IdentificationTriple, Tickers
3
+ from kfinance.client.kfinance import Client, IdentificationTriple, Tickers
4
4
 
5
5
 
6
6
  class TestTickers:
@@ -10,7 +10,7 @@ import pandas as pd
10
10
  from PIL.Image import open as image_open
11
11
  import time_machine
12
12
 
13
- from kfinance.kfinance import (
13
+ from kfinance.client.kfinance import (
14
14
  BusinessRelationships,
15
15
  Company,
16
16
  Earnings,
@@ -21,9 +21,13 @@ from kfinance.kfinance import (
21
21
  TradingItem,
22
22
  Transcript,
23
23
  )
24
- from kfinance.models.business_relationship_models import BusinessRelationshipType
25
- from kfinance.models.capitalization_models import Capitalizations
26
- from kfinance.pydantic_models import CompanyIdAndName, RelationshipResponse
24
+ from kfinance.domains.business_relationships.business_relationship_models import (
25
+ BusinessRelationshipType,
26
+ RelationshipResponse,
27
+ )
28
+ from kfinance.domains.capitalizations.capitalization_models import Capitalizations
29
+ from kfinance.domains.companies.company_models import CompanyIdAndName
30
+ from kfinance.domains.earnings.earning_models import EarningsCallResp
27
31
 
28
32
 
29
33
  msft_company_id = "21835"
@@ -70,25 +74,27 @@ MOCK_COMPANY_DB = {
70
74
  "iso_country": "USA",
71
75
  },
72
76
  "earnings_call_dates": {"earnings": ["2004-07-22T21:30:00"]},
73
- "earnings": {
74
- "earnings": [
75
- {
76
- "name": "Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024",
77
- "key_dev_id": 1916266380,
78
- "datetime": "2024-07-25T21:30:00",
79
- },
80
- {
81
- "name": "Microsoft Corporation, Q1 2025 Earnings Call, Oct 24, 2024",
82
- "keydevid": 1916266381,
83
- "datetime": "2024-10-24T21:30:00",
84
- },
85
- {
86
- "name": "Microsoft Corporation, Q2 2025 Earnings Call, Jan 25, 2025",
87
- "keydevid": 1916266382,
88
- "datetime": "2025-01-25T21:30:00",
89
- },
90
- ]
91
- },
77
+ "earnings": EarningsCallResp.model_validate(
78
+ {
79
+ "earnings": [
80
+ {
81
+ "name": "Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024",
82
+ "key_dev_id": 1916266380,
83
+ "datetime": "2024-07-25T21:30:00Z",
84
+ },
85
+ {
86
+ "name": "Microsoft Corporation, Q1 2025 Earnings Call, Oct 24, 2024",
87
+ "key_dev_id": 1916266381,
88
+ "datetime": "2024-10-24T21:30:00Z",
89
+ },
90
+ {
91
+ "name": "Microsoft Corporation, Q2 2025 Earnings Call, Jan 25, 2025",
92
+ "key_dev_id": 1916266382,
93
+ "datetime": "2025-01-25T21:30:00Z",
94
+ },
95
+ ]
96
+ }
97
+ ),
92
98
  "statements": {
93
99
  "income_statement": {
94
100
  "statements": {
@@ -890,12 +896,10 @@ class TestTicker(TestCase):
890
896
  THEN the Ticker object can correctly extract market caps from the dict.
891
897
  """
892
898
 
893
- expected_response = {
894
- "market_cap": [
895
- {"2025-01-01": {"unit": "USD", "value": "3133802247084.00"}},
896
- {"2025-01-02": {"unit": "USD", "value": "3112092395218.00"}},
897
- ]
898
- }
899
+ expected_response = [
900
+ {"date": "2025-01-01", "market_cap": {"unit": "USD", "value": "3133802247084.00"}},
901
+ {"date": "2025-01-02", "market_cap": {"unit": "USD", "value": "3112092395218.00"}},
902
+ ]
899
903
  market_caps = self.msft_ticker_from_ticker.market_cap()
900
904
  assert market_caps == expected_response
901
905
 
@@ -3,7 +3,7 @@ from datetime import datetime
3
3
  import pytest
4
4
  from requests_mock import Mocker
5
5
 
6
- from kfinance.kfinance import Client
6
+ from kfinance.client.kfinance import Client
7
7
 
8
8
 
9
9
  SPGI_COMPANY_ID = 21719
@@ -18,7 +18,7 @@ def mock_client(requests_mock: Mocker) -> Client:
18
18
  client = Client(refresh_token="foo")
19
19
  # Set access token so that the client doesn't try to fetch it.
20
20
  client.kfinance_api_client._access_token = "foo" # noqa: SLF001
21
- client.kfinance_api_client._access_token_expiry = datetime(2100, 1, 1).timestamp() # noqa: SLF001
21
+ client.kfinance_api_client._access_token_expiry = int(datetime(2100, 1, 1).timestamp()) # noqa: SLF001
22
22
 
23
23
  # Create a mock for the SPGI id triple.
24
24
  requests_mock.get(
@@ -33,4 +33,16 @@ def mock_client(requests_mock: Mocker) -> Client:
33
33
  url="https://kfinance.kensho.com/api/v1/id/MSFT",
34
34
  json={"trading_item_id": 2630413, "security_id": 2630412, "company_id": 21835},
35
35
  )
36
+
37
+ # Create mock security id and trading item id for company ids 1 and 2:
38
+ for company_id in [1, 2]:
39
+ requests_mock.get(
40
+ url=f"https://kfinance.kensho.com/api/v1/securities/{company_id}/primary",
41
+ json={"primary_security": company_id},
42
+ )
43
+ requests_mock.get(
44
+ url=f"https://kfinance.kensho.com/api/v1/trading_items/{company_id}/primary",
45
+ json={"primary_trading_item": company_id},
46
+ )
47
+
36
48
  return client
@@ -0,0 +1,14 @@
1
+ # Domains
2
+
3
+ Domains loosely correspond to datasets and permission boundaries.
4
+ They are used to group together related tools and models,
5
+ for example all earnings related tools and models.
6
+
7
+ There are no hard boundaries around what constitutes a domain. If you
8
+ think it makes sense to group certain tools or models together, it probably does.
9
+
10
+ Each domain will usually have a tools file with `KfinanceTools` and a
11
+ models file with enums, pydantic models, data classes, or typed dicts
12
+ related to the domain. We may at some point also move ORM models related
13
+ to the domain into these sub folders. Each domain should have a `tests`
14
+ directory for tool and model tests.
File without changes
File without changes
@@ -1,5 +1,8 @@
1
+ from pydantic import BaseModel
1
2
  from strenum import StrEnum
2
3
 
4
+ from kfinance.domains.companies.company_models import CompanyIdAndName
5
+
3
6
 
4
7
  class BusinessRelationshipType(StrEnum):
5
8
  """The type of business relationship"""
@@ -24,3 +27,10 @@ class BusinessRelationshipType(StrEnum):
24
27
  transfer_agent_client = "transfer_agent_client"
25
28
  vendor = "vendor"
26
29
  client_services = "client_services"
30
+
31
+
32
+ class RelationshipResponse(BaseModel):
33
+ """A response from the relationship endpoint that includes both company_id and name."""
34
+
35
+ current: list[CompanyIdAndName]
36
+ previous: list[CompanyIdAndName]
@@ -0,0 +1,74 @@
1
+ from textwrap import dedent
2
+ from typing import Type
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
7
+ from kfinance.client.permission_models import Permission
8
+ from kfinance.domains.business_relationships.business_relationship_models import (
9
+ BusinessRelationshipType,
10
+ )
11
+ from kfinance.domains.companies.company_identifiers import (
12
+ fetch_company_ids_from_identifiers,
13
+ parse_identifiers,
14
+ )
15
+ from kfinance.integrations.tool_calling.tool_calling_models import (
16
+ KfinanceTool,
17
+ ToolArgsWithIdentifiers,
18
+ )
19
+
20
+
21
+ class GetBusinessRelationshipFromIdentifiersArgs(ToolArgsWithIdentifiers):
22
+ # no description because the description for enum fields comes from the enum docstring.
23
+ business_relationship: BusinessRelationshipType
24
+
25
+
26
+ class GetBusinessRelationshipFromIdentifiers(KfinanceTool):
27
+ name: str = "get_business_relationship_from_identifiers"
28
+ description: str = dedent("""
29
+ Get the current and previous company IDs that are relationship_type for a list of identifiers.
30
+
31
+ Example:
32
+ Query: "What are the previous borrowers of SPGI and JPM?"
33
+ Function: get_business_relationship_from_identifiers(identifiers=["SPGI", "JPM"], business_relationship=BusinessRelationshipType.borrower)
34
+ """).strip()
35
+ args_schema: Type[BaseModel] = GetBusinessRelationshipFromIdentifiersArgs
36
+ accepted_permissions: set[Permission] | None = {Permission.RelationshipPermission}
37
+
38
+ def _run(self, identifiers: list[str], business_relationship: BusinessRelationshipType) -> dict:
39
+ """Sample response:
40
+
41
+ {
42
+ "SPGI": {
43
+ "current": [{"company_id": "C_883103", "company_name": "CRISIL Limited"}],
44
+ "previous": [
45
+ {"company_id": "C_472898", "company_name": "Morgan Stanley"},
46
+ {"company_id": "C_8182358", "company_name": "Eloqua, Inc."},
47
+ ],
48
+ }
49
+ }
50
+ """
51
+
52
+ api_client = self.kfinance_client.kfinance_api_client
53
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
54
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
55
+ identifiers=parsed_identifiers, api_client=api_client
56
+ )
57
+
58
+ tasks = [
59
+ Task(
60
+ func=api_client.fetch_companies_from_business_relationship,
61
+ kwargs=dict(
62
+ company_id=company_id,
63
+ relationship_type=business_relationship,
64
+ ),
65
+ result_key=identifier,
66
+ )
67
+ for identifier, company_id in identifiers_to_company_ids.items()
68
+ ]
69
+
70
+ relationship_responses = process_tasks_in_thread_pool_executor(
71
+ api_client=api_client, tasks=tasks
72
+ )
73
+
74
+ return {str(k): v.model_dump(mode="json") for k, v in relationship_responses.items()}
@@ -0,0 +1,55 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.conftest import SPGI_COMPANY_ID
5
+ from kfinance.domains.business_relationships.business_relationship_models import (
6
+ BusinessRelationshipType,
7
+ )
8
+ from kfinance.domains.business_relationships.business_relationship_tools import (
9
+ GetBusinessRelationshipFromIdentifiers,
10
+ GetBusinessRelationshipFromIdentifiersArgs,
11
+ )
12
+
13
+
14
+ class TestGetBusinessRelationshipFromIdentifiers:
15
+ def test_get_business_relationship_from_identifiers(
16
+ self, requests_mock: Mocker, mock_client: Client
17
+ ):
18
+ """
19
+ GIVEN the GetBusinessRelationshipFromIdentifiers tool
20
+ WHEN we request SPGI suppliers
21
+ THEN we get back the SPGI suppliers
22
+ """
23
+ supplier_resp = {
24
+ "current": [{"company_id": 883103, "company_name": "CRISIL Limited"}],
25
+ "previous": [
26
+ {"company_id": 472898, "company_name": "Morgan Stanley"},
27
+ {"company_id": 8182358, "company_name": "Eloqua, Inc."},
28
+ ],
29
+ }
30
+ expected_result = {
31
+ "SPGI": {
32
+ "current": [{"company_id": "C_883103", "company_name": "CRISIL Limited"}],
33
+ "previous": [
34
+ {"company_id": "C_472898", "company_name": "Morgan Stanley"},
35
+ {"company_id": "C_8182358", "company_name": "Eloqua, Inc."},
36
+ ],
37
+ }
38
+ }
39
+
40
+ requests_mock.get(
41
+ url=f"https://kfinance.kensho.com/api/v1/relationship/{SPGI_COMPANY_ID}/supplier",
42
+ json=supplier_resp,
43
+ )
44
+
45
+ tool = GetBusinessRelationshipFromIdentifiers(kfinance_client=mock_client)
46
+ args = GetBusinessRelationshipFromIdentifiersArgs(
47
+ identifiers=["SPGI"], business_relationship=BusinessRelationshipType.supplier
48
+ )
49
+ resp = tool.run(args.model_dump(mode="json"))
50
+
51
+ assert list(resp.keys()) == ["SPGI"]
52
+ # Sort companies by ID to make the result deterministic
53
+ resp["SPGI"]["current"].sort(key=lambda x: x["company_id"])
54
+ resp["SPGI"]["previous"].sort(key=lambda x: x["company_id"])
55
+ assert resp == expected_result
File without changes
@@ -5,7 +5,7 @@ from typing import Any
5
5
  from pydantic import BaseModel, Field, model_validator
6
6
  from strenum import StrEnum
7
7
 
8
- from kfinance.decimal_with_unit import Money, Shares
8
+ from kfinance.client.models.decimal_with_unit import Money, Shares
9
9
 
10
10
 
11
11
  class Capitalization(StrEnum):
@@ -68,23 +68,30 @@ class Capitalizations(BaseModel):
68
68
  capitalization[key] = dict(unit=currency, value=capitalization[key])
69
69
  return data
70
70
 
71
- def jsonify_single_attribute(self, capitalization_to_extract: Capitalization) -> dict:
72
- """Return a json representation of a single attribute like "market_cap".
71
+ def model_dump_json_single_metric(
72
+ self, capitalization_metric: Capitalization, only_include_most_recent_value: bool = False
73
+ ) -> dict:
74
+ """Dump only a single metric (market cap, tev, or shares outstanding) to json
73
75
 
74
- Example response:
75
- {
76
- "market_cap": [
77
- {'2024-06-24': {'unit': 'USD', 'value': '139231113000.00'}},
78
- {'2024-06-25': {'unit': 'USD', 'value': '140423262000.00'}}
79
- ]
80
- }
76
+ If only_include_most_recent_value is set to True, only the most recent value gets included.
81
77
 
78
+ Sample response:
79
+ [
80
+ {
81
+ 'date': datetime.date(2024, 4, 10),
82
+ 'market_cap': {'value': Decimal('132766738270.00'), 'unit': 'USD'}
83
+ }
84
+ ]
82
85
  """
83
86
 
84
- capitalizations = []
85
- for capitalization in self.capitalizations:
86
- attribute_val = getattr(capitalization, capitalization_to_extract.value)
87
- capitalizations.append(
88
- {capitalization.date.isoformat(): attribute_val.model_dump(mode="json")}
89
- )
90
- return {capitalization_to_extract: capitalizations}
87
+ return self.model_dump(
88
+ mode="json",
89
+ include={ # type: ignore[arg-type]
90
+ "capitalizations": {
91
+ 0 if only_include_most_recent_value else "__all__": {
92
+ "date",
93
+ capitalization_metric.value,
94
+ }
95
+ }
96
+ },
97
+ )["capitalizations"]
@@ -0,0 +1,89 @@
1
+ from datetime import date
2
+ from textwrap import dedent
3
+ from typing import Type
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
8
+ from kfinance.client.permission_models import Permission
9
+ from kfinance.domains.capitalizations.capitalization_models import Capitalization
10
+ from kfinance.domains.companies.company_identifiers import (
11
+ fetch_company_ids_from_identifiers,
12
+ parse_identifiers,
13
+ )
14
+ from kfinance.integrations.tool_calling.tool_calling_models import (
15
+ KfinanceTool,
16
+ ToolArgsWithIdentifiers,
17
+ )
18
+
19
+
20
+ class GetCapitalizationFromIdentifiersArgs(ToolArgsWithIdentifiers):
21
+ # no description because the description for enum fields comes from the enum docstring.
22
+ capitalization: Capitalization
23
+ start_date: date | None = Field(
24
+ description="The start date for historical capitalization retrieval", default=None
25
+ )
26
+ end_date: date | None = Field(
27
+ description="The end date for historical capitalization retrieval", default=None
28
+ )
29
+
30
+
31
+ class GetCapitalizationFromIdentifiers(KfinanceTool):
32
+ name: str = "get_capitalization_from_identifiers"
33
+ description: str = dedent("""
34
+ Get the historical market cap, tev (Total Enterprise Value), or shares outstanding for a group of identifiers between inclusive start_date and inclusive end date.
35
+
36
+ - When possible, pass multiple identifiers in a single call rather than making multiple calls.
37
+ - When requesting the most recent values, leave start_date and end_date empty.
38
+
39
+ Example:
40
+ Query: "What are the market caps of AAPL and WMT?"
41
+ Function: get_capitalization_from_identifiers(capitalization=Capitalization.market_cap, identifiers=["AAPL", "WMT"])
42
+ """).strip()
43
+ args_schema: Type[BaseModel] = GetCapitalizationFromIdentifiersArgs
44
+ accepted_permissions: set[Permission] | None = {Permission.PricingPermission}
45
+
46
+ def _run(
47
+ self,
48
+ identifiers: list[str],
49
+ capitalization: Capitalization,
50
+ start_date: str | None = None,
51
+ end_date: str | None = None,
52
+ ) -> dict:
53
+ """Sample response:
54
+
55
+ {
56
+ 'SPGI': [
57
+ {'date': '2024-04-10', 'market_cap': {'unit': 'USD', 'value': '132766738270.00'}},
58
+ {'date': '2024-04-11', 'market_cap': {'unit': 'USD', 'value': '132416066761.00'}}
59
+ ]
60
+ }
61
+ """
62
+ api_client = self.kfinance_client.kfinance_api_client
63
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
64
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
65
+ identifiers=parsed_identifiers, api_client=api_client
66
+ )
67
+
68
+ tasks = [
69
+ Task(
70
+ func=api_client.fetch_market_caps_tevs_and_shares_outstanding,
71
+ kwargs=dict(company_id=company_id, start_date=start_date, end_date=end_date),
72
+ result_key=identifier,
73
+ )
74
+ for identifier, company_id in identifiers_to_company_ids.items()
75
+ ]
76
+
77
+ capitalization_responses = process_tasks_in_thread_pool_executor(
78
+ api_client=api_client, tasks=tasks
79
+ )
80
+
81
+ return {
82
+ str(identifier): capitalization_response.model_dump_json_single_metric(
83
+ capitalization_metric=capitalization,
84
+ only_include_most_recent_value=True
85
+ if (len(identifiers) > 1 and start_date == end_date is None)
86
+ else False,
87
+ )
88
+ for identifier, capitalization_response in capitalization_responses.items()
89
+ }
File without changes
@@ -1,8 +1,8 @@
1
1
  from datetime import date
2
2
  from decimal import Decimal
3
3
 
4
- from kfinance.decimal_with_unit import Money, Shares
5
- from kfinance.models.capitalization_models import (
4
+ from kfinance.client.models.decimal_with_unit import Money, Shares
5
+ from kfinance.domains.capitalizations.capitalization_models import (
6
6
  Capitalization,
7
7
  Capitalizations,
8
8
  DailyCapitalization,
@@ -69,15 +69,13 @@ class TestCapitalizations:
69
69
  def test_single_attribute_serialization(self):
70
70
  """
71
71
  GIVEN a Capitalizations object
72
- WHEN we only want to jsonify a single attribute like market_cap
72
+ WHEN we only want to dump a single attribute like market_cap
73
73
  THEN that attribute gets returned in an LLM-readable, jsonifyable dict format
74
74
  """
75
- expected_serialization = {
76
- "market_cap": [
77
- {"2024-06-24": {"unit": "USD", "value": "139231113000.00"}},
78
- {"2024-06-25": {"unit": "USD", "value": "140423262000.00"}},
79
- ]
80
- }
75
+ expected_serialization = [
76
+ {"date": "2024-06-24", "market_cap": {"unit": "USD", "value": "139231113000.00"}},
77
+ {"date": "2024-06-25", "market_cap": {"unit": "USD", "value": "140423262000.00"}},
78
+ ]
81
79
  capitalizations = Capitalizations.model_validate(self.api_resp)
82
- serialized = capitalizations.jsonify_single_attribute(Capitalization.market_cap)
80
+ serialized = capitalizations.model_dump_json_single_metric(Capitalization.market_cap)
83
81
  assert serialized == expected_serialization