kabukit 0.1.0__py3-none-any.whl → 0.1.1__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.
- kabukit/__init__.py +3 -4
- kabukit/cli/__init__.py +0 -0
- kabukit/cli/app.py +22 -0
- kabukit/cli/auth.py +86 -0
- kabukit/concurrent.py +40 -0
- kabukit/config.py +26 -0
- kabukit/edinet/__init__.py +0 -0
- kabukit/edinet/client.py +111 -0
- kabukit/jquants/__init__.py +3 -0
- kabukit/jquants/client.py +218 -180
- kabukit/jquants/info.py +23 -0
- kabukit/jquants/prices.py +0 -0
- kabukit/jquants/schema.py +169 -0
- kabukit/jquants/statements.py +69 -0
- kabukit/jquants/stream.py +122 -0
- kabukit/params.py +47 -0
- kabukit/py.typed +0 -0
- {kabukit-0.1.0.dist-info → kabukit-0.1.1.dist-info}/METADATA +13 -16
- kabukit-0.1.1.dist-info/RECORD +21 -0
- {kabukit-0.1.0.dist-info → kabukit-0.1.1.dist-info}/WHEEL +1 -1
- kabukit-0.1.1.dist-info/entry_points.txt +3 -0
- kabukit/cli.py +0 -40
- kabukit-0.1.0.dist-info/RECORD +0 -8
- kabukit-0.1.0.dist-info/entry_points.txt +0 -3
kabukit/__init__.py
CHANGED
kabukit/cli/__init__.py
ADDED
File without changes
|
kabukit/cli/app.py
ADDED
@@ -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')}")
|
kabukit/cli/auth.py
ADDED
@@ -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.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.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}")
|
kabukit/concurrent.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from typing import TYPE_CHECKING
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
8
|
+
|
9
|
+
MAX_CONCURRENCY = 12
|
10
|
+
|
11
|
+
|
12
|
+
async def collect[R](
|
13
|
+
awaitables: Iterable[Awaitable[R]],
|
14
|
+
/,
|
15
|
+
max_concurrency: int | None = None,
|
16
|
+
) -> AsyncIterator[R]:
|
17
|
+
max_concurrency = max_concurrency or MAX_CONCURRENCY
|
18
|
+
semaphore = asyncio.Semaphore(max_concurrency)
|
19
|
+
|
20
|
+
async def run(awaitable: Awaitable[R]) -> R:
|
21
|
+
async with semaphore:
|
22
|
+
return await awaitable
|
23
|
+
|
24
|
+
futures = (run(awaitable) for awaitable in awaitables)
|
25
|
+
|
26
|
+
async for future in asyncio.as_completed(futures):
|
27
|
+
yield await future
|
28
|
+
|
29
|
+
|
30
|
+
async def collect_fn[T, R](
|
31
|
+
function: Callable[[T], Awaitable[R]],
|
32
|
+
args: Iterable[T],
|
33
|
+
/,
|
34
|
+
max_concurrency: int | None = None,
|
35
|
+
) -> AsyncIterator[R]:
|
36
|
+
max_concurrency = max_concurrency or MAX_CONCURRENCY
|
37
|
+
awaitables = (function(arg) for arg in args)
|
38
|
+
|
39
|
+
async for item in collect(awaitables, max_concurrency=max_concurrency):
|
40
|
+
yield item
|
kabukit/config.py
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from functools import cache
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
import dotenv
|
7
|
+
from platformdirs import user_config_dir
|
8
|
+
|
9
|
+
|
10
|
+
@cache
|
11
|
+
def get_dotenv_path() -> Path:
|
12
|
+
"""Return the path to the .env file in the user config directory."""
|
13
|
+
config_dir = Path(user_config_dir("kabukit"))
|
14
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
15
|
+
return config_dir / ".env"
|
16
|
+
|
17
|
+
|
18
|
+
@cache
|
19
|
+
def load_dotenv() -> bool:
|
20
|
+
dotenv_path = get_dotenv_path()
|
21
|
+
return dotenv.load_dotenv(dotenv_path)
|
22
|
+
|
23
|
+
|
24
|
+
def set_key(key: str, value: str) -> tuple[bool | None, str, str]:
|
25
|
+
dotenv_path = get_dotenv_path()
|
26
|
+
return dotenv.set_key(dotenv_path, key, value)
|
File without changes
|
kabukit/edinet/client.py
ADDED
@@ -0,0 +1,111 @@
|
|
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 httpx import AsyncClient
|
11
|
+
from polars import DataFrame
|
12
|
+
|
13
|
+
from kabukit.config import load_dotenv
|
14
|
+
from kabukit.params import get_params
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
import datetime
|
18
|
+
|
19
|
+
from httpx import Response
|
20
|
+
from httpx._types import QueryParamTypes
|
21
|
+
|
22
|
+
API_VERSION = "v2"
|
23
|
+
BASE_URL = f"https://api.edinet-fsa.go.jp/api/{API_VERSION}"
|
24
|
+
|
25
|
+
|
26
|
+
class AuthKey(StrEnum):
|
27
|
+
"""Environment variable keys for EDINET authentication."""
|
28
|
+
|
29
|
+
API_KEY = "EDINET_API_KEY"
|
30
|
+
|
31
|
+
|
32
|
+
class EdinetClient:
|
33
|
+
client: AsyncClient
|
34
|
+
|
35
|
+
def __init__(self, api_key: str | None = None) -> None:
|
36
|
+
self.client = AsyncClient(base_url=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 aclose(self) -> None:
|
48
|
+
await self.client.aclose()
|
49
|
+
|
50
|
+
async def get(self, url: str, params: QueryParamTypes) -> Response:
|
51
|
+
resp = await self.client.get(url, params=params)
|
52
|
+
resp.raise_for_status()
|
53
|
+
return resp
|
54
|
+
|
55
|
+
async def get_count(self, date: str | datetime.date) -> int:
|
56
|
+
params = get_params(date=date, type=1)
|
57
|
+
resp = await self.get("/documents.json", params)
|
58
|
+
data = resp.json()
|
59
|
+
metadata = data["metadata"]
|
60
|
+
|
61
|
+
if metadata["status"] != "200":
|
62
|
+
return 0
|
63
|
+
|
64
|
+
return metadata["resultset"]["count"]
|
65
|
+
|
66
|
+
async def get_list(self, date: str | datetime.date) -> DataFrame:
|
67
|
+
params = get_params(date=date, type=2)
|
68
|
+
resp = await self.get("/documents.json", params)
|
69
|
+
data = resp.json()
|
70
|
+
|
71
|
+
if "results" not in data:
|
72
|
+
return DataFrame()
|
73
|
+
|
74
|
+
return DataFrame(data["results"], infer_schema_length=None)
|
75
|
+
|
76
|
+
async def get_document(self, doc_id: str, doc_type: int) -> Response:
|
77
|
+
params = get_params(type=doc_type)
|
78
|
+
return await self.get(f"/documents/{doc_id}", params)
|
79
|
+
|
80
|
+
async def get_pdf(self, doc_id: str) -> bytes:
|
81
|
+
resp = await self.get_document(doc_id, doc_type=2)
|
82
|
+
if resp.headers["content-type"] == "application/pdf":
|
83
|
+
return resp.content
|
84
|
+
|
85
|
+
msg = "PDF is not available."
|
86
|
+
raise ValueError(msg)
|
87
|
+
|
88
|
+
async def get_zip(self, doc_id: str, doc_type: int) -> bytes:
|
89
|
+
resp = await self.get_document(doc_id, doc_type=doc_type)
|
90
|
+
if resp.headers["content-type"] == "application/octet-stream":
|
91
|
+
return resp.content
|
92
|
+
|
93
|
+
msg = "ZIP is not available."
|
94
|
+
raise ValueError(msg)
|
95
|
+
|
96
|
+
async def get_csv(self, doc_id: str) -> DataFrame:
|
97
|
+
content = await self.get_zip(doc_id, doc_type=5)
|
98
|
+
buffer = io.BytesIO(content)
|
99
|
+
|
100
|
+
with zipfile.ZipFile(buffer) as zf:
|
101
|
+
for info in zf.infolist():
|
102
|
+
if info.filename.endswith(".csv"):
|
103
|
+
with zf.open(info) as f:
|
104
|
+
return pl.read_csv(
|
105
|
+
f.read(),
|
106
|
+
separator="\t",
|
107
|
+
encoding="utf-16-le",
|
108
|
+
)
|
109
|
+
|
110
|
+
msg = "CSV is not available."
|
111
|
+
raise ValueError(msg)
|
kabukit/jquants/__init__.py
CHANGED