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 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
@@ -0,0 +1,8 @@
1
+ """Run the fisis CLI, so ``python -m fisis`` matches the ``fisis`` console script.
2
+
3
+ Importing this module runs the CLI and terminates the process via ``SystemExit``.
4
+ """
5
+
6
+ from .cli import main
7
+
8
+ raise SystemExit(main())
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
+ )