kensho-kfinance 2.7.0__py3-none-any.whl → 2.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kensho-kfinance might be problematic. Click here for more details.

Files changed (58) hide show
  1. {kensho_kfinance-2.7.0.dist-info → kensho_kfinance-2.9.0.dist-info}/METADATA +2 -2
  2. kensho_kfinance-2.9.0.dist-info/RECORD +70 -0
  3. kfinance/CHANGELOG.md +6 -0
  4. kfinance/decimal_with_unit.py +78 -0
  5. kfinance/fetch.py +15 -16
  6. kfinance/kfinance.py +108 -122
  7. kfinance/meta_classes.py +28 -27
  8. kfinance/models/business_relationship_models.py +26 -0
  9. kfinance/models/capitalization_models.py +90 -0
  10. kfinance/models/competitor_models.py +13 -0
  11. kfinance/models/currency_models.py +345 -0
  12. kfinance/models/date_and_period_models.py +48 -0
  13. kfinance/models/id_models.py +7 -0
  14. kfinance/models/industry_models.py +12 -0
  15. kfinance/{constants.py → models/line_item_models.py} +1 -165
  16. kfinance/models/permission_models.py +15 -0
  17. kfinance/models/price_models.py +70 -0
  18. kfinance/models/segment_models.py +8 -0
  19. kfinance/models/statement_models.py +9 -0
  20. kfinance/tests/test_batch_requests.py +61 -31
  21. kfinance/tests/test_client.py +1 -1
  22. kfinance/tests/test_decimal_with_unit.py +60 -0
  23. kfinance/tests/test_example_notebook.py +1 -0
  24. kfinance/tests/test_fetch.py +23 -12
  25. kfinance/tests/test_models/__init__.py +0 -0
  26. kfinance/tests/test_models/test_capitalization_models.py +83 -0
  27. kfinance/tests/test_models/test_price_models.py +58 -0
  28. kfinance/tests/test_objects.py +107 -57
  29. kfinance/tests/test_tools.py +35 -11
  30. kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +10 -7
  31. kfinance/tool_calling/get_business_relationship_from_identifier.py +2 -1
  32. kfinance/tool_calling/get_capitalization_from_identifier.py +4 -5
  33. kfinance/tool_calling/get_competitors_from_identifier.py +2 -1
  34. kfinance/tool_calling/get_cusip_from_ticker.py +1 -1
  35. kfinance/tool_calling/get_earnings.py +1 -1
  36. kfinance/tool_calling/get_financial_line_item_from_identifier.py +3 -1
  37. kfinance/tool_calling/get_financial_statement_from_identifier.py +3 -1
  38. kfinance/tool_calling/get_history_metadata_from_identifier.py +2 -1
  39. kfinance/tool_calling/get_info_from_identifier.py +1 -1
  40. kfinance/tool_calling/get_isin_from_ticker.py +1 -1
  41. kfinance/tool_calling/get_latest.py +2 -1
  42. kfinance/tool_calling/get_latest_earnings.py +1 -1
  43. kfinance/tool_calling/get_merger_info_from_transaction_id.py +6 -5
  44. kfinance/tool_calling/get_mergers_from_identifier.py +4 -1
  45. kfinance/tool_calling/get_n_quarters_ago.py +2 -1
  46. kfinance/tool_calling/get_next_earnings.py +1 -1
  47. kfinance/tool_calling/get_prices_from_identifier.py +4 -3
  48. kfinance/tool_calling/get_segments_from_identifier.py +3 -1
  49. kfinance/tool_calling/get_transcript.py +1 -1
  50. kfinance/tool_calling/resolve_identifier.py +1 -1
  51. kfinance/tool_calling/shared_models.py +1 -1
  52. kfinance/version.py +2 -2
  53. kensho_kfinance-2.7.0.dist-info/RECORD +0 -54
  54. {kensho_kfinance-2.7.0.dist-info → kensho_kfinance-2.9.0.dist-info}/WHEEL +0 -0
  55. {kensho_kfinance-2.7.0.dist-info → kensho_kfinance-2.9.0.dist-info}/licenses/AUTHORS.md +0 -0
  56. {kensho_kfinance-2.7.0.dist-info → kensho_kfinance-2.9.0.dist-info}/licenses/LICENSE +0 -0
  57. {kensho_kfinance-2.7.0.dist-info → kensho_kfinance-2.9.0.dist-info}/top_level.txt +0 -0
  58. /kfinance/{tests/scratch.py → models/__init__.py} +0 -0
@@ -0,0 +1,12 @@
1
+ from strenum import StrEnum
2
+
3
+
4
+ class IndustryClassification(StrEnum):
5
+ sic = "sic"
6
+ naics = "naics"
7
+ nace = "nace"
8
+ anzsic = "anzsic"
9
+ spcapiqetf = "spcapiqetf"
10
+ spratings = "spratings"
11
+ gics = "gics"
12
+ simple = "simple"
@@ -1,8 +1,5 @@
1
- from datetime import date
2
1
  from itertools import chain
3
- from typing import NamedTuple, TypedDict
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
@@ -0,0 +1,8 @@
1
+ from strenum import StrEnum
2
+
3
+
4
+ class SegmentType(StrEnum):
5
+ """The type of segment"""
6
+
7
+ business = "business"
8
+ geographic = "geographic"
@@ -0,0 +1,9 @@
1
+ from strenum import StrEnum
2
+
3
+
4
+ class StatementType(StrEnum):
5
+ """The type of financial statement"""
6
+
7
+ balance_sheet = "balance_sheet"
8
+ income_statement = "income_statement"
9
+ cashflow = "cashflow"
@@ -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
- {"date": "2024-01-01", "close": "100.000000"},
82
- {"date": "2024-01-02", "close": "101.000000"},
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
- {"date": "2024-01-01", "close": "200.000000"},
91
- {"date": "2024-01-02", "close": "201.000000"},
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
- trading_items = TradingItems(self.kfinance_api_client, [2, 3])
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
- result = trading_items.history()
99
- expected_dictionary_based_result = {
100
- 2: [
101
- {"date": "2024-01-01", "close": "100.000000"},
102
- {"date": "2024-01-02", "close": "101.000000"},
103
- ],
104
- 3: [
105
- {"date": "2024-01-01", "close": "200.000000"},
106
- {"date": "2024-01-02", "close": "201.000000"},
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
- self.assertEqual(len(result), len(expected_dictionary_based_result))
110
-
111
- for k, v in result.items():
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):
@@ -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)
@@ -118,6 +118,7 @@ def test_run_notebook(jupyter_kernel_name: str):
118
118
  )
119
119
 
120
120
  prices_resp = {
121
+ "currency": "USD",
121
122
  "prices": [
122
123
  {
123
124
  "date": "2024-05-20",
@@ -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
- self.kfinance_api_client.fetch_history(trading_item_id=trading_item_id)
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
- self.kfinance_api_client.fetch_history(
65
- trading_item_id=trading_item_id,
66
- is_adjusted=is_adjusted,
67
- start_date=start_date,
68
- end_date=end_date,
69
- periodicity=periodicity,
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
- client.fetch_market_caps_tevs_and_shares_outstanding(
314
- company_id=company_id, start_date=start_date, end_date=end_date
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