diff --git a/jquantsapi/apis/base.py b/jquantsapi/apis/base.py index ca53463..10ac69b 100644 --- a/jquantsapi/apis/base.py +++ b/jquantsapi/apis/base.py @@ -3,8 +3,6 @@ from abc import ABC, abstractmethod from typing import Any, Protocol -import pandas as pd # type: ignore - class SupportsRequest(Protocol): """ @@ -37,7 +35,7 @@ class BaseApi(ABC): version: str @abstractmethod - def execute(self, client: SupportsRequest, **params: Any) -> pd.DataFrame: + def execute(self, client: SupportsRequest, **params: Any) -> Any: """ 実際に API を実行し、結果を DataFrame で返す。 diff --git a/jquantsapi/apis/v2/bulk.py b/jquantsapi/apis/v2/bulk.py index 56cfdee..77360fc 100644 --- a/jquantsapi/apis/v2/bulk.py +++ b/jquantsapi/apis/v2/bulk.py @@ -14,6 +14,7 @@ class BulkListApiV2(BaseApi): v2 の Bulk List API (`/bulk/list`) のラッパークラス。 指定したエンドポイントで取得可能なデータ一覧を取得します。 + `endpoint` または `date` のどちらかは必須です。 """ name = "bulk_list" @@ -24,6 +25,9 @@ def execute( client: SupportsRequest, *, endpoint: Union[str, BulkEndpoint] = "", + date: str = "", + from_date: str = "", + to_date: str = "", **kwargs: Any, ) -> pd.DataFrame: """ @@ -31,7 +35,12 @@ def execute( Args: client: v2 `ClientV2` インスタンスを想定 - endpoint: 取得したいデータのエンドポイント (例: "/equities/master") + endpoint: 取得したいデータのエンドポイント (例: "/equities/master")。 + `date` と排他的に使用します。 + date: 対象日付 (YYYY-MM, YYYYMM, YYYY-MM-DD, YYYYMMDD)。 + `endpoint` と排他的に使用します。 + from_date: 取得期間の開始日。`endpoint` 指定時のみ使用可能。 + to_date: 取得期間の終了日。`endpoint` 指定時のみ使用可能。 """ url = f"{client.JQUANTS_API_BASE}/bulk/list" @@ -40,7 +49,15 @@ def execute( endpoint.value if isinstance(endpoint, BulkEndpoint) else endpoint ) - params: dict[str, Any] = {"endpoint": endpoint_str} + params: dict[str, Any] = {} + if endpoint_str: + params["endpoint"] = endpoint_str + if date: + params["date"] = date + if from_date: + params["from"] = from_date + if to_date: + params["to"] = to_date resp = client._get(url, params) # type: ignore[arg-type] payload = resp.json() @@ -73,6 +90,8 @@ def execute( client: SupportsRequest, *, key: str = "", + endpoint: Union[str, BulkEndpoint] = "", + date: str = "", **kwargs: Any, ) -> str: """ @@ -80,14 +99,27 @@ def execute( Args: client: v2 `ClientV2` インスタンスを想定 - key: BulkListで取得したKey + key: BulkListで取得したKey。`endpoint` + `date` と排他的に使用します。 + endpoint: 取得するデータのエンドポイント名。`date` と組み合わせて使用します。 + date: 対象日付 (YYYY-MM, YYYYMM, YYYY-MM-DD, YYYYMMDD)。`endpoint` と組み合わせて使用します。 Returns: str: ダウンロードURL """ url = f"{client.JQUANTS_API_BASE}/bulk/get" - params: dict[str, Any] = {"key": key} + # BulkEndpointの場合はvalue(str)を取得 + endpoint_str = ( + endpoint.value if isinstance(endpoint, BulkEndpoint) else endpoint + ) + + params: dict[str, Any] = {} + if key: + params["key"] = key + if endpoint_str: + params["endpoint"] = endpoint_str + if date: + params["date"] = date resp = client._get(url, params) # type: ignore[arg-type] payload = resp.json() diff --git a/jquantsapi/apis/v2/td.py b/jquantsapi/apis/v2/td.py new file mode 100644 index 0000000..2100336 --- /dev/null +++ b/jquantsapi/apis/v2/td.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from typing import Any, Optional + +import pandas as pd # type: ignore + +from jquantsapi import constants +from jquantsapi.apis.base import BaseApi, SupportsRequest + + +class TdListApiV2(BaseApi): + """ + v2 の TDnet/適時開示インデックス一覧 API (`/td/list`) のラッパークラス。 + + `date` または `code` のどちらかは必須です。 + """ + + name = "td_list" + version = "v2" + + def execute( + self, + client: SupportsRequest, + *, + date: str = "", + code: str = "", + from_date: str = "", + to_date: str = "", + disc_items: str = "", + cursor: str = "", + **kwargs: Any, + ) -> tuple[pd.DataFrame, Optional[str]]: + """ + v2 `/td/list` を実行し、(適時開示インデックス一覧, cursor) を返す。 + + pagination_key が返された場合は自動的に全件取得します。 + cursor はレスポンスの最終ページに含まれる場合に返却されます(含まれない場合は None)。 + + Args: + client: v2 `ClientV2` インスタンスを想定 + date: 開示日 (YYYYMMDD or YYYY-MM-DD)。`code` と排他的に使用します。 + code: 銘柄コード。`date` と排他的に使用します。 + from_date: 取得開始日。`code` と組み合わせて使用します。 + to_date: 取得終了日。`code` と組み合わせて使用します。 + disc_items: 公開項目コード(カンマ区切りで複数指定可能)。 + cursor: 前回レスポンスで返却された cursor。差分取得に使用します。 + `pagination_key` と同時指定不可。 + Returns: + tuple[pd.DataFrame, Optional[str]]: + - DataFrame: 取得した開示一覧 + - cursor: レスポンスに含まれる cursor(含まれない場合は None) + """ + url = f"{client.JQUANTS_API_BASE}/td/list" + + params: dict[str, Any] = {} + if date: + params["date"] = date + if code: + params["code"] = code + if from_date: + params["from"] = from_date + if to_date: + params["to"] = to_date + if disc_items: + params["discItems"] = disc_items + if cursor: + params["cursor"] = cursor + + all_data: list[dict[str, Any]] = [] + returned_cursor: Optional[str] = None + query = dict(params) + + while True: + resp = client._get(url, query) # type: ignore[arg-type] + payload = resp.json() + all_data.extend(payload.get("data", [])) + returned_cursor = payload.get("cursor") + + pagination_key = payload.get("pagination_key") + if not pagination_key: + break + query["pagination_key"] = pagination_key + + cols = constants.TD_LIST_COLUMNS_V2 + if not all_data: + return pd.DataFrame(columns=cols), returned_cursor + + df = pd.DataFrame.from_records(all_data) + if "DiscDate" in df.columns: + df["DiscDate"] = pd.to_datetime(df["DiscDate"], errors="coerce") + + return df[cols].reset_index(drop=True), returned_cursor + + +class TdFilesApiV2(BaseApi): + """ + v2 の TDnet/適時開示ファイル取得 API (`/td/files`) のラッパークラス。 + + 開示番号に対応するファイルのダウンロードURLを取得します。 + """ + + name = "td_files" + version = "v2" + + def execute( + self, + client: SupportsRequest, + *, + disc_no: str = "", + docs: str = "", + **kwargs: Any, + ) -> dict[str, Any]: + """ + v2 `/td/files` を実行し、ダウンロードURL情報を dict で返す。 + + Args: + client: v2 `ClientV2` インスタンスを想定 + disc_no: 開示番号(14桁) + docs: 取得するファイル種別(カンマ区切り: g/s/x)。省略時は全種別。 + + Returns: + dict: ``discNo`` と ``files`` (pdf/summaryPdf/xbrl の URL) を含む辞書 + """ + url = f"{client.JQUANTS_API_BASE}/td/files" + + params: dict[str, Any] = {"discNo": disc_no} + if docs: + params["docs"] = docs + + resp = client._get(url, params) # type: ignore[arg-type] + return resp.json() + + +class TdBulkApiV2(BaseApi): + """ + v2 の TDnet/適時開示インデックス一括ダウンロード API (`/td/bulk`) のラッパークラス。 + + 過去5年分の適時開示インデックスを収録した CSV (gzip) のダウンロードURLと + 最終更新日時を取得します。 + """ + + name = "td_bulk" + version = "v2" + + def execute( + self, + client: SupportsRequest, + **kwargs: Any, + ) -> dict[str, Any]: + """ + v2 `/td/bulk` を実行し、一括ダウンロード情報を dict で返す。 + + Returns: + dict: ``lastUpdated`` と ``url`` を含む辞書 + """ + url = f"{client.JQUANTS_API_BASE}/td/bulk" + resp = client._get(url, {}) # type: ignore[arg-type] + return resp.json() diff --git a/jquantsapi/client_v2.py b/jquantsapi/client_v2.py index 9db484a..f69f6b8 100644 --- a/jquantsapi/client_v2.py +++ b/jquantsapi/client_v2.py @@ -42,6 +42,7 @@ MktShortRatioApiV2, MktShortSaleReportApiV2, ) +from jquantsapi.apis.v2.td import TdBulkApiV2, TdFilesApiV2, TdListApiV2 from jquantsapi.enums import BulkEndpoint DatetimeLike = Union[datetime, pd.Timestamp, str] @@ -114,6 +115,9 @@ def __init__(self, api_key: Optional[str] = None) -> None: self._drv_bars_daily_opt_225_api = DrvBarsDailyOpt225ApiV2() self._bulk_list_api = BulkListApiV2() self._bulk_get_api = BulkGetApiV2() + self._td_list_api = TdListApiV2() + self._td_files_api = TdFilesApiV2() + self._td_bulk_api = TdBulkApiV2() # ------------------------------------------------------------------ # 内部ユーティリティ @@ -1300,29 +1304,55 @@ def get_drv_bars_daily_opt_225_range( # ------------------------------------------------------------------ def get_bulk_list( self, - endpoint: Union[str, BulkEndpoint], + endpoint: Union[str, BulkEndpoint] = "", + date: str = "", + from_date: str = "", + to_date: str = "", ) -> pd.DataFrame: """ bulk-list: 取得可能なデータ一覧 (v2: /bulk/list) + `endpoint` または `date` のどちらかは必須です。 + Args: endpoint: 取得したいデータのエンドポイント - (例: BulkEndpoint.EQ_MASTER または "/equities/master") + (例: BulkEndpoint.EQ_MASTER または "/equities/master")。 + `date` と排他的に使用します。 + date: 対象日付 (YYYY-MM, YYYYMM, YYYY-MM-DD, YYYYMMDD)。 + 契約プランでアクセス可能な全エンドポイントのファイル一覧を返します。 + `endpoint` と排他的に使用します。 + from_date: 取得期間の開始日。`endpoint` 指定時のみ使用可能。 + to_date: 取得期間の終了日。`endpoint` 指定時のみ使用可能。 Returns: pd.DataFrame: データ一覧 (Key, Size, LastModified) """ - return self._bulk_list_api.execute(self, endpoint=endpoint) + return self._bulk_list_api.execute( + self, + endpoint=endpoint, + date=date, + from_date=from_date, + to_date=to_date, + ) - def get_bulk(self, key: str) -> str: + def get_bulk( + self, + key: str = "", + endpoint: Union[str, BulkEndpoint] = "", + date: str = "", + ) -> str: """ bulk-get: データダウンロードURL取得 (v2: /bulk/get) + `key` または `endpoint` + `date` の組み合わせのどちらかを指定してください。 + Args: - key: get_bulk_listで取得したKey + key: get_bulk_listで取得したKey。`endpoint` + `date` と排他的に使用します。 + endpoint: 取得するデータのエンドポイント名。`date` と組み合わせて使用します。 + date: 対象日付 (YYYY-MM, YYYYMM, YYYY-MM-DD, YYYYMMDD)。`endpoint` と組み合わせて使用します。 Returns: str: ダウンロードURL """ - return self._bulk_get_api.execute(self, key=key) + return self._bulk_get_api.execute(self, key=key, endpoint=endpoint, date=date) def download_bulk(self, key: str, output_path: str) -> None: """ @@ -1335,24 +1365,122 @@ def download_bulk(self, key: str, output_path: str) -> None: Raises: ValueError: output_path が空文字列の場合 """ - # バリデーション if not output_path or not output_path.strip(): raise ValueError("output_path must not be empty") - # ダウンロード URL を取得 url = self._bulk_get_api.execute(self, key=key) - # ディレクトリが存在しない場合は作成 output_dir = os.path.dirname(os.path.abspath(output_path)) if output_dir: os.makedirs(output_dir, exist_ok=True) - # ファイルをダウンロード session = self._request_session() response = session.get(url, stream=True, timeout=300) self._raise_for_status(response) - # ファイルに書き込み with open(output_path, "wb") as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) + + def download_bulk_by_endpoint( + self, + endpoint: Union[str, BulkEndpoint], + date: str, + output_path: str, + ) -> None: + """ + エンドポイントと日付を指定してファイルをダウンロードして保存 + + Args: + endpoint: 取得するデータのエンドポイント名 + date: 対象日付 (YYYY-MM, YYYYMM, YYYY-MM-DD, YYYYMMDD) + output_path: ダウンロードファイルの保存先パス + + Raises: + ValueError: output_path が空文字列の場合 + """ + if not output_path or not output_path.strip(): + raise ValueError("output_path must not be empty") + + url = self._bulk_get_api.execute(self, endpoint=endpoint, date=date) + + output_dir = os.path.dirname(os.path.abspath(output_path)) + if output_dir: + os.makedirs(output_dir, exist_ok=True) + + session = self._request_session() + response = session.get(url, stream=True, timeout=300) + self._raise_for_status(response) + + with open(output_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + # ------------------------------------------------------------------ + # TDnet/適時開示 API (v2: /td/*) + # ------------------------------------------------------------------ + def get_td_list( + self, + date: str = "", + code: str = "", + from_date: str = "", + to_date: str = "", + disc_items: str = "", + cursor: str = "", + ) -> tuple[pd.DataFrame, Optional[str]]: + """ + td-list: 適時開示インデックス一覧 (v2: /td/list) + + `date` または `code` のどちらかは必須です。 + pagination_key が返された場合は自動的に全件取得します。 + + Args: + date: 開示日 (YYYYMMDD or YYYY-MM-DD)。`code` と排他的に使用します。 + code: 銘柄コード。`date` と排他的に使用します。 + from_date: 取得開始日。`code` と組み合わせて使用します。 + to_date: 取得終了日。`code` と組み合わせて使用します。 + disc_items: 公開項目コードで絞り込む(カンマ区切りで複数指定可能)。 + cursor: 前回レスポンスで返却された cursor。差分取得に使用します。 + Returns: + tuple[pd.DataFrame, Optional[str]]: + - DataFrame: 適時開示インデックス一覧 + - cursor: レスポンスに含まれる cursor(含まれない場合は None) + """ + return self._td_list_api.execute( + self, + date=date, + code=code, + from_date=from_date, + to_date=to_date, + disc_items=disc_items, + cursor=cursor, + ) + + def get_td_files( + self, + disc_no: str, + docs: str = "", + ) -> dict: + """ + td-files: 適時開示ファイルダウンロードURL取得 (v2: /td/files) + + Args: + disc_no: 開示番号(14桁) + docs: 取得するファイル種別(カンマ区切り: g=全文PDF, s=サマリPDF, x=XBRL)。 + 省略時は全種別を返します。 + Returns: + dict: ``discNo`` と ``files`` (pdf/summaryPdf/xbrl の URL) を含む辞書 + """ + return self._td_files_api.execute(self, disc_no=disc_no, docs=docs) + + def get_td_bulk(self) -> dict: + """ + td-bulk: 適時開示インデックス一括ダウンロードURL取得 (v2: /td/bulk) + + 過去5年分の適時開示インデックスを収録した CSV (gzip) の + ダウンロードURLと最終更新日時を取得します。 + + Returns: + dict: ``lastUpdated`` (ISO 8601) と ``url`` を含む辞書 + """ + return self._td_bulk_api.execute(self) diff --git a/jquantsapi/constants.py b/jquantsapi/constants.py index bbf6795..f85e89f 100644 --- a/jquantsapi/constants.py +++ b/jquantsapi/constants.py @@ -1207,3 +1207,16 @@ "Size", "LastModified", ] + +TD_LIST_COLUMNS_V2 = [ + "DiscNo", + "Code", + "Name", + "DiscDate", + "DiscTime", + "Title", + "DiscStatus", + "RevNo", + "DiscItems", + "Docs", +] diff --git a/jquantsapi/enums.py b/jquantsapi/enums.py index 1fada35..759fe30 100644 --- a/jquantsapi/enums.py +++ b/jquantsapi/enums.py @@ -52,6 +52,8 @@ class BulkEndpoint(str, Enum): MKT_MARGIN_ALERT = "/markets/margin-alert" # 売買内訳データAPI MKT_BREAKDOWN = "/markets/breakdown" + # 取引カレンダーAPI + MKT_CALENDAR = "/markets/calendar" # 指数四本値API IDX_BARS_DAILY = "/indices/bars/daily" diff --git a/tests/test_client_v2.py b/tests/test_client_v2.py index cfb09ac..b88231f 100644 --- a/tests/test_client_v2.py +++ b/tests/test_client_v2.py @@ -362,14 +362,35 @@ def test_aggregate_bars_n_minute_15min(): @pytest.mark.parametrize( - "endpoint, exp_params", + "kwargs, exp_params", ( - ("/equities/master", {"endpoint": "/equities/master"}), - ("/equities/bars/daily", {"endpoint": "/equities/bars/daily"}), - ("/fins/summary", {"endpoint": "/fins/summary"}), + ( + {"endpoint": "/equities/master"}, + {"endpoint": "/equities/master"}, + ), + ( + {"endpoint": "/equities/bars/daily"}, + {"endpoint": "/equities/bars/daily"}, + ), + ( + {"endpoint": "/fins/summary"}, + {"endpoint": "/fins/summary"}, + ), + ( + {"date": "2024-01"}, + {"date": "2024-01"}, + ), + ( + { + "endpoint": "/equities/bars/daily", + "from_date": "2024-01", + "to_date": "2024-03", + }, + {"endpoint": "/equities/bars/daily", "from": "2024-01", "to": "2024-03"}, + ), ), ) -def test_get_bulk_list(endpoint, exp_params): +def test_get_bulk_list(kwargs, exp_params): """get_bulk_listのパラメータテスト""" ret_value = {"data": []} # resp.json()で返される辞書 exp_ret_len = 0 @@ -381,13 +402,26 @@ def test_get_bulk_list(endpoint, exp_params): mock_get.return_value.json.return_value = ret_value cli = jquantsapi.ClientV2() - ret = cli.get_bulk_list(endpoint=endpoint) + ret = cli.get_bulk_list(**kwargs) args, _ = mock_get.call_args assert args[1] == exp_params assert len(ret) == exp_ret_len -def test_get_bulk(): +@pytest.mark.parametrize( + "kwargs, exp_params", + ( + ( + {"key": "2024/01/01/eq_master.csv"}, + {"key": "2024/01/01/eq_master.csv"}, + ), + ( + {"endpoint": "/equities/bars/daily", "date": "2024-01"}, + {"endpoint": "/equities/bars/daily", "date": "2024-01"}, + ), + ), +) +def test_get_bulk(kwargs, exp_params): """get_bulkのテスト""" ret_value = {"url": "https://example.com/data.csv"} # resp.json()で返される辞書 exp_raise = does_not_raise() @@ -398,12 +432,187 @@ def test_get_bulk(): mock_get.return_value.json.return_value = ret_value cli = jquantsapi.ClientV2() - ret = cli.get_bulk(key="2024/01/01/eq_master.csv") + ret = cli.get_bulk(**kwargs) args, _ = mock_get.call_args - assert args[1] == {"key": "2024/01/01/eq_master.csv"} + assert args[1] == exp_params assert ret == "https://example.com/data.csv" +def test_download_bulk_by_endpoint(): + """download_bulk_by_endpointのパラメータテスト""" + download_url = "https://example.com/data.csv.gz" + + with patch.object( + jquantsapi.ClientV2, "_load_config", return_value={"api_key": "dummy_key"} + ), patch.object(jquantsapi.ClientV2, "_get") as mock_get, patch.object( + jquantsapi.ClientV2, "_request_session" + ) as mock_session: + mock_get.return_value.json.return_value = {"url": download_url} + mock_response = MagicMock() + mock_response.ok = True + mock_response.iter_content.return_value = [] + mock_session.return_value.get.return_value = mock_response + + cli = jquantsapi.ClientV2() + with pytest.raises(ValueError): + cli.download_bulk_by_endpoint( + endpoint="/equities/bars/daily", date="2024-01", output_path="" + ) + + cli.download_bulk_by_endpoint( + endpoint="/equities/bars/daily", + date="2024-01", + output_path="/tmp/test.csv.gz", + ) + args, _ = mock_get.call_args + assert args[1] == {"endpoint": "/equities/bars/daily", "date": "2024-01"} + + +TD_RECORD = { + "DiscNo": "20250401130100", + "Code": "86970", + "Name": "日本取引所グループ", + "DiscDate": "2025-04-01", + "DiscTime": "08:00", + "Title": "決算短信", + "DiscStatus": None, + "RevNo": 1, + "DiscItems": ["14012"], + "Docs": ["g", "s"], +} + + +@pytest.mark.parametrize( + "kwargs, exp_params", + ( + ( + {"date": "20250401"}, + {"date": "20250401"}, + ), + ( + {"code": "86970"}, + {"code": "86970"}, + ), + ( + {"code": "86970", "from_date": "20250301", "to_date": "20250401"}, + {"code": "86970", "from": "20250301", "to": "20250401"}, + ), + ( + {"date": "20250401", "disc_items": "10010,10020"}, + {"date": "20250401", "discItems": "10010,10020"}, + ), + ), +) +def test_get_td_list(kwargs, exp_params): + """get_td_listのパラメータテスト: tuple(DataFrame, cursor)を返す""" + ret_value = {"data": [TD_RECORD]} + + with patch.object( + jquantsapi.ClientV2, "_load_config", return_value={"api_key": "dummy_key"} + ), patch.object(jquantsapi.ClientV2, "_get") as mock_get: + mock_get.return_value.json.return_value = ret_value + + cli = jquantsapi.ClientV2() + df, cursor = cli.get_td_list(**kwargs) + args, _ = mock_get.call_args + assert args[1] == exp_params + assert len(df) == 1 + assert cursor is None + + +def test_get_td_list_returns_cursor(): + """get_td_listがレスポンスのcursorを返すことを確認""" + cursor_value = "eyJkIjoiMjAyNS0wNC0wMSJ9" + ret_value = {"data": [TD_RECORD], "cursor": cursor_value} + + with patch.object( + jquantsapi.ClientV2, "_load_config", return_value={"api_key": "dummy_key"} + ), patch.object(jquantsapi.ClientV2, "_get") as mock_get: + mock_get.return_value.json.return_value = ret_value + + cli = jquantsapi.ClientV2() + df, cursor = cli.get_td_list(date="20250401") + assert len(df) == 1 + assert cursor == cursor_value + + +def test_get_td_list_with_pagination(): + """get_td_listがpagination_keyを自動処理して全件取得することを確認""" + page1 = {"data": [TD_RECORD], "pagination_key": "page2key"} + page2 = {"data": [TD_RECORD], "cursor": "eyJkIjoiMjAyNS0wNC0wMSJ9"} + + with patch.object( + jquantsapi.ClientV2, "_load_config", return_value={"api_key": "dummy_key"} + ), patch.object(jquantsapi.ClientV2, "_get") as mock_get: + mock_get.return_value.json.side_effect = [page1, page2] + + cli = jquantsapi.ClientV2() + df, cursor = cli.get_td_list(date="20250401") + assert len(df) == 2 + assert cursor == "eyJkIjoiMjAyNS0wNC0wMSJ9" + assert mock_get.call_count == 2 + + +def test_get_td_files(): + """get_td_filesのテスト""" + ret_value = { + "discNo": "20250401130100", + "files": { + "pdf": "https://example.com/pdf", + "summaryPdf": "https://example.com/summary", + "xbrl": "https://example.com/xbrl", + }, + } + + with patch.object( + jquantsapi.ClientV2, "_load_config", return_value={"api_key": "dummy_key"} + ), patch.object(jquantsapi.ClientV2, "_get") as mock_get: + mock_get.return_value.json.return_value = ret_value + + cli = jquantsapi.ClientV2() + ret = cli.get_td_files(disc_no="20250401130100") + args, _ = mock_get.call_args + assert args[1] == {"discNo": "20250401130100"} + assert ret["discNo"] == "20250401130100" + assert ret["files"]["pdf"] == "https://example.com/pdf" + + +def test_get_td_files_with_docs(): + """get_td_files docs パラメータのテスト""" + ret_value = { + "discNo": "20250401130100", + "files": {"pdf": "https://example.com/pdf"}, + } + + with patch.object( + jquantsapi.ClientV2, "_load_config", return_value={"api_key": "dummy_key"} + ), patch.object(jquantsapi.ClientV2, "_get") as mock_get: + mock_get.return_value.json.return_value = ret_value + + cli = jquantsapi.ClientV2() + cli.get_td_files(disc_no="20250401130100", docs="g,s") + args, _ = mock_get.call_args + assert args[1] == {"discNo": "20250401130100", "docs": "g,s"} + + +def test_get_td_bulk(): + """get_td_bulkのテスト""" + ret_value = { + "lastUpdated": "2025-04-01T08:00:00Z", + "url": "https://example.com/bulk.csv.gz", + } + + with patch.object( + jquantsapi.ClientV2, "_load_config", return_value={"api_key": "dummy_key"} + ), patch.object(jquantsapi.ClientV2, "_get") as mock_get: + mock_get.return_value.json.return_value = ret_value + + cli = jquantsapi.ClientV2() + ret = cli.get_td_bulk() + assert ret["lastUpdated"] == "2025-04-01T08:00:00Z" + assert ret["url"] == "https://example.com/bulk.csv.gz" + + def test_get_raises_with_api_error_message(): """_get()がエラー時にAPIのメッセージを含むHTTPErrorを送出することを確認""" mock_resp = MagicMock()