diff --git a/eodhd/APIs/SecFilings.py b/eodhd/APIs/SecFilings.py new file mode 100644 index 0000000..57c1d10 --- /dev/null +++ b/eodhd/APIs/SecFilings.py @@ -0,0 +1,137 @@ +# APIs/SecFilings.py + +from .BaseAPI import BaseAPI + + +class SecFilingsAPI(BaseAPI): + """ + Wrapper for the SEC Filings API: + + GET /api/sec-filings/{symbol} overview (counts + latest per form type) + GET /api/sec-filings/{symbol}/10k annual reports (10-K), paginated + GET /api/sec-filings/{symbol}/10q quarterly reports (10-Q), paginated + GET /api/sec-filings/{symbol}/8k material events (8-K), paginated + + Notes: + - The overview endpoint returns { data, meta, links } where data is a dict + { ticker, exchange, name, cik, filings } and filings is keyed by form type + ("10k"/"10q"/"8k"/"form4"), each { count, latest, url }. meta and links are + empty and there is no pagination. + - The 10-K, 10-Q and 8-K endpoints return { data, meta, links } with data as a + list of rows and meta { total, page: { offset, limit } }; links.next is a + URL string or null. + - Pagination uses page[offset] (>= 0, default 0) and page[limit] (1..100, + default 20). + - Any numeric field in a row may be null. + - Responses are parsed JSON. + + Docs: https://eodhd.com/financial-apis/sec-filings-api + """ + + @staticmethod + def _validate_symbol(symbol) -> str: + """Validate a ticker symbol and return it stripped (e.g. 'AAPL.US').""" + if symbol is None or not isinstance(symbol, str) or symbol.strip() == "": + raise ValueError("Parameter 'symbol' is required and must be a non-empty string (e.g. 'AAPL.US').") + return symbol.strip() + + def get_sec_filings_overview(self, api_token: str, symbol: str): + """ + GET /api/sec-filings/{symbol} + + Overview of a company's SEC filings: counts, latest date and URL per form + type. Parameterless (no pagination). + + Params: + symbol: ticker symbol (e.g. "AAPL.US"). + + Response data (dict): { ticker, exchange, name, cik, + filings: { "10k": {count, latest, url}, "10q": {...}, + "8k": {...}, "form4": {...} } }. + """ + symbol = self._validate_symbol(symbol) + + return self._rest_get_method( + api_key=api_token, + endpoint="sec-filings", + uri=symbol, + querystring="", + ) + + def get_sec_filings_10k(self, api_token: str, symbol: str, + page_offset: int = 0, page_limit: int = 20): + """ + GET /api/sec-filings/{symbol}/10k + + Annual reports (10-K) with parsed financials, paginated. + + Params: + symbol: ticker symbol (e.g. "AAPL.US"). + page_offset: >= 0 (default 0). page_limit: 1..100 (default 20). + + Response data[] row: accession_number, filed_at, period_of_report, + fiscal_year_end plus the parsed income-statement, balance-sheet and + cash-flow financials (any numeric may be null). + """ + symbol = self._validate_symbol(symbol) + + query_string = self._pagination(page_offset, page_limit) + + return self._rest_get_method( + api_key=api_token, + endpoint="sec-filings", + uri=f"{symbol}/10k", + querystring=query_string, + ) + + def get_sec_filings_10q(self, api_token: str, symbol: str, + page_offset: int = 0, page_limit: int = 20): + """ + GET /api/sec-filings/{symbol}/10q + + Quarterly reports (10-Q) with parsed financials, paginated. + + Same shape as the 10-K rows except the period metadata is + fiscal_quarter_end (str) instead of fiscal_year_end, plus fiscal_quarter + (int). + + Params: + symbol: ticker symbol (e.g. "AAPL.US"). + page_offset: >= 0 (default 0). page_limit: 1..100 (default 20). + """ + symbol = self._validate_symbol(symbol) + + query_string = self._pagination(page_offset, page_limit) + + return self._rest_get_method( + api_key=api_token, + endpoint="sec-filings", + uri=f"{symbol}/10q", + querystring=query_string, + ) + + def get_sec_filings_8k(self, api_token: str, symbol: str, + page_offset: int = 0, page_limit: int = 20): + """ + GET /api/sec-filings/{symbol}/8k + + Material events (8-K), paginated. + + Params: + symbol: ticker symbol (e.g. "AAPL.US"). + page_offset: >= 0 (default 0). page_limit: 1..100 (default 20). + + Response data[] item: { accession_number, filed_at, period_of_report, + items: [str], item_sections: [{item, title, text}], + exhibits: [{number, description}] }. + """ + symbol = self._validate_symbol(symbol) + + query_string = self._pagination(page_offset, page_limit) + + return self._rest_get_method( + api_key=api_token, + endpoint="sec-filings", + uri=f"{symbol}/8k", + querystring=query_string, + ) diff --git a/eodhd/APIs/__init__.py b/eodhd/APIs/__init__.py index 5ce175c..4afd5e6 100644 --- a/eodhd/APIs/__init__.py +++ b/eodhd/APIs/__init__.py @@ -38,6 +38,7 @@ from .SanctionsAPI import SanctionsAPI from .InterestRatesAPI import InterestRatesAPI from .RealEstate import RealEstateAPI +from .SecFilings import SecFilingsAPI #Marketplace endpoints from .MPIndexComponentsAPI import MPIndexComponentsAPI diff --git a/eodhd/apiclient.py b/eodhd/apiclient.py index de5c79e..2d5517b 100644 --- a/eodhd/apiclient.py +++ b/eodhd/apiclient.py @@ -55,6 +55,7 @@ from eodhd.APIs import SanctionsAPI from eodhd.APIs import InterestRatesAPI from eodhd.APIs import RealEstateAPI +from eodhd.APIs import SecFilingsAPI #Marketplace endpoints from eodhd.APIs import MPIndexComponentsAPI @@ -2018,6 +2019,77 @@ def get_real_estate_detailed_series(self, code): api_token=self._api_key, code=code, ) + def get_sec_filings_overview(self, symbol): + """ + SEC Filings API: overview of a company's filings (counts, latest date and + URL per form type: 10-K, 10-Q, 8-K, Form 4). + Endpoint: GET /api/sec-filings/{symbol} + + Args: + symbol [REQUIRED] - ticker symbol, e.g. "AAPL.US" + Returns: dict envelope { data, meta, links } where data is a dict + { ticker, exchange, name, cik, filings } + For more information visit: https://eodhd.com/financial-apis/sec-filings-api + """ + api_call = SecFilingsAPI(session=self._session, timeout=self._timeout) + return api_call.get_sec_filings_overview( + api_token=self._api_key, symbol=symbol, + ) + + def get_sec_filings_10k(self, symbol, page_offset=0, page_limit=20): + """ + SEC Filings API: annual reports (10-K) with parsed financials, paginated. + Endpoint: GET /api/sec-filings/{symbol}/10k + + Args: + symbol [REQUIRED] - ticker symbol, e.g. "AAPL.US" + page_offset [OPTIONAL] - >= 0 (default 0) + page_limit [OPTIONAL] - 1..100 (default 20) + Returns: dict envelope { data, meta, links } + For more information visit: https://eodhd.com/financial-apis/sec-filings-api + """ + api_call = SecFilingsAPI(session=self._session, timeout=self._timeout) + return api_call.get_sec_filings_10k( + api_token=self._api_key, symbol=symbol, + page_offset=page_offset, page_limit=page_limit, + ) + + def get_sec_filings_10q(self, symbol, page_offset=0, page_limit=20): + """ + SEC Filings API: quarterly reports (10-Q) with parsed financials, paginated. + Endpoint: GET /api/sec-filings/{symbol}/10q + + Args: + symbol [REQUIRED] - ticker symbol, e.g. "AAPL.US" + page_offset [OPTIONAL] - >= 0 (default 0) + page_limit [OPTIONAL] - 1..100 (default 20) + Returns: dict envelope { data, meta, links } + For more information visit: https://eodhd.com/financial-apis/sec-filings-api + """ + api_call = SecFilingsAPI(session=self._session, timeout=self._timeout) + return api_call.get_sec_filings_10q( + api_token=self._api_key, symbol=symbol, + page_offset=page_offset, page_limit=page_limit, + ) + + def get_sec_filings_8k(self, symbol, page_offset=0, page_limit=20): + """ + SEC Filings API: material events (8-K), paginated. + Endpoint: GET /api/sec-filings/{symbol}/8k + + Args: + symbol [REQUIRED] - ticker symbol, e.g. "AAPL.US" + page_offset [OPTIONAL] - >= 0 (default 0) + page_limit [OPTIONAL] - 1..100 (default 20) + Returns: dict envelope { data, meta, links } + For more information visit: https://eodhd.com/financial-apis/sec-filings-api + """ + api_call = SecFilingsAPI(session=self._session, timeout=self._timeout) + return api_call.get_sec_filings_8k( + api_token=self._api_key, symbol=symbol, + page_offset=page_offset, page_limit=page_limit, + ) + class ScannerClient: """Scanner class""" diff --git a/tests/test_sec_filings.py b/tests/test_sec_filings.py new file mode 100644 index 0000000..01d00d3 --- /dev/null +++ b/tests/test_sec_filings.py @@ -0,0 +1,181 @@ +"""Tests for SecFilingsAPI (SEC filings: overview, 10-K, 10-Q, 8-K).""" + +import pytest +from unittest.mock import MagicMock + +from eodhd.APIs.SecFilings import SecFilingsAPI + + +@pytest.fixture +def mock_session(): + return MagicMock() + + +def _make_api(session): + return SecFilingsAPI(session=session) + + +def _mock_response(session, data=None): + resp = MagicMock() + resp.status_code = 200 + resp.json.return_value = data if data is not None else {"data": [], "meta": {}, "links": {}} + session.get.return_value = resp + + +TOKEN = "test1234567890123456" + + +# ------------------------------------------------------------------- overview + +def test_overview_url(mock_session): + _mock_response(mock_session, {"data": {}, "meta": {}, "links": {}}) + api = _make_api(mock_session) + api.get_sec_filings_overview(api_token=TOKEN, symbol="AAPL.US") + + call_url = mock_session.get.call_args[0][0] + assert "/sec-filings/AAPL.US?" in call_url + assert "api_token=" + TOKEN in call_url + # overview is parameterless: no pagination params + assert "page[offset]" not in call_url + assert "page[limit]" not in call_url + + +def test_overview_symbol_stripped(mock_session): + _mock_response(mock_session, {"data": {}, "meta": {}, "links": {}}) + api = _make_api(mock_session) + api.get_sec_filings_overview(api_token=TOKEN, symbol=" MSFT.US ") + + call_url = mock_session.get.call_args[0][0] + assert "/sec-filings/MSFT.US?" in call_url + + +def test_overview_missing_symbol(mock_session): + api = _make_api(mock_session) + with pytest.raises(ValueError): + api.get_sec_filings_overview(api_token=TOKEN, symbol="") + + +def test_overview_none_symbol(mock_session): + api = _make_api(mock_session) + with pytest.raises(ValueError): + api.get_sec_filings_overview(api_token=TOKEN, symbol=None) + + +# ------------------------------------------------------------------------ 10-K + +def test_10k_url_and_defaults(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + api.get_sec_filings_10k(api_token=TOKEN, symbol="AAPL.US") + + call_url = mock_session.get.call_args[0][0] + assert "/sec-filings/AAPL.US/10k" in call_url + assert "page[offset]=0" in call_url + assert "page[limit]=20" in call_url + + +def test_10k_custom_pagination(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + api.get_sec_filings_10k(api_token=TOKEN, symbol="AAPL.US", page_offset=40, page_limit=100) + + call_url = mock_session.get.call_args[0][0] + assert "page[offset]=40" in call_url + assert "page[limit]=100" in call_url + + +def test_10k_missing_symbol(mock_session): + api = _make_api(mock_session) + with pytest.raises(ValueError): + api.get_sec_filings_10k(api_token=TOKEN, symbol="") + + +# ------------------------------------------------------------------------ 10-Q + +def test_10q_url_and_defaults(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + api.get_sec_filings_10q(api_token=TOKEN, symbol="AAPL.US") + + call_url = mock_session.get.call_args[0][0] + assert "/sec-filings/AAPL.US/10q" in call_url + assert "page[offset]=0" in call_url + assert "page[limit]=20" in call_url + + +def test_10q_custom_pagination(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + api.get_sec_filings_10q(api_token=TOKEN, symbol="MSFT.US", page_offset=20, page_limit=50) + + call_url = mock_session.get.call_args[0][0] + assert "/sec-filings/MSFT.US/10q" in call_url + assert "page[offset]=20" in call_url + assert "page[limit]=50" in call_url + + +# ------------------------------------------------------------------------- 8-K + +def test_8k_url_and_defaults(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + api.get_sec_filings_8k(api_token=TOKEN, symbol="AAPL.US") + + call_url = mock_session.get.call_args[0][0] + assert "/sec-filings/AAPL.US/8k" in call_url + assert "page[offset]=0" in call_url + assert "page[limit]=20" in call_url + + +def test_8k_custom_pagination(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + api.get_sec_filings_8k(api_token=TOKEN, symbol="TSLA.US", page_offset=10, page_limit=5) + + call_url = mock_session.get.call_args[0][0] + assert "/sec-filings/TSLA.US/8k" in call_url + assert "page[offset]=10" in call_url + assert "page[limit]=5" in call_url + + +# --------------------------------------------------------------- pagination bounds + +def test_pagination_limit_too_high(mock_session): + api = _make_api(mock_session) + with pytest.raises(ValueError): + api.get_sec_filings_10k(api_token=TOKEN, symbol="AAPL.US", page_limit=101) + + +def test_pagination_negative_offset(mock_session): + api = _make_api(mock_session) + with pytest.raises(ValueError): + api.get_sec_filings_10q(api_token=TOKEN, symbol="AAPL.US", page_offset=-1) + + +def test_pagination_limit_zero(mock_session): + api = _make_api(mock_session) + with pytest.raises(ValueError): + api.get_sec_filings_8k(api_token=TOKEN, symbol="AAPL.US", page_limit=0) + + +# --------------------------------------------------- apiclient facade delegation + +def test_apiclient_facade_delegates(): + from eodhd.apiclient import APIClient + + client = APIClient(TOKEN) + session = MagicMock() + _mock_response(session) + client._session = session + + client.get_sec_filings_overview("AAPL.US") + assert "/sec-filings/AAPL.US?" in session.get.call_args[0][0] + + client.get_sec_filings_10k("AAPL.US", page_offset=0, page_limit=20) + assert "/sec-filings/AAPL.US/10k" in session.get.call_args[0][0] + + client.get_sec_filings_10q("AAPL.US") + assert "/sec-filings/AAPL.US/10q" in session.get.call_args[0][0] + + client.get_sec_filings_8k("AAPL.US") + assert "/sec-filings/AAPL.US/8k" in session.get.call_args[0][0]