fisis 0.1.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.
- fisis/__init__.py +56 -0
- fisis/__main__.py +8 -0
- fisis/_accessor.py +228 -0
- fisis/_companies.py +567 -0
- fisis/_config.py +63 -0
- fisis/_parse.py +146 -0
- fisis/_transport.py +170 -0
- fisis/cli.py +294 -0
- fisis/client.py +313 -0
- fisis/exceptions.py +78 -0
- fisis/py.typed +0 -0
- fisis/types.py +224 -0
- fisis-0.1.0.dist-info/METADATA +364 -0
- fisis-0.1.0.dist-info/RECORD +17 -0
- fisis-0.1.0.dist-info/WHEEL +4 -0
- fisis-0.1.0.dist-info/entry_points.txt +2 -0
- fisis-0.1.0.dist-info/licenses/LICENSE +21 -0
fisis/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""fisis -- a Python client for the FISIS Open API (금융감독원 금융통계정보시스템)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
from ._accessor import CompanyView, SectorView
|
|
8
|
+
from .client import FISIS
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
FISISAuthError,
|
|
11
|
+
FISISConfigError,
|
|
12
|
+
FISISError,
|
|
13
|
+
FISISNetworkError,
|
|
14
|
+
FISISRateLimitError,
|
|
15
|
+
FISISResponseError,
|
|
16
|
+
)
|
|
17
|
+
from .types import (
|
|
18
|
+
AccountRow,
|
|
19
|
+
Category,
|
|
20
|
+
Column,
|
|
21
|
+
CompanyRow,
|
|
22
|
+
Data,
|
|
23
|
+
DataRow,
|
|
24
|
+
Lang,
|
|
25
|
+
Sector,
|
|
26
|
+
StatisticsRow,
|
|
27
|
+
Term,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
__version__ = version("fisis")
|
|
32
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
33
|
+
__version__ = "0.0.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"FISIS",
|
|
37
|
+
"SectorView",
|
|
38
|
+
"CompanyView",
|
|
39
|
+
"Sector",
|
|
40
|
+
"Category",
|
|
41
|
+
"Term",
|
|
42
|
+
"Lang",
|
|
43
|
+
"CompanyRow",
|
|
44
|
+
"StatisticsRow",
|
|
45
|
+
"AccountRow",
|
|
46
|
+
"DataRow",
|
|
47
|
+
"Column",
|
|
48
|
+
"Data",
|
|
49
|
+
"FISISError",
|
|
50
|
+
"FISISConfigError",
|
|
51
|
+
"FISISAuthError",
|
|
52
|
+
"FISISRateLimitError",
|
|
53
|
+
"FISISResponseError",
|
|
54
|
+
"FISISNetworkError",
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
fisis/__main__.py
ADDED
fisis/_accessor.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Fluent, IDE-navigable views over the FISIS client's flat operations.
|
|
2
|
+
|
|
3
|
+
The :class:`FISIS` client exposes four flat primitives keyed by codes
|
|
4
|
+
(``list_companies(sector=...)``, ``fetch_data(finance_cd=..., ...)``). These two
|
|
5
|
+
views bind those codes so a caller can navigate instead of repeating them:
|
|
6
|
+
``f.life.company("0010001").fetch(list_no=..., ...)`` reads left to right --
|
|
7
|
+
sector, then company, then the pull -- and every step is a plain attribute or
|
|
8
|
+
method an editor can autocomplete.
|
|
9
|
+
|
|
10
|
+
Both views are thin: they hold identifying state (a sector, a company code) and
|
|
11
|
+
delegate every request to the client's primitives, so there is no second copy of
|
|
12
|
+
the HTTP or parsing logic here. The client is taken as a duck-typed
|
|
13
|
+
:class:`_ClientProtocol` to keep this module free of an import cycle with
|
|
14
|
+
``client.py``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Generic, Protocol, Self, TypeVar
|
|
20
|
+
|
|
21
|
+
from .types import (
|
|
22
|
+
AccountRow,
|
|
23
|
+
Category,
|
|
24
|
+
CompanyRow,
|
|
25
|
+
Data,
|
|
26
|
+
Lang,
|
|
27
|
+
Sector,
|
|
28
|
+
StatisticsRow,
|
|
29
|
+
Term,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# The company-view type a sector yields: the base :class:`CompanyView`, or a
|
|
33
|
+
# sector-specific subclass (see ``_companies``) carrying named-statistic methods.
|
|
34
|
+
_C = TypeVar("_C", bound="CompanyView")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _ClientProtocol(Protocol):
|
|
38
|
+
"""The client surface the views delegate to -- the four flat primitives.
|
|
39
|
+
|
|
40
|
+
Declared structurally so :class:`SectorView` / :class:`CompanyView` depend on
|
|
41
|
+
the *shape* of :class:`FISIS`, not the class itself, which would import-cycle
|
|
42
|
+
(``client`` imports this module to build its per-sector attributes).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def list_companies(
|
|
46
|
+
self, *, sector: Sector | str, finance_cd: str | None = ...,
|
|
47
|
+
lang: Lang | str = ...,
|
|
48
|
+
) -> list[CompanyRow]: ...
|
|
49
|
+
|
|
50
|
+
def list_statistics(
|
|
51
|
+
self, *, sector: Sector | str, category: Category | str | None = ...,
|
|
52
|
+
lang: Lang | str = ...,
|
|
53
|
+
) -> list[StatisticsRow]: ...
|
|
54
|
+
|
|
55
|
+
def list_accounts(
|
|
56
|
+
self, *, list_no: str, lang: Lang | str = ...,
|
|
57
|
+
) -> list[AccountRow]: ...
|
|
58
|
+
|
|
59
|
+
def fetch_data(
|
|
60
|
+
self, *, finance_cd: str, list_no: str, term: Term | str,
|
|
61
|
+
start_month: str, end_month: str, account_cd: str | None = ...,
|
|
62
|
+
lang: Lang | str = ...,
|
|
63
|
+
) -> Data: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SectorView(Generic[_C]):
|
|
67
|
+
"""One financial sector, bound to a client -- the ``f.<sector>`` handle.
|
|
68
|
+
|
|
69
|
+
Delegates :meth:`companies` / :meth:`statistics` to the client's flat
|
|
70
|
+
primitives with the sector already filled in, and :meth:`company` resolves a
|
|
71
|
+
company (by code or name) into a :class:`CompanyView` for the next step.
|
|
72
|
+
|
|
73
|
+
Generic over the company-view type it yields: a plain sector produces the
|
|
74
|
+
base :class:`CompanyView`, while a sector with headline named-statistic
|
|
75
|
+
methods (banks, insurers, ...) produces its subclass, so an editor
|
|
76
|
+
autocompletes those methods off ``sector.company(...)``.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
client: _ClientProtocol,
|
|
82
|
+
sector: Sector,
|
|
83
|
+
company_cls: type[_C],
|
|
84
|
+
) -> None:
|
|
85
|
+
self._client = client
|
|
86
|
+
self._sector = sector
|
|
87
|
+
self._company_cls = company_cls
|
|
88
|
+
|
|
89
|
+
def companies(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
finance_cd: str | None = None,
|
|
93
|
+
lang: Lang | str = Lang.KO,
|
|
94
|
+
) -> list[CompanyRow]:
|
|
95
|
+
"""List this sector's companies (delegates to ``list_companies``)."""
|
|
96
|
+
return self._client.list_companies(
|
|
97
|
+
sector=self._sector, finance_cd=finance_cd, lang=lang)
|
|
98
|
+
|
|
99
|
+
def statistics(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
category: Category | str | None = None,
|
|
103
|
+
lang: Lang | str = Lang.KO,
|
|
104
|
+
) -> list[StatisticsRow]:
|
|
105
|
+
"""Browse this sector's statistics catalog (via ``list_statistics``)."""
|
|
106
|
+
return self._client.list_statistics(
|
|
107
|
+
sector=self._sector, category=category, lang=lang)
|
|
108
|
+
|
|
109
|
+
def accounts(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
list_no: str,
|
|
113
|
+
lang: Lang | str = Lang.KO,
|
|
114
|
+
) -> list[AccountRow]:
|
|
115
|
+
"""List a statistic's account items (delegates to ``list_accounts``).
|
|
116
|
+
|
|
117
|
+
The account items belong to the statistic (``list_no``), not to any one
|
|
118
|
+
company, so they are listed at the sector level; pick a company only to
|
|
119
|
+
:meth:`CompanyView.fetch` the actual observations.
|
|
120
|
+
"""
|
|
121
|
+
return self._client.list_accounts(list_no=list_no, lang=lang)
|
|
122
|
+
|
|
123
|
+
def company(self, key: str, *, lang: Lang | str = Lang.KO) -> _C:
|
|
124
|
+
"""Resolve ``key`` to this sector's company-view type for the given company.
|
|
125
|
+
|
|
126
|
+
An all-digit ``key`` is taken as the ``finance_cd`` directly, with no
|
|
127
|
+
lookup. Otherwise the sector's companies are fetched and ``key`` is
|
|
128
|
+
matched against ``finance_nm`` -- an exact match wins; failing that, a
|
|
129
|
+
substring match wins only if it is unique. Raises ``ValueError`` if
|
|
130
|
+
nothing matches, or if a substring matches more than one company (the
|
|
131
|
+
message names the candidates so the caller can disambiguate). The result
|
|
132
|
+
is the sector's specific :class:`CompanyView` subclass where one exists,
|
|
133
|
+
so its named-statistic methods are reachable.
|
|
134
|
+
"""
|
|
135
|
+
if not key:
|
|
136
|
+
raise ValueError("company key must be a non-empty finance_cd or name")
|
|
137
|
+
if key.isascii() and key.isdigit():
|
|
138
|
+
return self._company_cls(self._client, finance_cd=key, finance_nm=None)
|
|
139
|
+
companies = self.companies(lang=lang)
|
|
140
|
+
return self._company_cls._from_name_match(self._client, key, companies)
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str:
|
|
143
|
+
return f"SectorView({self._sector.name.lower()})"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class CompanyView:
|
|
147
|
+
"""One company, bound to a client -- the ``f.<sector>.company(...)`` handle.
|
|
148
|
+
|
|
149
|
+
Carries the resolved ``finance_cd`` (and ``finance_nm`` when a name lookup
|
|
150
|
+
supplied it) so :meth:`fetch` needs only the statistic (``list_no``).
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self, client: _ClientProtocol, *, finance_cd: str, finance_nm: str | None,
|
|
155
|
+
) -> None:
|
|
156
|
+
self._client = client
|
|
157
|
+
self._finance_cd = finance_cd
|
|
158
|
+
self._finance_nm = finance_nm
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def _from_name_match(
|
|
162
|
+
cls, client: _ClientProtocol, name: str, companies: list[CompanyRow],
|
|
163
|
+
) -> Self:
|
|
164
|
+
"""Build a view by matching ``name`` against ``companies`` by ``finance_nm``.
|
|
165
|
+
|
|
166
|
+
Exact ``finance_nm`` match first; else a unique substring match. Raises
|
|
167
|
+
``ValueError`` on no match or an ambiguous (multiple) substring match.
|
|
168
|
+
Returns an instance of the calling class, so a subclass stays itself.
|
|
169
|
+
"""
|
|
170
|
+
exact = [row for row in companies if row.get("finance_nm") == name]
|
|
171
|
+
if len(exact) == 1:
|
|
172
|
+
return cls._from_row(client, exact[0])
|
|
173
|
+
|
|
174
|
+
substring = [
|
|
175
|
+
row for row in companies if name in (row.get("finance_nm") or "")]
|
|
176
|
+
if len(substring) == 1:
|
|
177
|
+
return cls._from_row(client, substring[0])
|
|
178
|
+
if len(substring) > 1:
|
|
179
|
+
candidates = ", ".join(
|
|
180
|
+
f"{row.get('finance_nm')} ({row.get('finance_cd')})"
|
|
181
|
+
for row in substring)
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"{name!r} matches more than one company: {candidates}")
|
|
184
|
+
raise ValueError(f"no company matching {name!r} in this sector")
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def _from_row(cls, client: _ClientProtocol, row: CompanyRow) -> Self:
|
|
188
|
+
finance_cd = row.get("finance_cd")
|
|
189
|
+
if not finance_cd:
|
|
190
|
+
raise ValueError(f"matched company has no finance_cd: {row!r}")
|
|
191
|
+
return cls(client, finance_cd=finance_cd, finance_nm=row.get("finance_nm"))
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def finance_cd(self) -> str:
|
|
195
|
+
"""The resolved company code -- what :meth:`fetch` identifies the company by."""
|
|
196
|
+
return self._finance_cd
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def finance_nm(self) -> str | None:
|
|
200
|
+
"""The company name, when a name lookup supplied it; ``None`` for a raw code."""
|
|
201
|
+
return self._finance_nm
|
|
202
|
+
|
|
203
|
+
def fetch(
|
|
204
|
+
self,
|
|
205
|
+
*,
|
|
206
|
+
list_no: str,
|
|
207
|
+
term: Term | str,
|
|
208
|
+
start_month: str,
|
|
209
|
+
end_month: str,
|
|
210
|
+
account_cd: str | None = None,
|
|
211
|
+
lang: Lang | str = Lang.KO,
|
|
212
|
+
) -> Data:
|
|
213
|
+
"""Fetch this company's observations (delegates to ``fetch_data``).
|
|
214
|
+
|
|
215
|
+
Returns a :class:`Data` -- ``data.rows`` for the values, ``data.columns``
|
|
216
|
+
for each value column's unit, ``data.date_of_settlement`` for the fiscal
|
|
217
|
+
date. See :meth:`FISIS.fetch_data`.
|
|
218
|
+
"""
|
|
219
|
+
return self._client.fetch_data(
|
|
220
|
+
finance_cd=self._finance_cd, list_no=list_no, term=term,
|
|
221
|
+
start_month=start_month, end_month=end_month, account_cd=account_cd,
|
|
222
|
+
lang=lang)
|
|
223
|
+
|
|
224
|
+
def __repr__(self) -> str:
|
|
225
|
+
return (
|
|
226
|
+
f"CompanyView(finance_cd={self._finance_cd!r}, "
|
|
227
|
+
f"name={self._finance_nm!r})"
|
|
228
|
+
)
|