kabukit 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (41) hide show
  1. kabukit-0.2.0/LICENSE +21 -0
  2. kabukit-0.2.0/PKG-INFO +64 -0
  3. kabukit-0.2.0/README.md +18 -0
  4. kabukit-0.2.0/pyproject.toml +102 -0
  5. kabukit-0.2.0/src/kabukit/__init__.py +7 -0
  6. kabukit-0.2.0/src/kabukit/analysis/indicators.py +0 -0
  7. kabukit-0.2.0/src/kabukit/analysis/preprocess.py +0 -0
  8. kabukit-0.2.0/src/kabukit/analysis/screener.py +0 -0
  9. kabukit-0.2.0/src/kabukit/analysis/visualization.py +57 -0
  10. kabukit-0.2.0/src/kabukit/cli/__init__.py +0 -0
  11. kabukit-0.2.0/src/kabukit/cli/app.py +22 -0
  12. kabukit-0.2.0/src/kabukit/cli/auth.py +86 -0
  13. kabukit-0.2.0/src/kabukit/core/__init__.py +0 -0
  14. kabukit-0.2.0/src/kabukit/core/base.py +45 -0
  15. kabukit-0.2.0/src/kabukit/core/client.py +25 -0
  16. kabukit-0.2.0/src/kabukit/core/info.py +12 -0
  17. kabukit-0.2.0/src/kabukit/core/prices.py +30 -0
  18. kabukit-0.2.0/src/kabukit/core/statements.py +7 -0
  19. kabukit-0.2.0/src/kabukit/edinet/__init__.py +3 -0
  20. kabukit-0.2.0/src/kabukit/edinet/client.py +113 -0
  21. kabukit-0.2.0/src/kabukit/edinet/concurrent.py +153 -0
  22. kabukit-0.2.0/src/kabukit/edinet/doc.py +32 -0
  23. kabukit-0.2.0/src/kabukit/jquants/__init__.py +3 -0
  24. kabukit-0.2.0/src/kabukit/jquants/client.py +324 -0
  25. kabukit-0.2.0/src/kabukit/jquants/concurrent.py +91 -0
  26. kabukit-0.2.0/src/kabukit/jquants/info.py +31 -0
  27. kabukit-0.2.0/src/kabukit/jquants/prices.py +29 -0
  28. kabukit-0.2.0/src/kabukit/jquants/schema.py +180 -0
  29. kabukit-0.2.0/src/kabukit/jquants/statements.py +102 -0
  30. kabukit-0.2.0/src/kabukit/py.typed +0 -0
  31. kabukit-0.2.0/src/kabukit/utils/__init__.py +0 -0
  32. kabukit-0.2.0/src/kabukit/utils/concurrent.py +148 -0
  33. kabukit-0.2.0/src/kabukit/utils/config.py +26 -0
  34. kabukit-0.2.0/src/kabukit/utils/params.py +47 -0
  35. kabukit-0.1.0/PKG-INFO +0 -33
  36. kabukit-0.1.0/README.md +0 -19
  37. kabukit-0.1.0/pyproject.toml +0 -77
  38. kabukit-0.1.0/src/kabukit/__init__.py +0 -5
  39. kabukit-0.1.0/src/kabukit/cli.py +0 -40
  40. kabukit-0.1.0/src/kabukit/jquants/client.py +0 -324
  41. {kabukit-0.1.0/src/kabukit/jquants → kabukit-0.2.0/src/kabukit/analysis}/__init__.py +0 -0
