kensho-kfinance 1.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.

Potentially problematic release.


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

@@ -0,0 +1,367 @@
1
+ from datetime import datetime
2
+ from functools import cache, cached_property
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any, Callable, Optional
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ from .constants import LINE_ITEMS, BusinessRelationshipType
10
+ from .fetch import KFinanceApiClient
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from .kfinance import BusinessRelationships
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class CompanyFunctionsMetaClass:
21
+ def __init__(self) -> None:
22
+ """Init company functions"""
23
+ self.kfinance_api_client: KFinanceApiClient
24
+
25
+ @cached_property
26
+ def company_id(self) -> Any:
27
+ """Set and return the company id for the object"""
28
+ raise NotImplementedError("child classes must implement company id property")
29
+
30
+ def validate_inputs(
31
+ self,
32
+ period_type: Optional[str] = None,
33
+ start_year: Optional[int] = None,
34
+ end_year: Optional[int] = None,
35
+ start_quarter: Optional[int] = None,
36
+ end_quarter: Optional[int] = None,
37
+ ) -> None:
38
+ """Test the time inputs for validity."""
39
+
40
+ if period_type not in {"annual", "quarterly", "ytd", "ltm", None}:
41
+ raise RuntimeError(f"Period type {period_type} is not valid.")
42
+
43
+ if start_year and (start_year > datetime.now().year):
44
+ raise ValueError("start_year is in the future")
45
+
46
+ if end_year and not (1900 < end_year < 2100):
47
+ raise ValueError("end_year is not in range")
48
+
49
+ if start_quarter and not (1 <= start_quarter <= 4):
50
+ raise ValueError("start_qtr is out of range 1 to 4")
51
+
52
+ if end_quarter and not (1 <= end_quarter <= 4):
53
+ raise ValueError("end_qtr is out of range 1 to 4")
54
+
55
+ @cache
56
+ def statement(
57
+ self,
58
+ statement_type: str,
59
+ period_type: Optional[str] = None,
60
+ start_year: Optional[int] = None,
61
+ end_year: Optional[int] = None,
62
+ start_quarter: Optional[int] = None,
63
+ end_quarter: Optional[int] = None,
64
+ ) -> pd.DataFrame:
65
+ """Get the company's financial statement"""
66
+ try:
67
+ self.validate_inputs(
68
+ period_type=period_type,
69
+ start_year=start_year,
70
+ end_year=end_year,
71
+ start_quarter=start_quarter,
72
+ end_quarter=end_quarter,
73
+ )
74
+ except ValueError:
75
+ return pd.DataFrame()
76
+
77
+ return (
78
+ pd.DataFrame(
79
+ self.kfinance_api_client.fetch_statement(
80
+ company_id=self.company_id,
81
+ statement_type=statement_type,
82
+ period_type=period_type,
83
+ start_year=start_year,
84
+ end_year=end_year,
85
+ start_quarter=start_quarter,
86
+ end_quarter=end_quarter,
87
+ )["statements"]
88
+ )
89
+ .apply(pd.to_numeric)
90
+ .replace(np.nan, None)
91
+ )
92
+
93
+ def income_statement(
94
+ self,
95
+ period_type: Optional[str] = None,
96
+ start_year: Optional[int] = None,
97
+ end_year: Optional[int] = None,
98
+ start_quarter: Optional[int] = None,
99
+ end_quarter: Optional[int] = None,
100
+ ) -> pd.DataFrame:
101
+ """The templated income statement"""
102
+ return self.statement(
103
+ statement_type="income_statement",
104
+ period_type=period_type,
105
+ start_year=start_year,
106
+ end_year=end_year,
107
+ start_quarter=start_quarter,
108
+ end_quarter=end_quarter,
109
+ )
110
+
111
+ def income_stmt(
112
+ self,
113
+ period_type: Optional[str] = None,
114
+ start_year: Optional[int] = None,
115
+ end_year: Optional[int] = None,
116
+ start_quarter: Optional[int] = None,
117
+ end_quarter: Optional[int] = None,
118
+ ) -> pd.DataFrame:
119
+ """The templated income statement"""
120
+ return self.statement(
121
+ statement_type="income_statement",
122
+ period_type=period_type,
123
+ start_year=start_year,
124
+ end_year=end_year,
125
+ start_quarter=start_quarter,
126
+ end_quarter=end_quarter,
127
+ )
128
+
129
+ def balance_sheet(
130
+ self,
131
+ period_type: Optional[str] = None,
132
+ start_year: Optional[int] = None,
133
+ end_year: Optional[int] = None,
134
+ start_quarter: Optional[int] = None,
135
+ end_quarter: Optional[int] = None,
136
+ ) -> pd.DataFrame:
137
+ """The templated balance sheet"""
138
+ return self.statement(
139
+ statement_type="balance_sheet",
140
+ period_type=period_type,
141
+ start_year=start_year,
142
+ end_year=end_year,
143
+ start_quarter=start_quarter,
144
+ end_quarter=end_quarter,
145
+ )
146
+
147
+ def cash_flow(
148
+ self,
149
+ period_type: Optional[str] = None,
150
+ start_year: Optional[int] = None,
151
+ end_year: Optional[int] = None,
152
+ start_quarter: Optional[int] = None,
153
+ end_quarter: Optional[int] = None,
154
+ ) -> pd.DataFrame:
155
+ """The templated cash flow statement"""
156
+ return self.statement(
157
+ statement_type="cash_flow",
158
+ period_type=period_type,
159
+ start_year=start_year,
160
+ end_year=end_year,
161
+ start_quarter=start_quarter,
162
+ end_quarter=end_quarter,
163
+ )
164
+
165
+ def cashflow(
166
+ self,
167
+ period_type: Optional[str] = None,
168
+ start_year: Optional[int] = None,
169
+ end_year: Optional[int] = None,
170
+ start_quarter: Optional[int] = None,
171
+ end_quarter: Optional[int] = None,
172
+ ) -> pd.DataFrame:
173
+ """The templated cash flow statement"""
174
+ return self.statement(
175
+ statement_type="cash_flow",
176
+ period_type=period_type,
177
+ start_year=start_year,
178
+ end_year=end_year,
179
+ start_quarter=start_quarter,
180
+ end_quarter=end_quarter,
181
+ )
182
+
183
+ @cache
184
+ def line_item(
185
+ self,
186
+ line_item: str,
187
+ period_type: Optional[str] = None,
188
+ start_year: Optional[int] = None,
189
+ end_year: Optional[int] = None,
190
+ start_quarter: Optional[int] = None,
191
+ end_quarter: Optional[int] = None,
192
+ ) -> pd.DataFrame:
193
+ """Get a DataFrame of a financial line item according to the date ranges."""
194
+ try:
195
+ self.validate_inputs(
196
+ period_type=period_type,
197
+ start_year=start_year,
198
+ end_year=end_year,
199
+ start_quarter=start_quarter,
200
+ end_quarter=end_quarter,
201
+ )
202
+ except ValueError:
203
+ return pd.DataFrame()
204
+
205
+ return (
206
+ pd.DataFrame(
207
+ self.kfinance_api_client.fetch_line_item(
208
+ company_id=self.company_id,
209
+ line_item=line_item,
210
+ period_type=period_type,
211
+ start_year=start_year,
212
+ end_year=end_year,
213
+ start_quarter=start_quarter,
214
+ end_quarter=end_quarter,
215
+ )
216
+ )
217
+ .transpose()
218
+ .apply(pd.to_numeric)
219
+ .replace(np.nan, None)
220
+ .set_index(pd.Index([line_item]))
221
+ )
222
+
223
+ def relationships(self, relationship_type: BusinessRelationshipType) -> "BusinessRelationships":
224
+ """Returns a BusinessRelationships object that includes the current and previous Companies associated with company_id and filtered by relationship_type. The function calls fetch_companies_from_business_relationship.
225
+
226
+ :param relationship_type: The type of relationship to filter by. Valid relationship types are defined in the BusinessRelationshipType class.
227
+ :type relationship_type: BusinessRelationshipType
228
+ :return: A BusinessRelationships object containing a tuple of Companies objects that lists current and previous company IDs that have the specified relationship with the given company_id.
229
+ :rtype: BusinessRelationships
230
+ """
231
+ from .kfinance import BusinessRelationships, Companies
232
+
233
+ companies = self.kfinance_api_client.fetch_companies_from_business_relationship(
234
+ self.company_id,
235
+ relationship_type,
236
+ )
237
+ return BusinessRelationships(
238
+ Companies(self.kfinance_api_client, companies["current"]),
239
+ Companies(self.kfinance_api_client, companies["previous"]),
240
+ )
241
+
242
+
243
+ for line_item in LINE_ITEMS:
244
+ line_item_name = line_item["name"]
245
+
246
+ def _line_item_outer_wrapper(line_item_name: str, alias_for: Optional[str] = None) -> Callable:
247
+ def line_item_inner_wrapper(
248
+ self: Any,
249
+ period_type: Optional[str] = None,
250
+ start_year: Optional[int] = None,
251
+ end_year: Optional[int] = None,
252
+ start_quarter: Optional[int] = None,
253
+ end_quarter: Optional[int] = None,
254
+ ) -> pd.DataFrame:
255
+ return self.line_item(
256
+ line_item=line_item_name,
257
+ period_type=period_type,
258
+ start_year=start_year,
259
+ end_year=end_year,
260
+ start_quarter=start_quarter,
261
+ end_quarter=end_quarter,
262
+ )
263
+
264
+ doc = "ciq data item " + str(line_item["dataitemid"])
265
+ TAB = " "
266
+ if alias_for is not None:
267
+ doc = f"alias for {alias_for}\n\n{TAB}{TAB}" + doc
268
+ line_item_inner_wrapper.__doc__ = doc
269
+ line_item_inner_wrapper.__name__ = line_item_name
270
+ return line_item_inner_wrapper
271
+
272
+ setattr(
273
+ CompanyFunctionsMetaClass,
274
+ line_item_name,
275
+ _line_item_outer_wrapper(line_item_name),
276
+ )
277
+
278
+ for alias in line_item["aliases"]:
279
+ setattr(
280
+ CompanyFunctionsMetaClass,
281
+ alias,
282
+ _line_item_outer_wrapper(alias, line_item_name),
283
+ )
284
+
285
+
286
+ class DelegatedCompanyFunctionsMetaClass(CompanyFunctionsMetaClass):
287
+ """all methods in CompanyFunctionsMetaClass delegated to company attribute"""
288
+
289
+ def __init__(self) -> None:
290
+ """delegate CompanyFunctionsMetaClass methods to company attribute"""
291
+ super().__init__()
292
+ company_function_names = [
293
+ company_function
294
+ for company_function in dir(CompanyFunctionsMetaClass)
295
+ if not company_function.startswith("__")
296
+ and callable(getattr(CompanyFunctionsMetaClass, company_function))
297
+ ]
298
+ for company_function_name in company_function_names:
299
+
300
+ def delegated_function(company_function_name: str) -> Callable:
301
+ # wrapper is necessary so that self.company is lazy loaded
302
+ def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
303
+ fn = getattr(self.company, company_function_name)
304
+ response = fn(*args, **kwargs)
305
+ return response
306
+
307
+ company_function = getattr(
308
+ DelegatedCompanyFunctionsMetaClass, company_function_name
309
+ )
310
+ wrapper.__doc__ = company_function.__doc__
311
+ wrapper.__name__ = company_function.__name__
312
+ return wrapper
313
+
314
+ setattr(
315
+ DelegatedCompanyFunctionsMetaClass,
316
+ company_function_name,
317
+ delegated_function(company_function_name),
318
+ )
319
+
320
+ @cached_property
321
+ def company(self) -> Any:
322
+ """Set and return the company for the object"""
323
+ raise NotImplementedError("child classes must implement company property")
324
+
325
+
326
+ for relationship in BusinessRelationshipType:
327
+
328
+ def _relationship_outer_wrapper(relationship_type: BusinessRelationshipType) -> cached_property:
329
+ """Creates a cached property for a relationship type.
330
+
331
+ This function returns a property that retrieves the associated company's current and previous
332
+ relationships of the specified type.
333
+
334
+ Args:
335
+ relationship_type (BusinessRelationshipType): The type of relationship to be wrapped.
336
+
337
+ Returns:
338
+ property: A cached property that calls the inner wrapper to retrieve the relationship data.
339
+ """
340
+
341
+ def relationship_inner_wrapper(
342
+ self: Any,
343
+ ) -> "BusinessRelationships":
344
+ """Inner wrapper function for the relationship type.
345
+
346
+ This function retrieves the associated company's current and previous relationships
347
+ of the specified type.
348
+
349
+ Returns:
350
+ BusinessRelationships: A BusinessRelationships object containing the current and previous companies
351
+ associated with the relationship type.
352
+ """
353
+ return self.relationships(relationship_type)
354
+
355
+ doc = f"Returns the associated company's current and previous {relationship_type}s"
356
+ relationship_inner_wrapper.__doc__ = doc
357
+ relationship_inner_wrapper.__name__ = relationship
358
+
359
+ return cached_property(relationship_inner_wrapper)
360
+
361
+ relationship_cached_property = _relationship_outer_wrapper(relationship)
362
+ relationship_cached_property.__set_name__(CompanyFunctionsMetaClass, relationship)
363
+ setattr(
364
+ CompanyFunctionsMetaClass,
365
+ relationship,
366
+ relationship_cached_property,
367
+ )