kensho-kfinance 3.2.4__py3-none-any.whl → 4.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.
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/METADATA +3 -3
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/RECORD +57 -56
- kfinance/CHANGELOG.md +51 -0
- kfinance/client/batch_request_handling.py +3 -1
- kfinance/client/fetch.py +127 -54
- kfinance/client/kfinance.py +38 -39
- kfinance/client/meta_classes.py +50 -20
- kfinance/client/models/date_and_period_models.py +32 -7
- kfinance/client/models/decimal_with_unit.py +14 -2
- kfinance/client/models/response_models.py +33 -0
- kfinance/client/models/tests/test_decimal_with_unit.py +9 -0
- kfinance/client/tests/test_batch_requests.py +5 -4
- kfinance/client/tests/test_fetch.py +134 -58
- kfinance/client/tests/test_objects.py +207 -145
- kfinance/conftest.py +10 -0
- kfinance/domains/business_relationships/business_relationship_tools.py +17 -8
- kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -16
- kfinance/domains/capitalizations/capitalization_models.py +7 -5
- kfinance/domains/capitalizations/capitalization_tools.py +38 -20
- kfinance/domains/capitalizations/tests/test_capitalization_tools.py +66 -36
- kfinance/domains/companies/company_models.py +22 -2
- kfinance/domains/companies/company_tools.py +49 -16
- kfinance/domains/companies/tests/test_company_tools.py +27 -9
- kfinance/domains/competitors/competitor_tools.py +19 -5
- kfinance/domains/competitors/tests/test_competitor_tools.py +22 -19
- kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +29 -8
- kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +13 -8
- kfinance/domains/earnings/earning_tools.py +73 -29
- kfinance/domains/earnings/tests/test_earnings_tools.py +52 -43
- kfinance/domains/line_items/line_item_models.py +372 -16
- kfinance/domains/line_items/line_item_tools.py +198 -46
- kfinance/domains/line_items/tests/test_line_item_tools.py +305 -39
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +46 -2
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -74
- kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +61 -59
- kfinance/domains/prices/price_models.py +7 -6
- kfinance/domains/prices/price_tools.py +24 -16
- kfinance/domains/prices/tests/test_price_tools.py +47 -39
- kfinance/domains/segments/segment_models.py +17 -3
- kfinance/domains/segments/segment_tools.py +102 -42
- kfinance/domains/segments/tests/test_segment_tools.py +166 -37
- kfinance/domains/statements/statement_models.py +17 -3
- kfinance/domains/statements/statement_tools.py +130 -46
- kfinance/domains/statements/tests/test_statement_tools.py +251 -49
- kfinance/integrations/local_mcp/kfinance_mcp.py +1 -1
- kfinance/integrations/tests/test_example_notebook.py +57 -16
- kfinance/integrations/tool_calling/all_tools.py +5 -1
- kfinance/integrations/tool_calling/static_tools/get_n_quarters_ago.py +5 -0
- kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +13 -10
- kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +2 -1
- kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +15 -4
- kfinance/integrations/tool_calling/tool_calling_models.py +18 -6
- kfinance/version.py +2 -2
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/top_level.txt +0 -0
kfinance/client/kfinance.py
CHANGED
|
@@ -31,13 +31,21 @@ from kfinance.client.meta_classes import (
|
|
|
31
31
|
DelegatedCompanyFunctionsMetaClass,
|
|
32
32
|
)
|
|
33
33
|
from kfinance.client.models.date_and_period_models import (
|
|
34
|
+
CurrentPeriod,
|
|
35
|
+
LatestAnnualPeriod,
|
|
34
36
|
LatestPeriods,
|
|
37
|
+
LatestQuarterlyPeriod,
|
|
35
38
|
Periodicity,
|
|
36
39
|
YearAndQuarter,
|
|
37
40
|
)
|
|
38
41
|
from kfinance.client.server_thread import ServerThread
|
|
39
42
|
from kfinance.domains.companies.company_models import IdentificationTriple
|
|
40
43
|
from kfinance.domains.earnings.earning_models import EarningsCall, TranscriptComponent
|
|
44
|
+
from kfinance.domains.mergers_and_acquisitions.merger_and_acquisition_models import (
|
|
45
|
+
MergerConsideration,
|
|
46
|
+
MergerInfo,
|
|
47
|
+
MergerTimelineElement,
|
|
48
|
+
)
|
|
41
49
|
from kfinance.domains.prices.price_models import HistoryMetadataResp, PriceHistory
|
|
42
50
|
|
|
43
51
|
|
|
@@ -1266,24 +1274,13 @@ class MergerOrAcquisition:
|
|
|
1266
1274
|
self.transaction_id = transaction_id
|
|
1267
1275
|
self.merger_title = merger_title
|
|
1268
1276
|
self.closed_date = closed_date
|
|
1269
|
-
self._merger_info:
|
|
1277
|
+
self._merger_info: MergerInfo | None = None
|
|
1270
1278
|
|
|
1271
1279
|
@property
|
|
1272
|
-
def merger_info(self) ->
|
|
1280
|
+
def merger_info(self) -> MergerInfo:
|
|
1273
1281
|
"""Property for the combined information in the merger."""
|
|
1274
1282
|
if not self._merger_info:
|
|
1275
1283
|
self._merger_info = self.kfinance_api_client.fetch_merger_info(self.transaction_id)
|
|
1276
|
-
if "timeline" in self._merger_info and self._merger_info["timeline"]:
|
|
1277
|
-
timeline = pd.DataFrame(self._merger_info["timeline"])
|
|
1278
|
-
timeline["date"] = pd.to_datetime(timeline["date"])
|
|
1279
|
-
self._merger_info["timeline"] = timeline
|
|
1280
|
-
if (
|
|
1281
|
-
"consideration" in self._merger_info
|
|
1282
|
-
and self._merger_info["consideration"]
|
|
1283
|
-
and "details" in self._merger_info["consideration"]
|
|
1284
|
-
):
|
|
1285
|
-
details = pd.DataFrame(self._merger_info["consideration"]["details"])
|
|
1286
|
-
self._merger_info["consideration"]["details"] = details
|
|
1287
1284
|
return self._merger_info
|
|
1288
1285
|
|
|
1289
1286
|
@property
|
|
@@ -1292,9 +1289,9 @@ class MergerOrAcquisition:
|
|
|
1292
1289
|
return self.merger_title
|
|
1293
1290
|
|
|
1294
1291
|
@property
|
|
1295
|
-
def get_timeline(self) ->
|
|
1292
|
+
def get_timeline(self) -> list[MergerTimelineElement]:
|
|
1296
1293
|
"""The timeline of the merger includes every new status, along with the dates of each status change."""
|
|
1297
|
-
return self.merger_info
|
|
1294
|
+
return self.merger_info.timeline
|
|
1298
1295
|
|
|
1299
1296
|
@property
|
|
1300
1297
|
def get_participants(self) -> dict:
|
|
@@ -1308,8 +1305,8 @@ class MergerOrAcquisition:
|
|
|
1308
1305
|
transaction_id=self.transaction_id,
|
|
1309
1306
|
company=Company(
|
|
1310
1307
|
kfinance_api_client=self.kfinance_api_client,
|
|
1311
|
-
company_id=self.merger_info
|
|
1312
|
-
company_name=self.merger_info
|
|
1308
|
+
company_id=self.merger_info.participants.target.company_id,
|
|
1309
|
+
company_name=self.merger_info.participants.target.company_name,
|
|
1313
1310
|
),
|
|
1314
1311
|
),
|
|
1315
1312
|
"buyers": [
|
|
@@ -1318,11 +1315,11 @@ class MergerOrAcquisition:
|
|
|
1318
1315
|
transaction_id=self.transaction_id,
|
|
1319
1316
|
company=Company(
|
|
1320
1317
|
kfinance_api_client=self.kfinance_api_client,
|
|
1321
|
-
company_id=company
|
|
1322
|
-
company_name=company
|
|
1318
|
+
company_id=company.company_id,
|
|
1319
|
+
company_name=company.company_name,
|
|
1323
1320
|
),
|
|
1324
1321
|
)
|
|
1325
|
-
for company in self.merger_info
|
|
1322
|
+
for company in self.merger_info.participants.buyers
|
|
1326
1323
|
],
|
|
1327
1324
|
"sellers": [
|
|
1328
1325
|
ParticipantInMerger(
|
|
@@ -1330,16 +1327,16 @@ class MergerOrAcquisition:
|
|
|
1330
1327
|
transaction_id=self.transaction_id,
|
|
1331
1328
|
company=Company(
|
|
1332
1329
|
kfinance_api_client=self.kfinance_api_client,
|
|
1333
|
-
company_id=company
|
|
1334
|
-
company_name=company
|
|
1330
|
+
company_id=company.company_id,
|
|
1331
|
+
company_name=company.company_name,
|
|
1335
1332
|
),
|
|
1336
1333
|
)
|
|
1337
|
-
for company in self.merger_info
|
|
1334
|
+
for company in self.merger_info.participants.sellers
|
|
1338
1335
|
],
|
|
1339
1336
|
}
|
|
1340
1337
|
|
|
1341
1338
|
@property
|
|
1342
|
-
def get_consideration(self) ->
|
|
1339
|
+
def get_consideration(self) -> MergerConsideration:
|
|
1343
1340
|
"""A merger's consideration is the assets exchanged for the target company.
|
|
1344
1341
|
|
|
1345
1342
|
Properties in the consideration include:
|
|
@@ -1354,7 +1351,7 @@ class MergerOrAcquisition:
|
|
|
1354
1351
|
- The number of shares in the target company.
|
|
1355
1352
|
- The current gross total of the consideration detail.
|
|
1356
1353
|
"""
|
|
1357
|
-
return self.merger_info
|
|
1354
|
+
return self.merger_info.consideration
|
|
1358
1355
|
|
|
1359
1356
|
|
|
1360
1357
|
@add_methods_of_singular_class_to_iterable_class(Company)
|
|
@@ -1903,16 +1900,18 @@ class Client:
|
|
|
1903
1900
|
most_recent_year_annual = current_year - 1
|
|
1904
1901
|
|
|
1905
1902
|
current_month = datetime_now.month
|
|
1906
|
-
latest
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1903
|
+
latest = LatestPeriods(
|
|
1904
|
+
annual=LatestAnnualPeriod(latest_year=most_recent_year_annual),
|
|
1905
|
+
quarterly=LatestQuarterlyPeriod(
|
|
1906
|
+
latest_quarter=most_recent_qtr, latest_year=most_recent_year_qtrly
|
|
1907
|
+
),
|
|
1908
|
+
now=CurrentPeriod(
|
|
1909
|
+
current_year=current_year,
|
|
1910
|
+
current_quarter=current_qtr,
|
|
1911
|
+
current_month=current_month,
|
|
1912
|
+
current_date=datetime_now.date(),
|
|
1913
|
+
),
|
|
1914
|
+
)
|
|
1916
1915
|
return latest
|
|
1917
1916
|
|
|
1918
1917
|
@staticmethod
|
|
@@ -1932,9 +1931,9 @@ class Client:
|
|
|
1932
1931
|
year_n_quarters_ago = total_quarters_completed_n_quarters_ago // 4
|
|
1933
1932
|
quarter_n_quarters_ago = total_quarters_completed_n_quarters_ago % 4 + 1
|
|
1934
1933
|
|
|
1935
|
-
year_quarter_n_quarters_ago
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1934
|
+
year_quarter_n_quarters_ago = YearAndQuarter(
|
|
1935
|
+
year=year_n_quarters_ago,
|
|
1936
|
+
quarter=quarter_n_quarters_ago,
|
|
1937
|
+
)
|
|
1939
1938
|
|
|
1940
1939
|
return year_quarter_n_quarters_ago
|
kfinance/client/meta_classes.py
CHANGED
|
@@ -85,22 +85,34 @@ class CompanyFunctionsMetaClass:
|
|
|
85
85
|
except ValueError:
|
|
86
86
|
return pd.DataFrame()
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
start_quarter=start_quarter,
|
|
97
|
-
end_quarter=end_quarter,
|
|
98
|
-
).model_dump(mode="json")["statements"]
|
|
99
|
-
)
|
|
100
|
-
.apply(pd.to_numeric)
|
|
101
|
-
.replace(np.nan, None)
|
|
88
|
+
statement_response = self.kfinance_api_client.fetch_statement(
|
|
89
|
+
company_ids=[self.company_id],
|
|
90
|
+
statement_type=statement_type,
|
|
91
|
+
period_type=period_type,
|
|
92
|
+
start_year=start_year,
|
|
93
|
+
end_year=end_year,
|
|
94
|
+
start_quarter=start_quarter,
|
|
95
|
+
end_quarter=end_quarter,
|
|
102
96
|
)
|
|
103
97
|
|
|
98
|
+
if not statement_response.results:
|
|
99
|
+
return pd.DataFrame()
|
|
100
|
+
|
|
101
|
+
# Get the first (and only) result
|
|
102
|
+
statement_resp = list(statement_response.results.values())[0]
|
|
103
|
+
periods = statement_resp.model_dump(mode="json")["periods"]
|
|
104
|
+
|
|
105
|
+
# Extract statements data from each period
|
|
106
|
+
statements_data = {}
|
|
107
|
+
for period_key, period_data in periods.items():
|
|
108
|
+
period_statements = {}
|
|
109
|
+
for statement in period_data["statements"]:
|
|
110
|
+
for line_item in statement["line_items"]:
|
|
111
|
+
period_statements[line_item["name"]] = line_item["value"]
|
|
112
|
+
statements_data[period_key] = period_statements
|
|
113
|
+
|
|
114
|
+
return pd.DataFrame(statements_data).apply(pd.to_numeric).replace(np.nan, None)
|
|
115
|
+
|
|
104
116
|
def income_statement(
|
|
105
117
|
self,
|
|
106
118
|
period_type: Optional[PeriodType] = None,
|
|
@@ -212,8 +224,8 @@ class CompanyFunctionsMetaClass:
|
|
|
212
224
|
except ValueError:
|
|
213
225
|
return pd.DataFrame()
|
|
214
226
|
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
response = self.kfinance_api_client.fetch_line_item(
|
|
228
|
+
company_ids=[self.company_id],
|
|
217
229
|
line_item=line_item,
|
|
218
230
|
period_type=period_type,
|
|
219
231
|
start_year=start_year,
|
|
@@ -221,8 +233,19 @@ class CompanyFunctionsMetaClass:
|
|
|
221
233
|
start_quarter=start_quarter,
|
|
222
234
|
end_quarter=end_quarter,
|
|
223
235
|
)
|
|
236
|
+
|
|
237
|
+
if not response.results:
|
|
238
|
+
return pd.DataFrame()
|
|
239
|
+
|
|
240
|
+
# Get the first (and only) result
|
|
241
|
+
line_item_response = list(response.results.values())[0]
|
|
242
|
+
|
|
243
|
+
line_item_data = {}
|
|
244
|
+
for period_key, period_data in line_item_response.periods.items():
|
|
245
|
+
line_item_data[period_key] = period_data.line_item.value
|
|
246
|
+
|
|
224
247
|
return (
|
|
225
|
-
pd.DataFrame({"line_item":
|
|
248
|
+
pd.DataFrame({"line_item": line_item_data})
|
|
226
249
|
.transpose()
|
|
227
250
|
.apply(pd.to_numeric)
|
|
228
251
|
.replace(np.nan, None)
|
|
@@ -351,15 +374,22 @@ class CompanyFunctionsMetaClass:
|
|
|
351
374
|
except ValueError:
|
|
352
375
|
return {}
|
|
353
376
|
|
|
354
|
-
|
|
355
|
-
|
|
377
|
+
segments_response = self.kfinance_api_client.fetch_segments(
|
|
378
|
+
company_ids=[self.company_id],
|
|
356
379
|
segment_type=segment_type,
|
|
357
380
|
period_type=period_type,
|
|
358
381
|
start_year=start_year,
|
|
359
382
|
end_year=end_year,
|
|
360
383
|
start_quarter=start_quarter,
|
|
361
384
|
end_quarter=end_quarter,
|
|
362
|
-
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if not segments_response.results:
|
|
388
|
+
return {}
|
|
389
|
+
|
|
390
|
+
# Get the first (and only) result
|
|
391
|
+
segments_resp = list(segments_response.results.values())[0]
|
|
392
|
+
return segments_resp.model_dump(mode="json")["periods"]
|
|
363
393
|
|
|
364
394
|
def business_segments(
|
|
365
395
|
self,
|
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import date
|
|
2
|
+
from typing import Annotated
|
|
2
3
|
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
3
5
|
from strenum import StrEnum
|
|
4
6
|
|
|
5
7
|
|
|
8
|
+
# Constrained integer types for period counts
|
|
9
|
+
NumPeriods = Annotated[
|
|
10
|
+
int,
|
|
11
|
+
Field(ge=1, le=99, description="The number of periods to retrieve data for (1-99)"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
NumPeriodsBack = Annotated[
|
|
15
|
+
int,
|
|
16
|
+
Field(
|
|
17
|
+
ge=0,
|
|
18
|
+
le=99,
|
|
19
|
+
description="The end period of the data range expressed as number of periods back relative to the present period (0-99)",
|
|
20
|
+
),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
class PeriodType(StrEnum):
|
|
7
25
|
"""The period type"""
|
|
8
26
|
|
|
@@ -21,28 +39,35 @@ class Periodicity(StrEnum):
|
|
|
21
39
|
year = "year"
|
|
22
40
|
|
|
23
41
|
|
|
24
|
-
class
|
|
42
|
+
class EndpointType(StrEnum):
|
|
43
|
+
"""The type of API endpoint to use for financial data retrieval"""
|
|
44
|
+
|
|
45
|
+
absolute = "absolute"
|
|
46
|
+
relative = "relative"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class YearAndQuarter(BaseModel):
|
|
25
50
|
year: int
|
|
26
51
|
quarter: int
|
|
27
52
|
|
|
28
53
|
|
|
29
|
-
class LatestAnnualPeriod(
|
|
54
|
+
class LatestAnnualPeriod(BaseModel):
|
|
30
55
|
latest_year: int
|
|
31
56
|
|
|
32
57
|
|
|
33
|
-
class LatestQuarterlyPeriod(
|
|
58
|
+
class LatestQuarterlyPeriod(BaseModel):
|
|
34
59
|
latest_quarter: int
|
|
35
60
|
latest_year: int
|
|
36
61
|
|
|
37
62
|
|
|
38
|
-
class CurrentPeriod(
|
|
63
|
+
class CurrentPeriod(BaseModel):
|
|
39
64
|
current_year: int
|
|
40
65
|
current_quarter: int
|
|
41
66
|
current_month: int
|
|
42
|
-
current_date:
|
|
67
|
+
current_date: date
|
|
43
68
|
|
|
44
69
|
|
|
45
|
-
class LatestPeriods(
|
|
70
|
+
class LatestPeriods(BaseModel):
|
|
46
71
|
annual: LatestAnnualPeriod
|
|
47
72
|
quarterly: LatestQuarterlyPeriod
|
|
48
73
|
now: CurrentPeriod
|
|
@@ -2,7 +2,7 @@ from copy import deepcopy
|
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field, model_validator
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
6
6
|
from typing_extensions import Self
|
|
7
7
|
|
|
8
8
|
from kfinance.client.models.currency_models import ISO_CODE_TO_CURRENCY
|
|
@@ -20,11 +20,23 @@ class DecimalWithUnit(BaseModel):
|
|
|
20
20
|
existing subclass like `Money` or `Shares` or create a new one.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
value: Decimal
|
|
23
|
+
value: Decimal = Field(allow_inf_nan=True)
|
|
24
24
|
unit: str
|
|
25
25
|
# exclude conventional_decimals from serialization
|
|
26
26
|
conventional_decimals: int = Field(exclude=True)
|
|
27
27
|
|
|
28
|
+
@field_validator("value", mode="before")
|
|
29
|
+
@classmethod
|
|
30
|
+
def convert_none_to_nan(cls, v: Any) -> Any:
|
|
31
|
+
"""Convert None values to NaN.
|
|
32
|
+
|
|
33
|
+
Price data can include None for open prices.
|
|
34
|
+
https://kfinance.kensho.com/api/v1/pricing/37284793/2003-01-01/2024-12-31/month/adjusted
|
|
35
|
+
"""
|
|
36
|
+
if v is None:
|
|
37
|
+
return Decimal("NaN")
|
|
38
|
+
return v
|
|
39
|
+
|
|
28
40
|
@model_validator(mode="after")
|
|
29
41
|
def quantize_value(self) -> Self:
|
|
30
42
|
"""Quantize the value at the end of the deserialization.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, Generic, TypeAlias, TypeVar
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, model_serializer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T", bound=BaseModel)
|
|
7
|
+
|
|
8
|
+
Source: TypeAlias = dict[str, str]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RespWithErrors(BaseModel):
|
|
12
|
+
"""A response with an `errors` field.
|
|
13
|
+
|
|
14
|
+
- `errors` is always the last field in the response.
|
|
15
|
+
- `errors` is only included if there is at least one error.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
errors: dict[str, str] = Field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
@model_serializer(mode="wrap")
|
|
21
|
+
def serialize_model(self, handler: Callable) -> Dict[str, Any]:
|
|
22
|
+
"""Make `errors` the last response field and only include if there is at least one error."""
|
|
23
|
+
data = handler(self)
|
|
24
|
+
errors = data.pop("errors")
|
|
25
|
+
if errors:
|
|
26
|
+
data["errors"] = errors
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PostResponse(RespWithErrors, Generic[T]):
|
|
31
|
+
"""Generic response class that wraps results and errors from API calls."""
|
|
32
|
+
|
|
33
|
+
results: dict[str, T]
|
|
@@ -32,6 +32,15 @@ class TestDecimalWithUnit:
|
|
|
32
32
|
)
|
|
33
33
|
assert dwu.value == expected_value
|
|
34
34
|
|
|
35
|
+
@pytest.mark.parametrize("value", [None, "NaN"])
|
|
36
|
+
def test_null_nan_allowed(self, value: str | None):
|
|
37
|
+
"""
|
|
38
|
+
WHEN a DecimalWithUnit gets deserialized with None or "NaN"
|
|
39
|
+
THEN the deserialized value is Decimal("NaN")
|
|
40
|
+
"""
|
|
41
|
+
dwu = DecimalWithUnit.model_validate(dict(value=value, conventional_decimals=1, unit="foo"))
|
|
42
|
+
assert dwu.value.is_nan()
|
|
43
|
+
|
|
35
44
|
|
|
36
45
|
class TestMoney:
|
|
37
46
|
@pytest.mark.parametrize("currency, expected_conventional_decimals", [("USD", 2), ("BIF", 0)])
|
|
@@ -212,11 +212,12 @@ class TestTradingItem(TestCase):
|
|
|
212
212
|
)
|
|
213
213
|
m.get("https://kfinance.kensho.com/api/v1/info/1002", status_code=400)
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
companies = Companies(self.kfinance_api_client, [1001, 1002])
|
|
216
|
+
result = companies.city
|
|
217
|
+
id_based_result = self.company_object_keys_as_company_id(result)
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
expected_id_based_result = {1001: "Mock City A", 1002: None}
|
|
220
|
+
self.assertDictEqual(id_based_result, expected_id_based_result)
|
|
220
221
|
|
|
221
222
|
@requests_mock.Mocker()
|
|
222
223
|
def test_batch_request_500(self, m):
|