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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kensho-kfinance
|
|
3
|
-
Version: 2.
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
kensho_kfinance-2.
|
|
3
|
-
kfinance/CHANGELOG.md,sha256=
|
|
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=
|
|
8
|
-
kfinance/kfinance.py,sha256=
|
|
9
|
-
kfinance/meta_classes.py,sha256=
|
|
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=
|
|
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=
|
|
19
|
-
kfinance/tests/test_fetch.py,sha256=
|
|
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=
|
|
22
|
-
kfinance/tests/test_tools.py,sha256=
|
|
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=
|
|
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.
|
|
41
|
-
kensho_kfinance-2.
|
|
42
|
-
kensho_kfinance-2.
|
|
43
|
-
kensho_kfinance-2.
|
|
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
|
-
) ->
|
|
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:
|
|
524
|
+
:rtype: RelationshipResponse | RelationshipResponseNoName
|
|
525
525
|
"""
|
|
526
526
|
url = f"{self.url_base}relationship/{company_id}/{relationship_type}"
|
|
527
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|