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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kensho-kfinance
3
- Version: 2.2.4
3
+ Version: 2.3.0
4
4
  Summary: Python CLI for kFinance
5
5
  Author-email: Luke Brown <luke.brown@kensho.com>, Michelle Keoy <michelle.keoy@kensho.com>, Keith Page <keith.page@kensho.com>, Matthew Rosen <matthew.rosen@kensho.com>, Nick Roshdieh <nick.roshdieh@kensho.com>
6
6
  Project-URL: source, https://github.com/kensho-technologies/kfinance
@@ -14,7 +14,7 @@ License-File: LICENSE
14
14
  License-File: AUTHORS.md
15
15
  Requires-Dist: cachetools<6,>=5.5
16
16
  Requires-Dist: langchain-core>=0.3.15
17
- Requires-Dist: langchain-google-genai<3,>=2.1.0
17
+ Requires-Dist: langchain-google-genai<3,>=2.1.5
18
18
  Requires-Dist: numpy>=1.22.4
19
19
  Requires-Dist: pandas>=2.0.0
20
20
  Requires-Dist: pillow>=10
@@ -29,7 +29,8 @@ Requires-Dist: urllib3>=1.21.1
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: coverage<8,>=7.6.10; extra == "dev"
31
31
  Requires-Dist: ipykernel<7,>=6.29; extra == "dev"
32
- Requires-Dist: mypy<2,>=1.15.0; extra == "dev"
32
+ Requires-Dist: langchain-anthropic<1,>=0.3.10; extra == "dev"
33
+ Requires-Dist: mypy<2,>=1.16.0; extra == "dev"
33
34
  Requires-Dist: nbconvert<8,>=7.16; extra == "dev"
34
35
  Requires-Dist: nbformat<6,>5.10; extra == "dev"
35
36
  Requires-Dist: nbqa<2,>1.9; extra == "dev"
