Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions eodhd/APIs/ASXCorporateActionsAPI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# APIs/ASXCorporateActionsAPI.py

from urllib.parse import quote

from .BaseAPI import BaseAPI


class ASXCorporateActionsAPI(BaseAPI):
"""
Wrapper for the ASX Corporate Actions endpoint:

GET /api/asx-corporate-actions

Notes:
- Returns a JSON:API envelope { data, meta, links }.
meta has { total, page: { offset, limit } }; links has { next }.
- Filtering uses BARE query params (type=, symbol=, date_from=, date_to=),
NOT filter[...]. Pagination uses page[offset] and page[limit].
- data[] items vary by action type (dividends carry code/date/value/currency/
exchange/recordDate/paymentDate/period/_asx_extra with franking fields;
splits carry code/date/split/exchange; etc.).
- 1 API call per request.
"""

_TYPE_VALUES = (
"dividends",
"splits",
"bonus-issues",
"rights-issues",
"buybacks",
"capital-returns",
"spp",
"other",
)

@staticmethod
def _param(name: str, value) -> str:
"""Build a URL-encoded &name=value fragment.

Returns "" for None or blank/whitespace-only values (so an empty string
is treated as "omit the param", not "send &name="). Values are
URL-encoded; the key is a bare query param (not filter[...])."""
if value is None:
return ""
text = str(value).strip()
if text == "":
return ""
return f"&{name}={quote(text, safe='')}"

@staticmethod
def _pagination(page_offset: int = None, page_limit: int = None) -> str:
query_string = ""
if page_offset is not None:
page_offset = int(page_offset)
if page_offset < 0:
raise ValueError("page_offset must be >= 0.")
query_string += f"&page[offset]={page_offset}"
if page_limit is not None:
page_limit = int(page_limit)
if page_limit < 1 or page_limit > 1000:
raise ValueError("page_limit must be between 1 and 1000.")
query_string += f"&page[limit]={page_limit}"
return query_string

def get_corporate_actions(
self,
api_token: str,
type: str = None,
symbol: str = None,
date_from: str = None,
date_to: str = None,
page_offset: int = None,
page_limit: int = None,
):
"""
GET /api/asx-corporate-actions

Query params (bare, not filter[...]):
type - action type: "dividends" | "splits" | "bonus-issues" |
"rights-issues" | "buybacks" | "capital-returns" |
"spp" | "other"
symbol - ASX ticker with .AU suffix, e.g. "PMV.AU"
date_from - start date, YYYY-MM-DD
date_to - end date, YYYY-MM-DD
page[offset] - pagination offset (default 0)
page[limit] - pagination limit, 1-1000 (default 100)

Returns: dict envelope { data, meta, links }.
"""
if type is not None and str(type).strip().lower() not in self._TYPE_VALUES:
raise ValueError(f"type must be one of {self._TYPE_VALUES}.")

query_string = ""
if type is not None:
query_string += self._param("type", str(type).strip().lower())
query_string += self._param("symbol", symbol)
query_string += self._param("date_from", date_from)
query_string += self._param("date_to", date_to)
query_string += self._pagination(page_offset, page_limit)

return self._rest_get_method(
api_key=api_token,
endpoint="asx-corporate-actions",
uri="",
querystring=query_string,
)
1 change: 1 addition & 0 deletions eodhd/APIs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from .BulkFundamentalsAPI import BulkFundamentalsAPI
from .TreasuryAPI import TreasuryAPI
from .ExchangeDetailsV2API import ExchangeDetailsV2API
from .ASXCorporateActionsAPI import ASXCorporateActionsAPI

#Marketplace endpoints
from .MPIndexComponentsAPI import MPIndexComponentsAPI
Expand Down
30 changes: 30 additions & 0 deletions eodhd/apiclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from eodhd.APIs import BulkFundamentalsAPI
from eodhd.APIs import TreasuryAPI
from eodhd.APIs import ExchangeDetailsV2API
from eodhd.APIs import ASXCorporateActionsAPI

#Marketplace endpoints
from eodhd.APIs import MPIndexComponentsAPI
Expand Down Expand Up @@ -1586,6 +1587,35 @@ def get_treasury_real_yield_rates(self, from_date=None, to_date=None):
api_call = TreasuryAPI(session=self._session, timeout=self._timeout)
return api_call.get_treasury_real_yield_rates(api_token=self._api_key, from_date=from_date, to_date=to_date)

# ------------------------------------------------------------------
# ASX Corporate Actions API (/api/asx-corporate-actions)
# ------------------------------------------------------------------
def get_asx_corporate_actions(self, type=None, symbol=None, date_from=None,
date_to=None, page_offset=None, page_limit=None):
"""
ASX Corporate Actions
Endpoint: GET /api/asx-corporate-actions

Note: this endpoint uses bare query params, NOT filter[...].

Args:
type [OPTIONAL] - action type: "dividends"|"splits"|"bonus-issues"|
"rights-issues"|"buybacks"|"capital-returns"|
"spp"|"other"
symbol [OPTIONAL] - ASX ticker with .AU suffix, e.g. "PMV.AU"
date_from [OPTIONAL] - start date, YYYY-MM-DD
date_to [OPTIONAL] - end date, YYYY-MM-DD
page_offset [OPTIONAL] - pagination offset (default 0)
page_limit [OPTIONAL] - pagination limit, 1-1000 (default 100)
Returns: dict envelope { data, meta, links }
"""
api_call = ASXCorporateActionsAPI(session=self._session, timeout=self._timeout)
return api_call.get_corporate_actions(
api_token=self._api_key, type=type, symbol=symbol,
date_from=date_from, date_to=date_to,
page_offset=page_offset, page_limit=page_limit,
)

