kensho-kfinance 2.6.5__py3-none-any.whl → 2.8.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.6.5.dist-info → kensho_kfinance-2.8.0.dist-info}/METADATA +1 -1
- kensho_kfinance-2.8.0.dist-info/RECORD +70 -0
- kfinance/CHANGELOG.md +6 -0
- kfinance/decimal_with_unit.py +78 -0
- kfinance/fetch.py +36 -16
- kfinance/kfinance.py +45 -44
- kfinance/meta_classes.py +28 -27
- kfinance/models/business_relationship_models.py +26 -0
- kfinance/models/capitalization_models.py +90 -0
- kfinance/models/competitor_models.py +13 -0
- kfinance/models/currency_models.py +345 -0
- kfinance/models/date_and_period_models.py +48 -0
- kfinance/models/id_models.py +7 -0
- kfinance/models/industry_models.py +12 -0
- kfinance/{constants.py → models/line_item_models.py} +1 -165
- kfinance/models/permission_models.py +15 -0
- kfinance/models/price_models.py +70 -0
- kfinance/models/segment_models.py +8 -0
- kfinance/models/statement_models.py +9 -0
- kfinance/tests/test_batch_requests.py +61 -31
- kfinance/tests/test_client.py +1 -1
- kfinance/tests/test_decimal_with_unit.py +60 -0
- kfinance/tests/test_example_notebook.py +1 -0
- kfinance/tests/test_fetch.py +23 -12
- kfinance/tests/test_models/__init__.py +0 -0
- kfinance/tests/test_models/test_capitalization_models.py +83 -0
- kfinance/tests/test_models/test_price_models.py +58 -0
- kfinance/tests/test_objects.py +29 -23
- kfinance/tests/test_tools.py +63 -11
- kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +1 -1
- kfinance/tool_calling/get_business_relationship_from_identifier.py +2 -1
- kfinance/tool_calling/get_capitalization_from_identifier.py +4 -5
- kfinance/tool_calling/get_competitors_from_identifier.py +2 -1
- kfinance/tool_calling/get_cusip_from_ticker.py +1 -1
- kfinance/tool_calling/get_earnings.py +1 -1
- kfinance/tool_calling/get_financial_line_item_from_identifier.py +3 -1
- kfinance/tool_calling/get_financial_statement_from_identifier.py +3 -1
- kfinance/tool_calling/get_history_metadata_from_identifier.py +2 -1
- kfinance/tool_calling/get_info_from_identifier.py +1 -1
- kfinance/tool_calling/get_isin_from_ticker.py +1 -1
- kfinance/tool_calling/get_latest.py +2 -1
- kfinance/tool_calling/get_latest_earnings.py +1 -1
- kfinance/tool_calling/get_merger_info_from_transaction_id.py +1 -1
- kfinance/tool_calling/get_mergers_from_identifier.py +1 -1
- kfinance/tool_calling/get_n_quarters_ago.py +2 -1
- kfinance/tool_calling/get_next_earnings.py +1 -1
- kfinance/tool_calling/get_prices_from_identifier.py +4 -3
- kfinance/tool_calling/get_segments_from_identifier.py +3 -1
- kfinance/tool_calling/get_transcript.py +1 -1
- kfinance/tool_calling/resolve_identifier.py +1 -1
- kfinance/tool_calling/shared_models.py +20 -1
- kfinance/version.py +2 -2
- kensho_kfinance-2.6.5.dist-info/RECORD +0 -54
- {kensho_kfinance-2.6.5.dist-info → kensho_kfinance-2.8.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-2.6.5.dist-info → kensho_kfinance-2.8.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.6.5.dist-info → kensho_kfinance-2.8.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.6.5.dist-info → kensho_kfinance-2.8.0.dist-info}/top_level.txt +0 -0
- /kfinance/{tests/scratch.py → models/__init__.py} +0 -0
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
from datetime import date
|
|
2
1
|
from itertools import chain
|
|
3
|
-
from typing import
|
|
4
|
-
|
|
5
|
-
from strenum import StrEnum
|
|
2
|
+
from typing import TypedDict
|
|
6
3
|
|
|
7
4
|
|
|
8
5
|
class LineItemType(TypedDict):
|
|
@@ -12,150 +9,6 @@ class LineItemType(TypedDict):
|
|
|
12
9
|
spgi_name: str
|
|
13
10
|
|
|
14
11
|
|
|
15
|
-
class HistoryMetadata(TypedDict):
|
|
16
|
-
currency: str
|
|
17
|
-
symbol: str
|
|
18
|
-
exchange_name: str
|
|
19
|
-
instrument_type: str
|
|
20
|
-
first_trade_date: date
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class IdentificationTriple(NamedTuple):
|
|
24
|
-
trading_item_id: int
|
|
25
|
-
security_id: int
|
|
26
|
-
company_id: int
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class Capitalization(StrEnum):
|
|
30
|
-
"""The capitalization type"""
|
|
31
|
-
|
|
32
|
-
market_cap = "market_cap"
|
|
33
|
-
tev = "tev"
|
|
34
|
-
shares_outstanding = "shares_outstanding"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class PeriodType(StrEnum):
|
|
38
|
-
"""The period type"""
|
|
39
|
-
|
|
40
|
-
annual = "annual"
|
|
41
|
-
quarterly = "quarterly"
|
|
42
|
-
ltm = "ltm"
|
|
43
|
-
ytd = "ytd"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class Periodicity(StrEnum):
|
|
47
|
-
"""The frequency or interval at which the historical data points are sampled or aggregated. Periodicity is not the same as the date range. The date range specifies the time span over which the data is retrieved, while periodicity determines how the data within that date range is aggregated."""
|
|
48
|
-
|
|
49
|
-
day = "day"
|
|
50
|
-
week = "week"
|
|
51
|
-
month = "month"
|
|
52
|
-
year = "year"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class StatementType(StrEnum):
|
|
56
|
-
"""The type of financial statement"""
|
|
57
|
-
|
|
58
|
-
balance_sheet = "balance_sheet"
|
|
59
|
-
income_statement = "income_statement"
|
|
60
|
-
cashflow = "cashflow"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class SegmentType(StrEnum):
|
|
64
|
-
"""The type of segment"""
|
|
65
|
-
|
|
66
|
-
business = "business"
|
|
67
|
-
geographic = "geographic"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class BusinessRelationshipType(StrEnum):
|
|
71
|
-
"""The type of business relationship"""
|
|
72
|
-
|
|
73
|
-
supplier = "supplier"
|
|
74
|
-
customer = "customer"
|
|
75
|
-
distributor = "distributor"
|
|
76
|
-
franchisor = "franchisor"
|
|
77
|
-
franchisee = "franchisee"
|
|
78
|
-
landlord = "landlord"
|
|
79
|
-
tenant = "tenant"
|
|
80
|
-
licensor = "licensor"
|
|
81
|
-
licensee = "licensee"
|
|
82
|
-
creditor = "creditor"
|
|
83
|
-
borrower = "borrower"
|
|
84
|
-
lessor = "lessor"
|
|
85
|
-
lessee = "lessee"
|
|
86
|
-
strategic_alliance = "strategic_alliance"
|
|
87
|
-
investor_relations_firm = "investor_relations_firm"
|
|
88
|
-
investor_relations_client = "investor_relations_client"
|
|
89
|
-
transfer_agent = "transfer_agent"
|
|
90
|
-
transfer_agent_client = "transfer_agent_client"
|
|
91
|
-
vendor = "vendor"
|
|
92
|
-
client_services = "client_services"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
class Permission(StrEnum):
|
|
96
|
-
EarningsPermission = "EarningsPermission"
|
|
97
|
-
TranscriptsPermission = "TranscriptsPermission"
|
|
98
|
-
GICSPermission = "GICSPermission"
|
|
99
|
-
IDPermission = "IDPermission"
|
|
100
|
-
ISCRSPermission = "ISCRSPermission"
|
|
101
|
-
PricingPermission = "PricingPermission"
|
|
102
|
-
RelationshipPermission = "RelationshipPermission"
|
|
103
|
-
StatementsPermission = "StatementsPermission"
|
|
104
|
-
SegmentsPermission = "SegmentsPermission"
|
|
105
|
-
MergersPermission = "MergersPermission"
|
|
106
|
-
CompetitorsPermission = "CompetitorsPermission"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class CompetitorSource(StrEnum):
|
|
110
|
-
"""The source type of the competitor information: 'filing' (from SEC filings), 'key_dev' (from key developments), 'contact' (from contact relationships), 'third_party' (from third-party sources), 'self_identified' (self-identified), 'named_by_competitor' (from competitor's perspective)."""
|
|
111
|
-
|
|
112
|
-
all = "all"
|
|
113
|
-
filing = "filing"
|
|
114
|
-
key_dev = "key_dev"
|
|
115
|
-
contact = "contact"
|
|
116
|
-
third_party = "third_party"
|
|
117
|
-
self_identified = "self_identified"
|
|
118
|
-
named_by_competitor = "named_by_competitor"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class YearAndQuarter(TypedDict):
|
|
122
|
-
year: int
|
|
123
|
-
quarter: int
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class LatestAnnualPeriod(TypedDict):
|
|
127
|
-
latest_year: int
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class LatestQuarterlyPeriod(TypedDict):
|
|
131
|
-
latest_quarter: int
|
|
132
|
-
latest_year: int
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
class CurrentPeriod(TypedDict):
|
|
136
|
-
current_year: int
|
|
137
|
-
current_quarter: int
|
|
138
|
-
current_month: int
|
|
139
|
-
current_date: str
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
class LatestPeriods(TypedDict):
|
|
143
|
-
annual: LatestAnnualPeriod
|
|
144
|
-
quarterly: LatestQuarterlyPeriod
|
|
145
|
-
now: CurrentPeriod
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
class IndustryClassification(StrEnum):
|
|
149
|
-
sic = "sic"
|
|
150
|
-
naics = "naics"
|
|
151
|
-
nace = "nace"
|
|
152
|
-
anzsic = "anzsic"
|
|
153
|
-
spcapiqetf = "spcapiqetf"
|
|
154
|
-
spratings = "spratings"
|
|
155
|
-
gics = "gics"
|
|
156
|
-
simple = "simple"
|
|
157
|
-
|
|
158
|
-
|
|
159
12
|
# all of these values must be lower case keys
|
|
160
13
|
LINE_ITEMS: list[LineItemType] = [
|
|
161
14
|
{
|
|
@@ -1740,23 +1593,6 @@ LINE_ITEMS: list[LineItemType] = [
|
|
|
1740
1593
|
"spgi_name": "Total Debt/Equity",
|
|
1741
1594
|
},
|
|
1742
1595
|
]
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
1596
|
LINE_ITEM_NAMES_AND_ALIASES: list[str] = list(
|
|
1746
1597
|
chain(*[[line_item["name"]] + list(line_item["aliases"]) for line_item in LINE_ITEMS])
|
|
1747
1598
|
)
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
FINANCIAL_STATEMENTS: dict[str, int] = {
|
|
1751
|
-
"income_statement": 1,
|
|
1752
|
-
"balance_sheet": 2,
|
|
1753
|
-
"cash_flow": 3,
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
FINANCIAL_STATEMENTS_SYNONYMS: dict[str, int] = {
|
|
1757
|
-
"is": 1,
|
|
1758
|
-
"bs": 2,
|
|
1759
|
-
"cf": 3,
|
|
1760
|
-
"income_stmt": 1,
|
|
1761
|
-
"cashflow": 3,
|
|
1762
|
-
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from strenum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Permission(StrEnum):
|
|
5
|
+
CompetitorsPermission = "CompetitorsPermission"
|
|
6
|
+
EarningsPermission = "EarningsPermission"
|
|
7
|
+
GICSPermission = "GICSPermission"
|
|
8
|
+
IDPermission = "IDPermission"
|
|
9
|
+
ISCRSPermission = "ISCRSPermission"
|
|
10
|
+
MergersPermission = "MergersPermission"
|
|
11
|
+
PricingPermission = "PricingPermission"
|
|
12
|
+
RelationshipPermission = "RelationshipPermission"
|
|
13
|
+
SegmentsPermission = "SegmentsPermission"
|
|
14
|
+
StatementsPermission = "StatementsPermission"
|
|
15
|
+
TranscriptsPermission = "TranscriptsPermission"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, model_validator
|
|
6
|
+
|
|
7
|
+
from kfinance.decimal_with_unit import Money, Shares
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HistoryMetadata(TypedDict):
|
|
11
|
+
currency: str
|
|
12
|
+
symbol: str
|
|
13
|
+
exchange_name: str
|
|
14
|
+
instrument_type: str
|
|
15
|
+
first_trade_date: date
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Prices(BaseModel):
|
|
19
|
+
"""Prices represents prices for a stock for a specific "date".
|
|
20
|
+
|
|
21
|
+
I'm putting "date" in quotes because dates can be daily ("2024-01-01"),
|
|
22
|
+
weekly ("2024 Week 2"), monthly ("January 2024"), or annual ("2024").
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
date: str
|
|
26
|
+
open: Money
|
|
27
|
+
high: Money
|
|
28
|
+
low: Money
|
|
29
|
+
close: Money
|
|
30
|
+
volume: Shares
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PriceHistory(BaseModel):
|
|
34
|
+
"""PriceHistory represents stock prices over a time range."""
|
|
35
|
+
|
|
36
|
+
prices: list[Prices]
|
|
37
|
+
|
|
38
|
+
@model_validator(mode="before")
|
|
39
|
+
@classmethod
|
|
40
|
+
def inject_currency_into_data(cls, data: Any) -> Any:
|
|
41
|
+
"""Inject the currency into each open/high/low/close price.
|
|
42
|
+
|
|
43
|
+
The price history response only includes the currency as a top level element.
|
|
44
|
+
However, the Prices model expects the unit to be included with each price.
|
|
45
|
+
Before:
|
|
46
|
+
{
|
|
47
|
+
"date": "2024-06-25",
|
|
48
|
+
"open": "445.790000",
|
|
49
|
+
"high": "449.240000",
|
|
50
|
+
...
|
|
51
|
+
}
|
|
52
|
+
After:
|
|
53
|
+
{
|
|
54
|
+
"date": "2024-06-25",
|
|
55
|
+
"open": {"value": "445.790000", "unit": "USD"},
|
|
56
|
+
"high": {"value": "449.240000", "unit": "USD"},
|
|
57
|
+
...
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Note: Volume does not need the unit injected because the Shares class
|
|
61
|
+
already has "Shares" encoded. However, currencies differ between companies,
|
|
62
|
+
so we need to inject that information.
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(data, dict) and "currency" in data:
|
|
65
|
+
data = deepcopy(data)
|
|
66
|
+
currency = data["currency"]
|
|
67
|
+
for capitalization in data["prices"]:
|
|
68
|
+
for key in ["open", "high", "low", "close"]:
|
|
69
|
+
capitalization[key] = dict(unit=currency, value=capitalization[key])
|
|
70
|
+
return data
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
from decimal import Decimal
|
|
2
3
|
import time
|
|
3
4
|
from typing import Any, Dict
|
|
4
5
|
from unittest import TestCase
|
|
5
6
|
from unittest.mock import PropertyMock, patch
|
|
6
7
|
|
|
7
|
-
import numpy as np
|
|
8
|
-
import pandas as pd
|
|
9
8
|
import pytest
|
|
10
9
|
import requests
|
|
11
10
|
import requests_mock
|
|
12
11
|
|
|
13
12
|
from kfinance.batch_request_handling import MAX_WORKERS_CAP
|
|
13
|
+
from kfinance.decimal_with_unit import Money, Shares
|
|
14
14
|
from kfinance.fetch import KFinanceApiClient
|
|
15
|
-
from kfinance.kfinance import Companies, Company, Ticker, TradingItems
|
|
15
|
+
from kfinance.kfinance import Companies, Company, Ticker, TradingItem, TradingItems
|
|
16
|
+
from kfinance.models.price_models import PriceHistory, Prices
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@pytest.fixture(autouse=True)
|
|
@@ -77,46 +78,75 @@ class TestTradingItem(TestCase):
|
|
|
77
78
|
m.get(
|
|
78
79
|
"https://kfinance.kensho.com/api/v1/pricing/2/none/none/day/adjusted",
|
|
79
80
|
json={
|
|
81
|
+
"currency": "USD",
|
|
80
82
|
"prices": [
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
{
|
|
84
|
+
"date": "2024-04-11",
|
|
85
|
+
"open": "1.0000",
|
|
86
|
+
"high": "2.0000",
|
|
87
|
+
"low": "3.0000",
|
|
88
|
+
"close": "4.0000",
|
|
89
|
+
"volume": "5",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
84
92
|
},
|
|
85
93
|
)
|
|
86
94
|
m.get(
|
|
87
95
|
"https://kfinance.kensho.com/api/v1/pricing/3/none/none/day/adjusted",
|
|
88
96
|
json={
|
|
97
|
+
"currency": "USD",
|
|
89
98
|
"prices": [
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
{
|
|
100
|
+
"date": "2024-04-11",
|
|
101
|
+
"open": "5.0000",
|
|
102
|
+
"high": "6.0000",
|
|
103
|
+
"low": "7.0000",
|
|
104
|
+
"close": "8.0000",
|
|
105
|
+
"volume": "9",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
93
108
|
},
|
|
94
109
|
)
|
|
95
110
|
|
|
96
|
-
|
|
111
|
+
trading_item_2 = TradingItem(
|
|
112
|
+
kfinance_api_client=self.kfinance_api_client, trading_item_id=2
|
|
113
|
+
)
|
|
114
|
+
trading_item_3 = TradingItem(
|
|
115
|
+
kfinance_api_client=self.kfinance_api_client, trading_item_id=3
|
|
116
|
+
)
|
|
97
117
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
118
|
+
trading_items = TradingItems(
|
|
119
|
+
self.kfinance_api_client, trading_items=[trading_item_2, trading_item_3]
|
|
120
|
+
)
|
|
121
|
+
expected_result = {
|
|
122
|
+
trading_item_2: PriceHistory(
|
|
123
|
+
prices=[
|
|
124
|
+
Prices(
|
|
125
|
+
date="2024-04-11",
|
|
126
|
+
open=Money(value=Decimal("1.00"), unit="USD", conventional_decimals=2),
|
|
127
|
+
high=Money(value=Decimal("2.00"), unit="USD", conventional_decimals=2),
|
|
128
|
+
low=Money(value=Decimal("3.00"), unit="USD", conventional_decimals=2),
|
|
129
|
+
close=Money(value=Decimal("4.00"), unit="USD", conventional_decimals=2),
|
|
130
|
+
volume=Shares(value=Decimal("5"), unit="Shares", conventional_decimals=0),
|
|
131
|
+
)
|
|
132
|
+
]
|
|
133
|
+
),
|
|
134
|
+
trading_item_3: PriceHistory(
|
|
135
|
+
prices=[
|
|
136
|
+
Prices(
|
|
137
|
+
date="2024-04-11",
|
|
138
|
+
open=Money(value=Decimal("5.00"), unit="USD", conventional_decimals=2),
|
|
139
|
+
high=Money(value=Decimal("6.00"), unit="USD", conventional_decimals=2),
|
|
140
|
+
low=Money(value=Decimal("7.00"), unit="USD", conventional_decimals=2),
|
|
141
|
+
close=Money(value=Decimal("8.00"), unit="USD", conventional_decimals=2),
|
|
142
|
+
volume=Shares(value=Decimal("9"), unit="Shares", conventional_decimals=0),
|
|
143
|
+
)
|
|
144
|
+
]
|
|
145
|
+
),
|
|
108
146
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
trading_item_id = k.trading_item_id
|
|
113
|
-
pd.testing.assert_frame_equal(
|
|
114
|
-
v,
|
|
115
|
-
pd.DataFrame(expected_dictionary_based_result[trading_item_id])
|
|
116
|
-
.set_index("date")
|
|
117
|
-
.apply(pd.to_numeric)
|
|
118
|
-
.replace(np.nan, None),
|
|
119
|
-
)
|
|
147
|
+
|
|
148
|
+
result = trading_items.history()
|
|
149
|
+
assert result == expected_result
|
|
120
150
|
|
|
121
151
|
@requests_mock.Mocker()
|
|
122
152
|
def test_large_batch_request_property(self, m):
|
kfinance/tests/test_client.py
CHANGED
|
@@ -2,8 +2,8 @@ from unittest.mock import Mock
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import Permission
|
|
6
5
|
from kfinance.kfinance import Client
|
|
6
|
+
from kfinance.models.permission_models import Permission
|
|
7
7
|
from kfinance.tool_calling import (
|
|
8
8
|
GetBusinessRelationshipFromIdentifier,
|
|
9
9
|
GetEarnings,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from kfinance.decimal_with_unit import DecimalWithUnit, Money, Shares
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestDecimalWithUnit:
|
|
10
|
+
@pytest.mark.parametrize(
|
|
11
|
+
"conventional_decimals, input, expected_value",
|
|
12
|
+
[
|
|
13
|
+
pytest.param(2, "0.10", Decimal("0.10"), id="input matches conventional decimals"),
|
|
14
|
+
pytest.param(
|
|
15
|
+
1, "0.10", Decimal("0.1"), id="input more precise than conventional decimals"
|
|
16
|
+
),
|
|
17
|
+
pytest.param(
|
|
18
|
+
3, "0.10", Decimal("0.100"), id="input less precise than conventional decimals"
|
|
19
|
+
),
|
|
20
|
+
pytest.param(1, "0.11", Decimal("0.1"), id="input gets rounded if necessary"),
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
def test_quantize_value(
|
|
24
|
+
self, conventional_decimals: int, input: str, expected_value: Decimal
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
WHEN a DecimalWithUnit gets deserialized
|
|
28
|
+
THEN the value gets quantized to the conventional_decimals
|
|
29
|
+
"""
|
|
30
|
+
dwu = DecimalWithUnit.model_validate(
|
|
31
|
+
dict(value=input, conventional_decimals=conventional_decimals, unit="foo")
|
|
32
|
+
)
|
|
33
|
+
assert dwu.value == expected_value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestMoney:
|
|
37
|
+
@pytest.mark.parametrize("currency, expected_conventional_decimals", [("USD", 2), ("BIF", 0)])
|
|
38
|
+
def test_conventional_decimals_injection(
|
|
39
|
+
self, currency: str, expected_conventional_decimals: int
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
GIVEN a value and unit dict
|
|
43
|
+
WHEN we deserialize the dict into a Money object
|
|
44
|
+
THEN the conventional decimals for the currency get injected.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
money = Money.model_validate({"value": 1, "unit": currency})
|
|
48
|
+
assert money.conventional_decimals == expected_conventional_decimals
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestShares:
|
|
52
|
+
@pytest.mark.parametrize("input", ["1", 1, Decimal(1), {"value": 1}])
|
|
53
|
+
def test_share_deserialization(self, input: Any):
|
|
54
|
+
"""
|
|
55
|
+
GIVEN a str, int, decimal, or dict with value key
|
|
56
|
+
WHEN we deserialize the input into a Shares object
|
|
57
|
+
THEN all of these inputs are accepted.
|
|
58
|
+
"""
|
|
59
|
+
shares = Shares.model_validate(input)
|
|
60
|
+
assert shares.value == Decimal(1)
|
kfinance/tests/test_fetch.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from unittest import TestCase
|
|
2
2
|
from unittest.mock import MagicMock
|
|
3
3
|
|
|
4
|
+
from pydantic import ValidationError
|
|
4
5
|
import pytest
|
|
5
6
|
from requests_mock import Mocker
|
|
6
7
|
|
|
7
|
-
from kfinance.constants import BusinessRelationshipType, Periodicity, PeriodType
|
|
8
8
|
from kfinance.fetch import KFinanceApiClient
|
|
9
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
|
|
10
12
|
from kfinance.pydantic_models import (
|
|
11
13
|
CompanyIdAndName,
|
|
12
14
|
RelationshipResponse,
|
|
@@ -53,21 +55,28 @@ class TestFetchItem(TestCase):
|
|
|
53
55
|
expected_fetch_url = (
|
|
54
56
|
f"{self.kfinance_api_client.url_base}pricing/{trading_item_id}/none/none/none/adjusted"
|
|
55
57
|
)
|
|
56
|
-
|
|
58
|
+
# Validation error is ok, we only care that the function was called with the correct url
|
|
59
|
+
with pytest.raises(ValidationError):
|
|
60
|
+
self.kfinance_api_client.fetch_history(trading_item_id=trading_item_id)
|
|
57
61
|
self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
|
|
58
62
|
|
|
63
|
+
def test_fetch_history_with_dates(self) -> None:
|
|
64
|
+
trading_item_id = 2629108
|
|
59
65
|
start_date = "2025-01-01"
|
|
60
66
|
end_date = "2025-01-31"
|
|
61
67
|
is_adjusted = False
|
|
62
68
|
periodicity = Periodicity.day
|
|
63
69
|
expected_fetch_url = f"{self.kfinance_api_client.url_base}pricing/{trading_item_id}/{start_date}/{end_date}/{periodicity.value}/unadjusted"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
|
|
71
|
+
# Validation error is ok, we only care that the function was called with the correct url
|
|
72
|
+
with pytest.raises(ValidationError):
|
|
73
|
+
self.kfinance_api_client.fetch_history(
|
|
74
|
+
trading_item_id=trading_item_id,
|
|
75
|
+
is_adjusted=is_adjusted,
|
|
76
|
+
start_date=start_date,
|
|
77
|
+
end_date=end_date,
|
|
78
|
+
periodicity=periodicity,
|
|
79
|
+
)
|
|
71
80
|
self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
|
|
72
81
|
|
|
73
82
|
def test_fetch_history_metadata(self) -> None:
|
|
@@ -310,9 +319,11 @@ class TestMarketCap:
|
|
|
310
319
|
expected_fetch_url = (
|
|
311
320
|
f"{client.url_base}market_cap/{company_id}/{start_date_url}/{end_date_url}"
|
|
312
321
|
)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
# Validation error is ok, we only care that the function was called with the correct url
|
|
323
|
+
with pytest.raises(ValidationError):
|
|
324
|
+
client.fetch_market_caps_tevs_and_shares_outstanding(
|
|
325
|
+
company_id=company_id, start_date=start_date, end_date=end_date
|
|
326
|
+
)
|
|
316
327
|
client.fetch.assert_called_with(expected_fetch_url)
|
|
317
328
|
|
|
318
329
|
def test_fetch_permissions(self):
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from kfinance.decimal_with_unit import Money, Shares
|
|
5
|
+
from kfinance.models.capitalization_models import (
|
|
6
|
+
Capitalization,
|
|
7
|
+
Capitalizations,
|
|
8
|
+
DailyCapitalization,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCapitalizations:
|
|
13
|
+
api_resp = {
|
|
14
|
+
"currency": "USD",
|
|
15
|
+
"market_caps": [
|
|
16
|
+
{
|
|
17
|
+
"date": "2024-06-24",
|
|
18
|
+
"market_cap": "139231113000.000000",
|
|
19
|
+
"tev": "153942113000.000000",
|
|
20
|
+
"shares_outstanding": 312900000,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"date": "2024-06-25",
|
|
24
|
+
"market_cap": "140423262000.000000",
|
|
25
|
+
"tev": "155134262000.000000",
|
|
26
|
+
"shares_outstanding": 312900000,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def test_capitalizations_deserialization(self) -> None:
|
|
32
|
+
"""
|
|
33
|
+
GIVEN a capitalizations API response
|
|
34
|
+
WHEN we deserialize the response into a Capitalizations object
|
|
35
|
+
THEN the deserialization succeeds and returns the expected value.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
expected_capitalizations = Capitalizations.model_construct(
|
|
39
|
+
capitalizations=[
|
|
40
|
+
DailyCapitalization(
|
|
41
|
+
date=date(2024, 6, 24),
|
|
42
|
+
market_cap=Money(
|
|
43
|
+
value=Decimal("139231113000.00"), unit="USD", conventional_decimals=2
|
|
44
|
+
),
|
|
45
|
+
tev=Money(
|
|
46
|
+
value=Decimal("153942113000.00"), unit="USD", conventional_decimals=2
|
|
47
|
+
),
|
|
48
|
+
shares_outstanding=Shares(
|
|
49
|
+
value=Decimal("312900000"), unit="Shares", conventional_decimals=0
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
DailyCapitalization(
|
|
53
|
+
date=date(2024, 6, 25),
|
|
54
|
+
market_cap=Money(
|
|
55
|
+
value=Decimal("140423262000.00"), unit="USD", conventional_decimals=2
|
|
56
|
+
),
|
|
57
|
+
tev=Money(
|
|
58
|
+
value=Decimal("155134262000.00"), unit="USD", conventional_decimals=2
|
|
59
|
+
),
|
|
60
|
+
shares_outstanding=Shares(
|
|
61
|
+
value=Decimal("312900000"), unit="Shares", conventional_decimals=0
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert Capitalizations.model_validate(self.api_resp) == expected_capitalizations
|
|
68
|
+
|
|
69
|
+
def test_single_attribute_serialization(self):
|
|
70
|
+
"""
|
|
71
|
+
GIVEN a Capitalizations object
|
|
72
|
+
WHEN we only want to jsonify a single attribute like market_cap
|
|
73
|
+
THEN that attribute gets returned in an LLM-readable, jsonifyable dict format
|
|
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
|
+
}
|
|
81
|
+
capitalizations = Capitalizations.model_validate(self.api_resp)
|
|
82
|
+
serialized = capitalizations.jsonify_single_attribute(Capitalization.market_cap)
|
|
83
|
+
assert serialized == expected_serialization
|