@@ -61,7 +62,16 @@ To receive access, please email [S&P Global Market Intelligence](market.intellig
61
62
 
62
63
  Once access is obtained, get started using the [Authentication Guide](https://docs.kensho.com/llmreadyapi/kf-authentication) and [Usage Guide](https://docs.kensho.com/llmreadyapi/usage).
63
64
 
64
- We also provide an [interactive notebook](usage_examples.ipynb) that demonstrates some usage examples.
65
+ To get started, we provide some notebooks:
66
+
67
+ - The [LLM-ready API Basic Usage](example_notebooks%2Fbasic_usage.ipynb) notebook demonstrates how
68
+ fetch data with the kFinance client.
69
+ - The [tool_calling notebooks](example_notebooks%2Ftool_calling) show how the kFinance library can
70
+ be used for tool calling. We provide notebooks for OpenAI (GPT), Anthropic (Claude), and Google
71
+ (Gemini). Each of these integrations comes in a langchain version, which uses langchain as a
72
+ wrapper to simplify the integration, and as a lower level non-langchain version.
73
+
74
+ We also provide an [interactive notebook](example_notebooks/basic_usage.ipynb) that demonstrates some usage examples.
65
75
 
66
76
  # Versioning
67
77
  The kFinance uses semantic versioning (major, minor, patch).
@@ -1,30 +1,32 @@
1
- kensho_kfinance-2.2.4.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
2
- kensho_kfinance-2.2.4.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
3
- kfinance/CHANGELOG.md,sha256=rBjrg8H9eODymBNuLyOWIRrA5FAVR92WgxswFMkGI9E,1241
1
+ kensho_kfinance-2.3.0.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
2
+ kensho_kfinance-2.3.0.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
3
+ kfinance/CHANGELOG.md,sha256=vIFnA_ArXUrV64T76m3KVMX0wGeVMsEi_u4BmQ3os7Y,1418
4
4
  kfinance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  kfinance/batch_request_handling.py,sha256=p6p_G4_BL06GgeKlh7P1k9CUqOMahWCLEw1NoBwbLvU,5698
6
6
  kfinance/constants.py,sha256=UuFzqL253-2tRQfma785K9tfaZGv-o821tO2tVLwc5Q,48813
7
- kfinance/fetch.py,sha256=fguxYy5rjLQKNkmP7gsvZUJtmt9uDNxg6LN9FRJTAVQ,22936
8
- kfinance/kfinance.py,sha256=_U69k0Dcwkw1B_lzaUZy8N2-c-v93ZKoUAVVZb6wBUM,52758
9
- kfinance/meta_classes.py,sha256=1qYkj2L7jcBfdkye6TG2nuRhwzhAbmtxay6rSVY3DsA,19883
7
+ kfinance/fetch.py,sha256=yaCih8PAkOhVHb3tvmBW0x2w4QmXJiyUATu6Yx-xzP4,23851
8
+ kfinance/kfinance.py,sha256=9lcarUW4fLHJA_U7_1ihCMbNJWs0AztvKf_XEd0qupk,59889
9
+ kfinance/meta_classes.py,sha256=3V0nSXDDoake5o7kXnrqXuqNIiwI75KR4IYxFqSPhTE,20736
10
10
  kfinance/prompt.py,sha256=PtVB8c_FcSlVdyGgByAnIFGzuUuBaEjciCqnBJl1hSQ,25133
11
11
  kfinance/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ kfinance/pydantic_models.py,sha256=avpbPqwrAyLqsCbrmFpK_B8_fj1nPlBHrnPxRcBaSkE,774
12
13
  kfinance/server_thread.py,sha256=jUnt1YGoYDkqqz1MbCwd44zJs1T_Z2BCgvj75bdtLgA,2574
13
- kfinance/version.py,sha256=Ath1yNFbHtPzluPba0J7EfQTfLH_byRzLfGjqAUl6cY,511
14
+ kfinance/version.py,sha256=U--yqU7RFo8hQQm8oopUGYLkafj4phNIVfkf5HFEal8,511
14
15
  kfinance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
16
  kfinance/tests/conftest.py,sha256=voB-w8P_6L3Nel3rdgylXKe5WWaS1q7nCFt1O04uqoY,948
16
17
  kfinance/tests/test_batch_requests.py,sha256=uXJF2IcRdyBm5SthwIUHMKtkGZ21MY84pg_k1JeSNOY,11430
17
18
  kfinance/tests/test_client.py,sha256=O7icZCSDhlQ9WGhzoXlpiSvbuA-mQNJHBYVsilyP_dE,2209
18
- kfinance/tests/test_example_notebook.py,sha256=XhDAJp3H5Y6usLAt3k4Ug4W3H6gLyh68nzmoWgOOLn4,6441
19
- kfinance/tests/test_fetch.py,sha256=O9qOsffQYJPSAR1X_P6CNoA0Buyq2kx4vfS0FOc2Goc,13564
19
+ kfinance/tests/test_example_notebook.py,sha256=XHwDKw2avyMonTmi3snCcFWNfZhEJOkpBGOZNrMLrhk,6470
20
+ kfinance/tests/test_fetch.py,sha256=nfnz_ZxE-W3KMzpDaRClX55fQJrRjwLTha-rHTallmE,16713
20
21
  kfinance/tests/test_group_objects.py,sha256=SoMEZmkG4RYdgWOAwxLHHtzIQho92KM01YbQXPUg578,1689
21
- kfinance/tests/test_objects.py,sha256=CSk3iN-uDt-E6gaOX4jExuYPT8Up-YNC3hafpenadcA,23614
22
- kfinance/tests/test_tools.py,sha256=ne4XhebhoxDHFl4kRLl10j4K-SXLrxS6jv6Ypt0Gv_E,16846
22
+ kfinance/tests/test_objects.py,sha256=0nDmCFrVcfI8VBo1Ph3YqXNo3uPLsSUiQkjEEHsax1M,30416
23
+ kfinance/tests/test_tools.py,sha256=EWWWyPlGGES8Cn43_VaDAT7Vdp1nlIETE5dtOrP163o,24447
23
24
  kfinance/tool_calling/README.md,sha256=omJq7Us6r4U45QB7hRpLjRJ5BMalCkZkh4uXBjTbJXc,2022
24
- kfinance/tool_calling/__init__.py,sha256=Fb30Snu09B63NscSUJGGnFAdlXSDrTAOJlzjYvMyZXk,1873
25
+ kfinance/tool_calling/__init__.py,sha256=V5BVcJkLT1zC0QKZIQjLb-cZiQYV4T9Egj64uH807WE,2215
25
26
  kfinance/tool_calling/get_business_relationship_from_identifier.py,sha256=CipXvyqEjPm6BXYP0CA9Kp1BIyiIEm7abp85x1zXRV4,1472
26
27
  kfinance/tool_calling/get_capitalization_from_identifier.py,sha256=TdWdJDeI-jSL-1YfhnnwIA9D1SXobidvoHrjK42QmqQ,1521
27
28
  kfinance/tool_calling/get_cusip_from_ticker.py,sha256=houhGCYXoSzaaTtCvOBf3pPsYiSbcV1Ej5nAyGuMWcU,644
29
+ kfinance/tool_calling/get_earnings.py,sha256=7xavYvqwq4fO0vpks25eDM87bCYwZxEQSwgT0I8jAok,1165
28
30
  kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py,sha256=Zp7gHP-z2ePu484wWx4xw89hl0z7NLcG3qliJT-6rUo,782
29
31
  kfinance/tool_calling/get_financial_line_item_from_identifier.py,sha256=bMK-GUMz_wmJ-7iFS72sM8AvvUvR83JYRWzYtr9Fofw,2153
30
32
  kfinance/tool_calling/get_financial_statement_from_identifier.py,sha256=9ouzC43skA78uI3dh3bvtM86db5RGxuWQAAJ24y-kBM,1906
@@ -32,12 +34,16 @@ kfinance/tool_calling/get_history_metadata_from_identifier.py,sha256=kKIInfRC1Pl
32
34
  kfinance/tool_calling/get_info_from_identifier.py,sha256=PRhSYpYs_iUcIVFGYCMN_7QJyhewua7c7pqngPIp-qg,766
33
35
  kfinance/tool_calling/get_isin_from_ticker.py,sha256=2fJBcA-rNGbVOQmQ7qJEYxqejQwJ6nyWOBSFlzxG7dY,638
34
36
  kfinance/tool_calling/get_latest.py,sha256=btGeVBmvX5QJutzrKfE6spatGGejDuHxtq53NoAaGNk,786
37
+ kfinance/tool_calling/get_latest_earnings.py,sha256=pQdExGdztGY3pHcak0bd6ULNf_ROPc91bJ7zAVSKQkk,1117
35
38
  kfinance/tool_calling/get_n_quarters_ago.py,sha256=A0ilwPKUqU0YYQSz3gNsVF0Jy4YttXrSaDhYj7y8GHA,713
39
+ kfinance/tool_calling/get_next_earnings.py,sha256=knzQw-m-hscCvTuDUXG9v_dObKJBGn5BbDZWGKHKQcw,1097
36
40
  kfinance/tool_calling/get_prices_from_identifier.py,sha256=ViJkwLDvStB7grc8RuoKSDXQM399Wru4-OY3E8k1l_U,1882
37
41
  kfinance/tool_calling/get_segments_from_identifier.py,sha256=WIqJ1wWE6Z87VBREGu42nRc6_eJqUbGKcE9elzqBQJE,1867
42
+ kfinance/tool_calling/get_transcript.py,sha256=eB-IsRwD-mllsMOYRZbH35caQ1Y3teKft0tmI9nVL-A,756
43
+ kfinance/tool_calling/prompts.py,sha256=Yw1DJIMh90cjL-8q6_RMRiSjCtFDXvJAy7QiV5_uAU8,911
38
44
  kfinance/tool_calling/resolve_identifier.py,sha256=npslr6bBCu0qEDV1-8d24F5OC3nQ1KBMphuMbHVC1AU,626
39
45
  kfinance/tool_calling/shared_models.py,sha256=K-NPQyE_7Ew6Cs0zxG1xO2O47gp5uDHdHtWD7wUDZX4,2132
40
- kensho_kfinance-2.2.4.dist-info/METADATA,sha256=sx_SF7gbkLwRfyTRUw2WHaqkoSyqQfUoBUS5majgF7w,3436
41
- kensho_kfinance-2.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
42
- kensho_kfinance-2.2.4.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
43
- kensho_kfinance-2.2.4.dist-info/RECORD,,
46
+ kensho_kfinance-2.3.0.dist-info/METADATA,sha256=3rNOXczOABUGLavlhJUmseCJ0pOL6x00uckfLkJcSkI,4066
47
+ kensho_kfinance-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
+ kensho_kfinance-2.3.0.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
49
+ kensho_kfinance-2.3.0.dist-info/RECORD,,
kfinance/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.3.1
4
+ - Add llm tools for retrieving earnings and transcripts
5
+
6
+ ## v2.3.0
7
+ - Add earnings and transcript objects
8
+
9
+ ## v2.2.5
10
+ - Add parsing for relationship response with name
11
+
3
12
  ## v2.2.4
4
13
  - Add segments as llm tools
5
14
 
kfinance/fetch.py CHANGED
@@ -6,6 +6,7 @@ from typing import Callable, Generator, Optional
6
6
  from uuid import uuid4
7
7
 
8
8
  import jwt
9
+ from pydantic import ValidationError
9
10
  import requests
10
11
 
11
12
  from .constants import (
@@ -17,6 +18,7 @@ from .constants import (
17
18
  Permission,
18
19
  SegmentType,
19
20
  )
21
+ from .pydantic_models import RelationshipResponse, RelationshipResponseNoName
20
22
 
21
23
 
22
24
  # version.py gets autogenerated by setuptools-scm and is not available
@@ -505,26 +507,30 @@ class KFinanceApiClient:
505
507
 
506
508
  def fetch_companies_from_business_relationship(
507
509
  self, company_id: int, relationship_type: BusinessRelationshipType
508
- ) -> dict[str, list[int]]:
509
- """Fetches a dictionary of current and previous company IDs associated with a given company ID based on the specified relationship type.
510
-
511
- The returned dictionary has the following structure:
512
- {
513
- "current": List[int],
514
- "previous": List[int]
515
- }
510
+ ) -> RelationshipResponse | RelationshipResponseNoName:
511
+ """Fetches a dictionary of current and previous company IDs and names associated with a given company ID based on the specified relationship type.
516
512
 
517
513
  Example: fetch_companies_from_business_relationship(company_id=1234, relationship_type="distributor") returns a dictionary of company 1234's current and previous distributors.
518
514
 
515
+ As of 2024-05-28, we are changing the response on the backend from
516
+ RelationshipResponseNoName to RelationshipResponse. This function can handle both response
517
+ types.
518
+
519
519
  :param company_id: The ID of the company for which associated companies are being fetched.
520
520
  :type company_id: int
521
521
  :param relationship_type: The type of relationship to filter by. Valid relationship types are defined in the BusinessRelationshipType class.
522
522
  :type relationship_type: BusinessRelationshipType
523
523
  :return: A dictionary containing lists of current and previous company IDs that have the specified relationship with the given company_id.
524
- :rtype: dict[str, list[int]]
524
+ :rtype: RelationshipResponse | RelationshipResponseNoName
525
525
  """
526
526
  url = f"{self.url_base}relationship/{company_id}/{relationship_type}"
527
- return self.fetch(url)
527
+ result = self.fetch(url)
528
+ # Try to parse as the newer RelationshipResponse and fall back to
529
+ # RelationshipResponseNoName if that fails.
530
+ try:
531
+ return RelationshipResponse.model_validate(result)
532
+ except ValidationError:
533
+ return RelationshipResponseNoName.model_validate(result)
528
534
 
529
535
  def fetch_ticker_from_industry_code(
530
536
  self,
@@ -561,3 +567,13 @@ class KFinanceApiClient:
561
567
  """
562
568
  url = f"{self.url_base}company_groups/industry/{industry_classification}/{industry_code}"
563
569
  return self.fetch(url)
570
+
571
+ def fetch_earnings(self, company_id: int) -> dict:
572
+ """Get the earnings for a company."""
573
+ url = f"{self.url_base}earnings/{company_id}"
574
+ return self.fetch(url)
575
+
576
+ def fetch_transcript(self, key_dev_id: int) -> dict:
577
+ """Get the transcript for an earnings item."""
578
+ url = f"{self.url_base}transcript/{key_dev_id}"
579
+ return self.fetch(url)
kfinance/kfinance.py CHANGED
@@ -1,13 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Sequence
3
4
  from concurrent.futures import ThreadPoolExecutor
5
+ from copy import deepcopy
4
6
  from datetime import date, datetime, timezone
5
7
  from functools import cached_property
6
8
  from io import BytesIO
7
9
  import logging
8
10
  import re
9
11
  from sys import stdout
10
- from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Optional
12
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Optional, overload
11
13
  from urllib.parse import urljoin
12
14
  import webbrowser
13
15
 
@@ -39,6 +41,7 @@ from .meta_classes import (
39
41
  DelegatedCompanyFunctionsMetaClass,
40
42
  )
41
43
  from .prompt import PROMPT
44
+ from .pydantic_models import TranscriptComponent
42
45
  from .server_thread import ServerThread
43
46
 
44
47
 
@@ -48,6 +51,12 @@ if TYPE_CHECKING:
48
51
  logger = logging.getLogger(__name__)
49
52
 
50
53
 
54
+ class NoEarningsDataError(Exception):
55
+ """Exception raised when no earnings data is found for a company."""
56
+
57
+ pass
58
+
59
+
51
60
  class TradingItem:
52
61
  """Trading Class
53
62
 
@@ -199,6 +208,96 @@ class TradingItem:
199
208
  return image
200
209
 
201
210
 
211
+ class Transcript(Sequence[TranscriptComponent]):
212
+ """Transcript class that represents earnings item transcript components"""
213
+
214
+ def __init__(self, transcript_components: list[dict[str, str]]):
215
+ """Initialize the Transcript object
216
+
217
+ :param transcript_components: List of transcript component dictionaries
218
+ :type transcript_components: list[dict[str, str]]
219
+ """
220
+ self._components = [TranscriptComponent(**component) for component in transcript_components]
221
+ self._raw_transcript: str | None = None
222
+
223
+ @overload
224
+ def __getitem__(self, index: int) -> TranscriptComponent: ...
225
+
226
+ @overload
227
+ def __getitem__(self, index: slice) -> list[TranscriptComponent]: ...
228
+
229
+ def __getitem__(self, index: int | slice) -> TranscriptComponent | list[TranscriptComponent]:
230
+ return self._components[index]
231
+
232
+ def __len__(self) -> int:
233
+ return len(self._components)
234
+
235
+ @property
236
+ def raw(self) -> str:
237
+ """Get the raw transcript as a single string
238
+
239
+ :return: Raw transcript text with speaker names and double newlines between components
240
+ :rtype: str
241
+ """
242
+ if self._raw_transcript is not None:
243
+ return self._raw_transcript
244
+
245
+ raw_components = []
246
+ for component in self._components:
247
+ speaker = component.person_name
248
+ text = component.text
249
+ raw_components.append(f"{speaker}: {text}")
250
+
251
+ self._raw_transcript = "\n\n".join(raw_components)
252
+ return self._raw_transcript
253
+
254
+
255
+ class Earnings:
256
+ """Earnings class that represents an earnings item"""
257
+
258
+ def __init__(
259
+ self,
260
+ kfinance_api_client: "KFinanceApiClient",
261
+ name: str,
262
+ datetime: datetime,
263
+ key_dev_id: int,
264
+ ):
265
+ """Initialize the Earnings object
266
+
267
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
268
+ :type kfinance_api_client: KFinanceApiClient
269
+ :param name: The earnings name
270
+ :type name: str
271
+ :param datetime: The earnings datetime
272
+ :type datetime: datetime
273
+ :param key_dev_id: The key dev ID for the earnings
274
+ :type key_dev_id: int
275
+ """
276
+ self.kfinance_api_client = kfinance_api_client
277
+ self.name = name
278
+ self.datetime = datetime
279
+ self.key_dev_id = key_dev_id
280
+ self._transcript: Transcript | None = None
281
+
282
+ def __str__(self) -> str:
283
+ """String representation for the earnings object"""
284
+ return f"{type(self).__module__}.{type(self).__qualname__} of {self.key_dev_id}"
285
+
286
+ @property
287
+ def transcript(self) -> Transcript:
288
+ """Get the transcript for this earnings
289
+
290
+ :return: The transcript object containing all components
291
+ :rtype: Transcript
292
+ """
293
+ if self._transcript is not None:
294
+ return self._transcript
295
+
296
+ transcript_data = self.kfinance_api_client.fetch_transcript(self.key_dev_id)
297
+ self._transcript = Transcript(transcript_data["transcript"])
298
+ return self._transcript
299
+
300
+
202
301
  class Company(CompanyFunctionsMetaClass):
203
302
  """Company class
204
303
 
@@ -219,6 +318,7 @@ class Company(CompanyFunctionsMetaClass):
219
318
  super().__init__()
220
319
  self.kfinance_api_client = kfinance_api_client
221
320
  self.company_id = company_id
321
+ self._all_earnings: list[Earnings] | None = None
222
322
 
223
323
  def __str__(self) -> str:
224
324
  """String representation for the company object"""
@@ -248,16 +348,6 @@ class Company(CompanyFunctionsMetaClass):
248
348
  security_ids = self.kfinance_api_client.fetch_securities(self.company_id)["securities"]
249
349
  return Securities(kfinance_api_client=self.kfinance_api_client, security_ids=security_ids)
250
350
 
251
- @cached_property
252
- def latest_earnings_call(self) -> None:
253
- """Set and return the latest earnings call item for the object
254
-
255
- :raises NotImplementedError: This function is not yet implemented
256
- """
257
- raise NotImplementedError(
258
- "The latest earnings call property of company class not implemented yet"
259
- )
260
-
261
351
  @cached_property
262
352
  def info(self) -> dict:
263
353
  """Get the company info
@@ -398,6 +488,121 @@ class Company(CompanyFunctionsMetaClass):
398
488
  ]
399
489
  ]
400
490
 
491
+ @property
492
+ def all_earnings(self) -> list[Earnings]:
493
+ """Retrieve and cache all earnings items for this company"""
494
+ if self._all_earnings is not None:
495
+ return self._all_earnings
496
+
497
+ earnings_data = self.kfinance_api_client.fetch_earnings(self.company_id)
498
+ self._all_earnings = []
499
+
500
+ for earnings in earnings_data["earnings"]:
501
+ if "keydevid" in earnings:
502
+ earnings["key_dev_id"] = deepcopy(earnings["keydevid"])
503
+ del earnings["keydevid"]
504
+ earnings_datetime = datetime.fromisoformat(
505
+ earnings["datetime"].replace("Z", "+00:00")
506
+ ).replace(tzinfo=timezone.utc)
507
+
508
+ self._all_earnings.append(
509
+ Earnings(
510
+ kfinance_api_client=self.kfinance_api_client,
511
+ name=earnings["name"],
512
+ datetime=earnings_datetime,
513
+ key_dev_id=earnings["key_dev_id"],
514
+ )
515
+ )
516
+
517
+ return self._all_earnings
518
+
519
+ def earnings(
520
+ self, start_date: date | None = None, end_date: date | None = None
521
+ ) -> list[Earnings]:
522
+ """Get earnings for the company within date range sorted in descending order by date
523
+
524
+ :param start_date: Start date filter, defaults to None
525
+ :type start_date: date, optional
526
+ :param end_date: End date filter, defaults to None
527
+ :type end_date: date, optional
528
+ :return: List of earnings objects
529
+ :rtype: list[Earnings]
530
+ """
531
+ if not self.all_earnings:
532
+ return []
533
+
534
+ if start_date is not None:
535
+ start_date_utc = datetime.combine(start_date, datetime.min.time()).replace(
536
+ tzinfo=timezone.utc
537
+ )
538
+
539
+ else:
540
+ start_date_utc = None
541
+
542
+ if end_date is not None:
543
+ end_date_utc = datetime.combine(end_date, datetime.max.time()).replace(
544
+ tzinfo=timezone.utc
545
+ )
546
+
547
+ else:
548
+ end_date_utc = None
549
+
550
+ filtered_earnings = []
551
+
552
+ for earnings in self.all_earnings:
553
+ # Apply date filtering if provided
554
+ if start_date_utc is not None and earnings.datetime < start_date_utc:
555
+ continue
556
+
557
+ if end_date_utc is not None and earnings.datetime > end_date_utc:
558
+ continue
559
+
560
+ filtered_earnings.append(earnings)
561
+
562
+ return filtered_earnings
563
+
564
+ @property
565
+ def latest_earnings(self) -> Earnings | None:
566
+ """Get the most recent past earnings
567
+
568
+ :return: The most recent earnings or None if no data available
569
+ :rtype: Earnings | None
570
+ """
571
+ if not self.all_earnings:
572
+ return None
573
+
574
+ now = datetime.now(timezone.utc)
575
+ past_earnings = [
576
+ earnings_item for earnings_item in self.all_earnings if earnings_item.datetime <= now
577
+ ]
578
+
579
+ if not past_earnings:
580
+ return None
581
+
582
+ # Sort by datetime descending and get the most recent
583
+ return max(past_earnings, key=lambda x: x.datetime)
584
+
585
+ @property
586
+ def next_earnings(self) -> Earnings | None:
587
+ """Get the next upcoming earnings
588
+
589
+ :return: The next earnings or None if no data available
590
+ :rtype: Earnings | None
591
+ """
592
+ if not self.all_earnings:
593
+ return None
594
+
595
+ now = datetime.now(timezone.utc)
596
+ future_earnings = [
597
+ earnings_item for earnings_item in self.all_earnings if earnings_item.datetime > now
598
+ ]
599
+
600
+ if not future_earnings:
601
+ return None
602
+
603
+ # Sort by datetime ascending and get the earliest
604
+ return min(future_earnings, key=lambda x: x.datetime)
605
+
401
606
 
402
607
  class Security:
403
608
  """Security class
@@ -1345,6 +1550,17 @@ class Client:
1345
1550
  kfinance_api_client=self.kfinance_api_client, trading_item_id=trading_item_id
1346
1551
  )
1347
1552
 
1553
+ def transcript(self, key_dev_id: int) -> Transcript:
1554
+ """Generate Transcript object from key_dev_id
1555
+
1556
+ :param key_dev_id: The key dev ID for the earnings
1557
+ :type key_dev_id: int
1558
+ :return: The transcript specified by the key dev id
1559
+ :rtype: Transcript
1560
+ """
1561
+ transcript_data = self.kfinance_api_client.fetch_transcript(key_dev_id)
1562
+ return Transcript(transcript_data["transcript"])
1563
+
1348
1564
  @staticmethod
1349
1565
  def get_latest(use_local_timezone: bool = True) -> LatestPeriods:
1350
1566
  """Get the latest annual reporting year, latest quarterly reporting quarter and year, and current date.
kfinance/meta_classes.py CHANGED
@@ -9,12 +9,12 @@ import pandas as pd
9
9
 
10
10
  from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType, SegmentType
11
11
  from .fetch import KFinanceApiClient
12
+ from .pydantic_models import RelationshipResponse
12
13
 
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from .kfinance import BusinessRelationships
16
17
 
17
-
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
20
 
@@ -223,14 +223,32 @@ class CompanyFunctionsMetaClass:
223
223
  """
224
224
  from .kfinance import BusinessRelationships, Companies
225
225
 
226
- companies = self.kfinance_api_client.fetch_companies_from_business_relationship(
227
- self.company_id,
228
- relationship_type,
229
- )
230
- return BusinessRelationships(
231
- Companies(self.kfinance_api_client, companies["current"]),
232
- Companies(self.kfinance_api_client, companies["previous"]),
226
+ relationship_resp = self.kfinance_api_client.fetch_companies_from_business_relationship(
227
+ company_id=self.company_id,
228
+ relationship_type=relationship_type,
233
229
  )
230
+ if isinstance(relationship_resp, RelationshipResponse):
231
+ return BusinessRelationships(
232
+ current=Companies(
233
+ kfinance_api_client=self.kfinance_api_client,
234
+ company_ids=[c.company_id for c in relationship_resp.current],
235
+ ),
236
+ previous=Companies(
237
+ kfinance_api_client=self.kfinance_api_client,
238
+ company_ids=[c.company_id for c in relationship_resp.previous],
239
+ ),
240
+ )
241
+ else:
242
+ return BusinessRelationships(
243
+ current=Companies(
244
+ kfinance_api_client=self.kfinance_api_client,
245
+ company_ids=relationship_resp.current,
246
+ ),
247
+ previous=Companies(
248
+ kfinance_api_client=self.kfinance_api_client,
249
+ company_ids=relationship_resp.previous,
250
+ ),
251
+ )
234
252
 
235
253
  def market_cap(
236
254
  self,
@@ -0,0 +1,33 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class TranscriptComponent(BaseModel):
5
+ """A transcript component with person name, text, and component type."""
6
+
7
+ person_name: str
8
+ text: str
9
+ component_type: str
10
+
11
+
12
+ class RelationshipResponseNoName(BaseModel):
13
+ """A response from the relationship endpoint before adding the company name.
14
+
15
+ Each element in `current` and `previous` is a company_id.
16
+ """
17
+
18
+ current: list[int]
19
+ previous: list[int]
20
+
21
+
22
+ class CompanyIdAndName(BaseModel):
23
+ """A company_id and name"""
24
+
25
+ company_id: int
26
+ company_name: str
27
+
28
+
29
+ class RelationshipResponse(BaseModel):
30
+ """A response from the relationship endpoint that includes both company_id and name."""
31
+
32
+ current: list[CompanyIdAndName]
33
+ previous: list[CompanyIdAndName]
@@ -44,7 +44,7 @@ def jupyter_kernel_name() -> str:
44
44
 
45
45
  def test_run_notebook(jupyter_kernel_name: str):
46
46
  """
47
- GIVEN the usage_examples.ipynb notebook
47
+ GIVEN the basic_usage.ipynb notebook
48
48
  WHEN the notebook gets run with a mock client and mock responses
49
49
  THEN all cells of the notebook complete without errors.
50
50
  """
@@ -165,7 +165,9 @@ def test_run_notebook(jupyter_kernel_name: str):
165
165
  """)
166
166
 
167
167
  # Load the notebook
168
- notebook_path = Path(Path(__file__).parent.parent.parent, "usage_examples.ipynb")
168
+ notebook_path = Path(
169
+ Path(__file__).parent.parent.parent, "example_notebooks", "basic_usage.ipynb"
170
+ )
169
171
  with notebook_path.open() as f:
170
172
  nb = nbformat.read(f, as_version=4)
171
173