# ── Phase 2: Marketplace ──────────────────────────────────────

# --- InvestVerte ESG (6 methods) ---
Expand Down
142 changes: 142 additions & 0 deletions tests/test_asx_corporate_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Tests for ASXCorporateActionsAPI.

Note: the ASX corporate actions endpoint uses BARE query params
(type=, symbol=, date_from=, date_to=), NOT filter[...]. Pagination still
uses page[offset]/page[limit].
"""

import pytest
from unittest.mock import MagicMock

from eodhd.APIs.ASXCorporateActionsAPI import ASXCorporateActionsAPI


@pytest.fixture
def mock_session():
return MagicMock()


def _make_api(session):
return ASXCorporateActionsAPI(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": {"total": 0, "page": {"offset": 0, "limit": 100}}, "links": {}}
)
session.get.return_value = resp


def test_corporate_actions_url(mock_session):
_mock_response(mock_session)
api = _make_api(mock_session)
api.get_corporate_actions(api_token="test1234567890123456")

call_url = mock_session.get.call_args[0][0]
assert "/asx-corporate-actions" in call_url
assert "api_token=test1234567890123456" in call_url
assert "fmt=json" in call_url


def test_corporate_actions_bare_params(mock_session):
_mock_response(mock_session)
api = _make_api(mock_session)
api.get_corporate_actions(
api_token="test1234567890123456", type="dividends", symbol="PMV.AU",
date_from="2026-01-01", date_to="2026-12-31",
page_offset=0, page_limit=25,
)

call_url = mock_session.get.call_args[0][0]
# bare keys, not filter[...]
assert "&type=dividends" in call_url
assert "&symbol=PMV.AU" in call_url
assert "&date_from=2026-01-01" in call_url
assert "&date_to=2026-12-31" in call_url
assert "page[offset]=0" in call_url
assert "page[limit]=25" in call_url
# must NOT use filter[...] style
assert "filter[" not in call_url


def test_corporate_actions_returns_envelope(mock_session):
_mock_response(mock_session)
api = _make_api(mock_session)
result = api.get_corporate_actions(api_token="test1234567890123456")

assert "data" in result
assert "meta" in result
assert "links" in result


def test_type_normalised_lowercase(mock_session):
_mock_response(mock_session)
api = _make_api(mock_session)
api.get_corporate_actions(api_token="test1234567890123456", type="Dividends")

call_url = mock_session.get.call_args[0][0]
assert "&type=dividends" in call_url


def test_type_hyphenated_value(mock_session):
_mock_response(mock_session)
api = _make_api(mock_session)
api.get_corporate_actions(api_token="test1234567890123456", type="capital-returns")

call_url = mock_session.get.call_args[0][0]
assert "&type=capital-returns" in call_url


def test_invalid_type(mock_session):
api = _make_api(mock_session)
with pytest.raises(ValueError):
api.get_corporate_actions(api_token="test1234567890123456", type="mergers")


def test_symbol_url_encoded(mock_session):
_mock_response(mock_session)
api = _make_api(mock_session)
api.get_corporate_actions(api_token="test1234567890123456", symbol="A & B.AU")

call_url = mock_session.get.call_args[0][0]
assert "&symbol=A%20%26%20B.AU" in call_url
assert "&symbol=A & B.AU" not in call_url


def test_blank_value_omitted(mock_session):
_mock_response(mock_session)
api = _make_api(mock_session)
# empty/whitespace-only values are omitted, not sent as &symbol=
api.get_corporate_actions(api_token="test1234567890123456", symbol=" ")

call_url = mock_session.get.call_args[0][0]
assert "&symbol=" not in call_url


def test_invalid_pagination(mock_session):
api = _make_api(mock_session)
with pytest.raises(ValueError):
api.get_corporate_actions(api_token="test1234567890123456", page_offset=-1)
with pytest.raises(ValueError):
api.get_corporate_actions(api_token="test1234567890123456", page_limit=0)
with pytest.raises(ValueError):
api.get_corporate_actions(api_token="test1234567890123456", page_limit=1001)


def test_client_facade_delegates(mock_session):
_mock_response(mock_session)
from eodhd.apiclient import APIClient

client = APIClient("test1234567890123456")
client._session = mock_session
client.get_asx_corporate_actions(type="splits", symbol="BHP.AU", page_limit=10)

call_url = mock_session.get.call_args[0][0]
assert "/asx-corporate-actions" in call_url
assert "&type=splits" in call_url
assert "&symbol=BHP.AU" in call_url
assert "page[limit]=10" in call_url