kabukit-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daizu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
kabukit-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.3
2
+ Name: kabukit
3
+ Version: 0.2.0
4
+ Summary: A Python toolkit for Japanese financial market data, supporting J-Quants and EDINET APIs.
5
+ Author: daizutabi
6
+ Author-email: daizutabi <daizutabi@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 Daizu
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Classifier: Development Status :: 4 - Beta
29
+ Classifier: Programming Language :: Python
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Requires-Dist: altair>=5
33
+ Requires-Dist: async-typer>=0.1
34
+ Requires-Dist: holidays>=0.81
35
+ Requires-Dist: httpx>=0.28.1
36
+ Requires-Dist: marimo[lsp]>=0.16
37
+ Requires-Dist: platformdirs>=4
38
+ Requires-Dist: polars>=1
39
+ Requires-Dist: python-dotenv>=1
40
+ Requires-Dist: typer>=0.19
41
+ Requires-Dist: vegafusion-python-embed>=1.6
42
+ Requires-Dist: vegafusion>=2
43
+ Requires-Dist: vl-convert-python>=1.8
44
+ Requires-Python: >=3.12
45
+ Description-Content-Type: text/markdown
46
+
47
+ # kabukit
48
+
49
+ A Python toolkit for Japanese financial market data, supporting J-Quants and EDINET APIs.
50
+
51
+ [![PyPI Version][pypi-v-image]][pypi-v-link]
52
+ [![Python Version][python-v-image]][python-v-link]
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install kabukit
58
+ ```
59
+
60
+ <!-- Badges -->
61
+ [pypi-v-image]: https://img.shields.io/pypi/v/kabukit.svg
62
+ [pypi-v-link]: https://pypi.org/project/kabukit/
63
+ [python-v-image]: https://img.shields.io/pypi/pyversions/kabukit.svg
64
+ [python-v-link]: https://pypi.org/project/kabukit
@@ -0,0 +1,18 @@
1
+ # kabukit
2
+
3
+ A Python toolkit for Japanese financial market data, supporting J-Quants and EDINET APIs.
4
+
5
+ [![PyPI Version][pypi-v-image]][pypi-v-link]
6
+ [![Python Version][python-v-image]][python-v-link]
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install kabukit
12
+ ```
13
+
14
+ <!-- Badges -->
15
+ [pypi-v-image]: https://img.shields.io/pypi/v/kabukit.svg
16
+ [pypi-v-link]: https://pypi.org/project/kabukit/
17
+ [python-v-image]: https://img.shields.io/pypi/pyversions/kabukit.svg
18
+ [python-v-link]: https://pypi.org/project/kabukit
@@ -0,0 +1,102 @@
1
+ [build-system]
2
+ requires = ["uv_build"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "kabukit"
7
+ version = "0.2.0"
8
+ description = "A Python toolkit for Japanese financial market data, supporting J-Quants and EDINET APIs."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [{ name = "daizutabi", email = "daizutabi@gmail.com" }]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ ]
18
+ requires-python = ">=3.12"
19
+ dependencies = [
20
+ "altair>=5",
21
+ "async-typer>=0.1",
22
+ "holidays>=0.81",
23
+ "httpx>=0.28.1",
24
+ "marimo[lsp]>=0.16",
25
+ "platformdirs>=4",
26
+ "polars>=1",
27
+ "python-dotenv>=1",
28
+ "typer>=0.19",
29
+ "vegafusion-python-embed>=1.6",
30
+ "vegafusion>=2",
31
+ "vl-convert-python>=1.8",
32
+ ]
33
+
34
+ [project.scripts]
35
+ kabu = "kabukit.cli.app:app"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "basedpyright>=1.31.4",
40
+ "mkdocs-marimo>=0.2.1",
41
+ "mkdocs-material>=9.6.20",
42
+ "mkdocs>=1.6.1",
43
+ "numpy>=2.3.3", # polars 1.33 type hinting workaround,
44
+ "pytest-asyncio>=1.2.0",
45
+ "pytest-clarity>=1.0.1",
46
+ "pytest-cov>=7.0.0",
47
+ "pytest-randomly>=4.0.1",
48
+ "pytest-xdist>=3.8.0",
49
+ "rich>=14.1.0",
50
+ "tqdm>=4.67.1",
51
+ ]
52
+ docs = ["mkapi>=4.4", "mkdocs-material"]
53
+
54
+ [tool.pytest.ini_options]
55
+ addopts = ["--cov=kabukit", "--cov-report=lcov:lcov.info", "--doctest-modules"]
56
+ testpaths = ["src", "tests"]
57
+
58
+ [tool.coverage.report]
59
+ exclude_lines = ["no cov", "raise NotImplementedError", "if TYPE_CHECKING:"]
60
+ skip_covered = true
61
+
62
+ [tool.ruff]
63
+ line-length = 88
64
+ target-version = "py312"
65
+ include = ["src", "tests"]
66
+
67
+ [tool.ruff.lint]
68
+ select = ["ALL"]
69
+ unfixable = ["F401"]
70
+ ignore = [
71
+ "A002",
72
+ "ANN401",
73
+ "D",
74
+ "FBT001",
75
+ "N802",
76
+ "PGH003",
77
+ "PLC0415",
78
+ "PLR0913",
79
+ "PLR2004",
80
+ "RUF002",
81
+ "S603",
82
+ "SIM102",
83
+ "TRY003",
84
+ ]
85
+
86
+ [tool.ruff.lint.per-file-ignores]
87
+ "tests/*" = ["ANN", "FBT", "S101", "S607"]
88
+ "schema.py" = ["E501"]
89
+ "notebooks/*" = ["F704", "PLE1142"]
90
+
91
+ [tool.ruff.format]
92
+ exclude = ["schema.py"]
93
+
94
+ [tool.basedpyright]
95
+ include = ["src", "tests"]
96
+ reportAny = false
97
+ reportExplicitAny = false
98
+ reportImplicitOverride = false
99
+ reportImportCycles = false
100
+ reportIncompatibleVariableOverride = false
101
+ reportUnusedCallResult = false
102
+ reportUnusedImport = false
@@ -0,0 +1,7 @@
1
+ from .core.info import Info
2
+ from .core.prices import Prices
3
+ from .core.statements import Statements
4
+ from .edinet.client import EdinetClient
5
+ from .jquants.client import JQuantsClient
6
+
7
+ __all__ = ["EdinetClient", "Info", "JQuantsClient", "Prices", "Statements"]
File without changes
File without changes
File without changes
@@ -0,0 +1,57 @@
1
+ """チャート作成のためのモジュール"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ import altair as alt
8
+
9
+ if TYPE_CHECKING:
10
+ from kabukit.core.prices import Prices
11
+
12
+
13
+ def plot_prices(
14
+ prices: Prices,
15
+ kind: Literal["candlestick"] = "candlestick",
16
+ ) -> alt.VConcatChart:
17
+ if kind == "candlestick":
18
+ chart_price = plot_prices_candlestick(prices)
19
+ chart_price_volume = plot_prices_volume(prices)
20
+ return alt.vconcat(chart_price, chart_price_volume)
21
+
22
+ raise NotImplementedError # pyright: ignore[reportUnreachable]
23
+
24
+
25
+ def plot_prices_candlestick(prices: Prices) -> alt.LayerChart:
26
+ rule = alt.Chart(prices.data, mark="rule").encode(y="Low:Q", y2="High:Q")
27
+ bar = alt.Chart(prices.data, mark="bar").encode(y="Open:Q", y2="Close:Q")
28
+
29
+ color_condition = alt.condition(
30
+ "datum.Open < datum.Close",
31
+ alt.value("#ff3030"),
32
+ alt.value("#3030ff"),
33
+ )
34
+
35
+ return alt.layer(rule, bar, height=200).encode(
36
+ x=alt.X("Date:T", axis=alt.Axis(title="日付", format="%Y-%m-%d")),
37
+ y=alt.Y(title="株価", scale=alt.Scale(zero=False)),
38
+ color=color_condition,
39
+ tooltip=[
40
+ alt.Tooltip("Date:T", title="日付"),
41
+ alt.Tooltip("Open:Q", title="始値"),
42
+ alt.Tooltip("High:Q", title="高値"),
43
+ alt.Tooltip("Low:Q", title="安値"),
44
+ alt.Tooltip("Close:Q", title="終値"),
45
+ ],
46
+ )
47
+
48
+
49
+ def plot_prices_volume(prices: Prices) -> alt.Chart:
50
+ return alt.Chart(prices.data, mark="bar", height=50).encode(
51
+ x=alt.X("Date:T", axis=alt.Axis(title="日付", format="%Y-%m-%d")),
52
+ y=alt.Y("Volume:Q", title="出来高"),
53
+ tooltip=[
54
+ alt.Tooltip("Date:T", title="日付"),
55
+ alt.Tooltip("Volume:Q", title="出来高"),
56
+ ],
57
+ )
File without changes
@@ -0,0 +1,22 @@
1
+ """kabukit CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from async_typer import AsyncTyper # pyright: ignore[reportMissingTypeStubs]
7
+
8
+ from . import auth
9
+
10
+ app = AsyncTyper(
11
+ add_completion=False,
12
+ help="J-Quants/EDINETデータツール",
13
+ )
14
+ app.add_typer(auth.app, name="auth")
15
+
16
+
17
+ @app.command()
18
+ def version() -> None:
19
+ """バージョン情報を表示します。"""
20
+ from importlib.metadata import version
21
+
22
+ typer.echo(f"kabukit version: {version('kabukit')}")
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from async_typer import AsyncTyper # pyright: ignore[reportMissingTypeStubs]
7
+ from httpx import HTTPStatusError
8
+ from typer import Exit, Option
9
+
10
+ app = AsyncTyper(
11
+ add_completion=False,
12
+ help="J-QuantsまたはEDINETの認証トークンを保存します。",
13
+ )
14
+
15
+
16
+ async def auth_jquants(mailaddress: str, password: str) -> None:
17
+ """J-Quants APIの認証を行い、トークンを設定ファイルに保存します。"""
18
+ from kabukit.jquants.client import JQuantsClient
19
+
20
+ async with JQuantsClient() as client:
21
+ try:
22
+ await client.auth(mailaddress, password, save=True)
23
+ except HTTPStatusError as e:
24
+ typer.echo(f"認証に失敗しました: {e}")
25
+ raise Exit(1) from None
26
+
27
+ typer.echo("J-Quantsのリフレッシュトークン・IDトークンを保存しました。")
28
+
29
+
30
+ Mailaddress = Annotated[
31
+ str,
32
+ Option(prompt=True, help="J-Quantsに登録したメールアドレス。"),
33
+ ]
34
+ Password = Annotated[
35
+ str,
36
+ Option(prompt=True, hide_input=True, help="J-Quantsのパスワード。"),
37
+ ]
38
+
39
+
40
+ @app.async_command() # pyright: ignore[reportUnknownMemberType]
41
+ async def jquants(mailaddress: Mailaddress, password: Password) -> None:
42
+ """J-Quants APIの認証を行い、トークンを設定ファイルに保存します。(エイリアス: j)"""
43
+ await auth_jquants(mailaddress, password)
44
+
45
+
46
+ @app.async_command(name="j", hidden=True) # pyright: ignore[reportUnknownMemberType]
47
+ async def jquants_alias(mailaddress: Mailaddress, password: Password) -> None:
48
+ await auth_jquants(mailaddress, password)
49
+
50
+
51
+ def auth_edinet(api_key: str) -> None:
52
+ """EDINET APIのAPIキーを設定ファイルに保存します。"""
53
+ from kabukit.utils.config import set_key
54
+
55
+ set_key("EDINET_API_KEY", api_key)
56
+ typer.echo("EDINETのAPIキーを保存しました。")
57
+
58
+
59
+ ApiKey = Annotated[str, Option(prompt=True, help="取得したEDINET APIキー。")]
60
+
61
+
62
+ @app.command()
63
+ def edinet(api_key: ApiKey) -> None:
64
+ """EDINET APIのAPIキーを設定ファイルに保存します。(エイリアス: e)"""
65
+ auth_edinet(api_key)
66
+
67
+
68
+ @app.command(name="e", hidden=True)
69
+ def edinet_alias(api_key: ApiKey) -> None:
70
+ auth_edinet(api_key)
71
+
72
+
73
+ @app.command()
74
+ def show() -> None:
75
+ """設定ファイルに保存したトークン・APIキーを表示します。"""
76
+ from dotenv import dotenv_values
77
+
78
+ from kabukit.utils.config import get_dotenv_path
79
+
80
+ path = get_dotenv_path()
81
+ typer.echo(f"Configuration file: {path}")
82
+
83
+ if path.exists():
84
+ config = dotenv_values(path)
85
+ for key, value in config.items():
86
+ typer.echo(f"{key}: {value}")
File without changes
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ import polars as pl
8
+ from platformdirs import user_cache_dir
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Self
12
+
13
+ from polars import DataFrame
14
+
15
+
16
+ class Base:
17
+ data: DataFrame
18
+
19
+ def __init__(self, data: DataFrame) -> None:
20
+ self.data = data
21
+
22
+ @classmethod
23
+ def data_dir(cls) -> Path:
24
+ clsname = cls.__name__.lower()
25
+ return Path(user_cache_dir("kabukit")) / clsname
26
+
27
+ def write(self) -> Path:
28
+ data_dir = self.data_dir()
29
+ data_dir.mkdir(parents=True, exist_ok=True)
30
+ path = datetime.datetime.today().strftime("%Y%m%d") # noqa: DTZ002
31
+ filename = data_dir / f"{path}.parquet"
32
+ self.data.write_parquet(filename)
33
+ return filename
34
+
35
+ @classmethod
36
+ def read(cls, path: str | None = None) -> Self:
37
+ data_dir = cls.data_dir()
38
+
39
+ if path is None:
40
+ filename = sorted(data_dir.glob("*.parquet"))[-1]
41
+ else:
42
+ filename = data_dir / path
43
+
44
+ data = pl.read_parquet(filename)
45
+ return cls(data)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from httpx import AsyncClient
6
+
7
+ if TYPE_CHECKING:
8
+ from typing import Self
9
+
10
+
11
+ class Client:
12
+ client: AsyncClient
13
+
14
+ def __init__(self, base_url: str = "") -> None:
15
+ self.client = AsyncClient(base_url=base_url)
16
+
17
+ async def aclose(self) -> None:
18
+ """HTTPクライアントを閉じる。"""
19
+ await self.client.aclose()
20
+
21
+ async def __aenter__(self) -> Self:
22
+ return self
23
+
24
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # pyright: ignore[reportMissingParameterType, reportUnknownParameterType] # noqa: ANN001
25
+ await self.aclose()
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from .base import Base
6
+
7
+ if TYPE_CHECKING:
8
+ from polars import DataFrame
9
+
10
+
11
+ class Info(Base):
12
+ pass
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import polars as pl
6
+
7
+ from .base import Base
8
+
9
+ if TYPE_CHECKING:
10
+ from datetime import timedelta
11
+ from typing import Self
12
+
13
+ from polars import Expr
14
+
15
+
16
+ class Prices(Base):
17
+ def truncate(self, every: str | timedelta | Expr) -> Self:
18
+ data = (
19
+ self.data.group_by(pl.col("Date").dt.truncate(every), "Code")
20
+ .agg(
21
+ pl.col("Open").drop_nulls().first(),
22
+ pl.col("High").max(),
23
+ pl.col("Low").min(),
24
+ pl.col("Close").drop_nulls().last(),
25
+ pl.col("Volume").sum(),
26
+ pl.col("TurnoverValue").sum(),
27
+ )
28
+ .sort("Code", "Date")
29
+ )
30
+ return self.__class__(data)
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import Base
4
+
5
+
6
+ class Statements(Base):
7
+ pass
@@ -0,0 +1,3 @@
1
+ from .concurrent import fetch, fetch_list
2
+
3
+ __all__ = ["fetch", "fetch_list"]
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ import zipfile
6
+ from enum import StrEnum
7
+ from typing import TYPE_CHECKING
8
+
9
+ import polars as pl
10
+ from polars import DataFrame
11
+
12
+ from kabukit.core.client import Client
13
+ from kabukit.utils.config import load_dotenv
14
+ from kabukit.utils.params import get_params
15
+
16
+ from .doc import clean_csv, clean_list
17
+
18
+ if TYPE_CHECKING:
19
+ import datetime
20
+
21
+ from httpx import Response
22
+ from httpx._types import QueryParamTypes
23
+
24
+ API_VERSION = "v2"
25
+ BASE_URL = f"https://api.edinet-fsa.go.jp/api/{API_VERSION}"
26
+
27
+
28
+ class AuthKey(StrEnum):
29
+ """Environment variable keys for EDINET authentication."""
30
+
31
+ API_KEY = "EDINET_API_KEY"
32
+
33
+
34
+ class EdinetClient(Client):
35
+ def __init__(self, api_key: str | None = None) -> None:
36
+ super().__init__(BASE_URL)
37
+ self.set_api_key(api_key)
38
+
39
+ def set_api_key(self, api_key: str | None = None) -> None:
40
+ if api_key is None:
41
+ load_dotenv()
42
+ api_key = os.environ.get(AuthKey.API_KEY)
43
+
44
+ if api_key:
45
+ self.client.params = {"Subscription-Key": api_key}
46
+
47
+ async def get(self, url: str, params: QueryParamTypes) -> Response:
48
+ resp = await self.client.get(url, params=params)
49
+ resp.raise_for_status()
50
+ return resp
51
+
52
+ async def get_count(self, date: str | datetime.date) -> int:
53
+ params = get_params(date=date, type=1)
54
+ resp = await self.get("/documents.json", params)
55
+ data = resp.json()
56
+ metadata = data["metadata"]
57
+
58
+ if metadata["status"] != "200":
59
+ return 0
60
+
61
+ return metadata["resultset"]["count"]
62
+
63
+ async def get_list(self, date: str | datetime.date) -> DataFrame:
64
+ params = get_params(date=date, type=2)
65
+ resp = await self.get("/documents.json", params)
66
+ data = resp.json()
67
+
68
+ if "results" not in data:
69
+ return DataFrame()
70
+
71
+ df = DataFrame(data["results"], infer_schema_length=None)
72
+
73
+ if df.is_empty():
74
+ return df
75
+
76
+ return clean_list(df, date)
77
+
78
+ async def get_document(self, doc_id: str, doc_type: int) -> Response:
79
+ params = get_params(type=doc_type)
80
+ return await self.get(f"/documents/{doc_id}", params)
81
+
82
+ async def get_pdf(self, doc_id: str) -> bytes:
83
+ resp = await self.get_document(doc_id, doc_type=2)
84
+ if resp.headers["content-type"] == "application/pdf":
85
+ return resp.content
86
+
87
+ msg = "PDF is not available."
88
+ raise ValueError(msg)
89
+
90
+ async def get_zip(self, doc_id: str, doc_type: int) -> bytes:
91
+ resp = await self.get_document(doc_id, doc_type=doc_type)
92
+ if resp.headers["content-type"] == "application/octet-stream":
93
+ return resp.content
94
+
95
+ msg = "ZIP is not available."
96
+ raise ValueError(msg)
97
+
98
+ async def get_csv(self, doc_id: str) -> DataFrame:
99
+ content = await self.get_zip(doc_id, doc_type=5)
100
+ buffer = io.BytesIO(content)
101
+
102
+ with zipfile.ZipFile(buffer) as zf:
103
+ for info in zf.infolist():
104
+ if info.filename.endswith(".csv"):
105
+ with zf.open(info) as f:
106
+ return pl.read_csv(
107
+ f.read(),
108
+ separator="\t",
109
+ encoding="utf-16-le",
110
+ ).pipe(clean_csv, doc_id)
111
+
112
+ msg = "CSV is not available."
113
+ raise ValueError(msg)