From ff93fcc7fa6b2bc6882de73ec7dadc4e453e7d78 Mon Sep 17 00:00:00 2001 From: niallsemple Date: Fri, 14 Aug 2026 01:56:16 +0100 Subject: [PATCH] add get_scores_and_broadcast to in play service Adds support for the scoresAndBroadcast endpoint which returns broadcast (tv/radio/bfLiveVideo) and matchInfo data alongside scores, as requested in #569. - InPlayService.get_scores_and_broadcast() following the existing get_scores() pattern - Broadcasts, RadioBroadcast, LiveVideoBroadcast and MatchInfo resources, exposed as optional Scores.broadcasts / Scores.match_info - Tests and mock data covering both presence and absence of the new fields --- betfairlightweight/endpoints/inplayservice.py | 30 +++++++ .../resources/inplayserviceresources.py | 44 ++++++++++ docs/endpoints.md | 3 + tests/resources/scoresandbroadcast.json | 82 +++++++++++++++++++ tests/test_inplayservice.py | 25 ++++++ tests/test_inplayserviceresources.py | 31 +++++++ 6 files changed, 215 insertions(+) create mode 100644 tests/resources/scoresandbroadcast.json diff --git a/betfairlightweight/endpoints/inplayservice.py b/betfairlightweight/endpoints/inplayservice.py index a4b75a1e..1db7df9d 100644 --- a/betfairlightweight/endpoints/inplayservice.py +++ b/betfairlightweight/endpoints/inplayservice.py @@ -100,6 +100,36 @@ def get_scores( response_json, resources.Scores, elapsed_time, lightweight ) + def get_scores_and_broadcast( + self, + event_ids: list, + session: requests.Session = None, + lightweight: bool = None, + ) -> Union[list, List[resources.Scores]]: + """ + Returns a list of scores, including broadcast and + match info data, based on event id's supplied. + + :param list event_ids: List of event id's to return + :param requests.session session: Requests session object + :param bool lightweight: If True will return dict not a resource + + :rtype: list[resources.Scores] + """ + url = "%s%s" % (self.url, "scoresAndBroadcast") + params = { + "eventIds": ",".join(str(x) for x in event_ids), + "alt": "json", + "regionCode": "UK", + "locale": "en_GB", + } + (response, response_json, elapsed_time) = self.request( + params=params, session=session, url=url + ) + return self.process_response( + response_json, resources.Scores, elapsed_time, lightweight + ) + def request( self, method: str = None, diff --git a/betfairlightweight/resources/inplayserviceresources.py b/betfairlightweight/resources/inplayserviceresources.py index 577f9c87..7863d715 100644 --- a/betfairlightweight/resources/inplayserviceresources.py +++ b/betfairlightweight/resources/inplayserviceresources.py @@ -172,6 +172,44 @@ def __init__( self.wide = wide +class RadioBroadcast: + def __init__(self, url=None): + self.url = url + + +class LiveVideoBroadcast: + def __init__(self, startTime=None, endTime=None, channel=None): + self.start_time = BaseResource.strip_datetime(startTime) + self.end_time = BaseResource.strip_datetime(endTime) + self.channel = channel + + +class Broadcasts: + def __init__( + self, + tv=None, + radio=None, + bfLiveVideo=None, + isLiveVideoAvailable=None, + isDataVisualizationAvailable=None, + isPaddockViewAvailable=None, + channel=None, + ): + self.tv = tv if tv else [] + self.radio = RadioBroadcast(**radio) if radio else None + self.bf_live_video = LiveVideoBroadcast(**bfLiveVideo) if bfLiveVideo else None + self.is_live_video_available = isLiveVideoAvailable + self.is_data_visualization_available = isDataVisualizationAvailable + self.is_paddock_view_available = isPaddockViewAvailable + self.channel = channel + + +class MatchInfo: + def __init__(self, surface=None, numberOfSets=None): + self.surface = surface + self.number_of_sets = numberOfSets + + class Scores(BaseResource): def __init__(self, **kwargs): super(Scores, self).__init__(**kwargs) @@ -196,3 +234,9 @@ def __init__(self, **kwargs): if kwargs.get("stateOfBall") else None ) + self.broadcasts = ( + Broadcasts(**kwargs.get("broadcasts")) if kwargs.get("broadcasts") else None + ) + self.match_info = ( + MatchInfo(**kwargs.get("matchInfo")) if kwargs.get("matchInfo") else None + ) diff --git a/docs/endpoints.md b/docs/endpoints.md index dd5ca0e7..c065ee44 100644 --- a/docs/endpoints.md +++ b/docs/endpoints.md @@ -158,6 +158,9 @@ trading.in_play_service.get_event_timelines() ```python trading.in_play_service.get_scores() ``` +```python +trading.in_play_service.get_scores_and_broadcast() +``` ### Race Card diff --git a/tests/resources/scoresandbroadcast.json b/tests/resources/scoresandbroadcast.json new file mode 100644 index 00000000..1e39e53c --- /dev/null +++ b/tests/resources/scoresandbroadcast.json @@ -0,0 +1,82 @@ +{ + "eventTypeId": 2, + "eventId": 34392141, + "score": { + "home": { + "name": "Player One", + "score": "1", + "halfTimeScore": "", + "fullTimeScore": "", + "penaltiesScore": "", + "penaltiesSequence": [], + "games": "3", + "sets": "1", + "numberOfYellowCards": 0, + "numberOfRedCards": 0, + "numberOfCards": 0, + "numberOfCorners": 0, + "numberOfCornersFirstHalf": 0, + "numberOfCornersSecondHalf": 0, + "bookingPoints": 0, + "isServing": true, + "playerSeed": "4", + "serviceBreaks": 1 + }, + "away": { + "name": "Player Two", + "score": "0", + "halfTimeScore": "", + "fullTimeScore": "", + "penaltiesScore": "", + "penaltiesSequence": [], + "games": "2", + "sets": "0", + "numberOfYellowCards": 0, + "numberOfRedCards": 0, + "numberOfCards": 0, + "numberOfCorners": 0, + "numberOfCornersFirstHalf": 0, + "numberOfCornersSecondHalf": 0, + "bookingPoints": 0, + "isServing": false, + "playerSeed": "12", + "serviceBreaks": 0 + }, + "numberOfYellowCards": 0, + "numberOfRedCards": 0, + "numberOfCards": 0, + "numberOfCorners": 0, + "numberOfCornersFirstHalf": 0, + "numberOfCornersSecondHalf": 0, + "bookingPoints": 0 + }, + "timeElapsed": 34, + "elapsedRegularTime": 34, + "timeElapsedSeconds": 12, + "fullTimeElapsed": { + "hour": 0, + "min": 34, + "sec": 12 + }, + "status": "InProgress", + "matchStatus": "FirstSet", + "broadcasts": { + "tv": [], + "radio": { + "url": "http://radio.betfair.com" + }, + "bfLiveVideo": { + "startTime": "2024-03-26T16:00:00.000Z", + "endTime": "2024-03-26T21:00:00.000Z", + "channel": "http://livevideo.betfair.com/Default.do?mi=226657935" + }, + "isLiveVideoAvailable": true, + "isDataVisualizationAvailable": false, + "isPaddockViewAvailable": false, + "channel": "WEB" + }, + "matchInfo": { + "surface": "", + "numberOfSets": "3" + } +} diff --git a/tests/test_inplayservice.py b/tests/test_inplayservice.py index 89112862..98d2c2ea 100644 --- a/tests/test_inplayservice.py +++ b/tests/test_inplayservice.py @@ -93,6 +93,31 @@ def test_get_scores(self, mock_request, mock_process_response): assert mock_request.call_count == 1 assert mock_process_response.call_count == 1 + @mock.patch( + "betfairlightweight.endpoints.inplayservice.InPlayService.process_response" + ) + @mock.patch( + "betfairlightweight.endpoints.inplayservice.InPlayService.request", + return_value=(mock.Mock(), mock.Mock(), 1.3), + ) + def test_get_scores_and_broadcast(self, mock_request, mock_process_response): + event_ids = [12345] + params = { + "eventIds": ",".join(str(x) for x in event_ids), + "alt": "json", + "regionCode": "UK", + "locale": "en_GB", + } + self.in_play_service.get_scores_and_broadcast(event_ids) + + mock_request.assert_called_with( + url="https://ips.betfair.com/inplayservice/v1.1/scoresAndBroadcast", + session=None, + params=params, + ) + assert mock_request.call_count == 1 + assert mock_process_response.call_count == 1 + @mock.patch("betfairlightweight.endpoints.inplayservice.check_status_code") @mock.patch("betfairlightweight.endpoints.inplayservice.InPlayService.headers") @mock.patch("betfairlightweight.baseclient.requests.get") diff --git a/tests/test_inplayserviceresources.py b/tests/test_inplayserviceresources.py index b26b92d1..b95b3d34 100644 --- a/tests/test_inplayserviceresources.py +++ b/tests/test_inplayserviceresources.py @@ -12,6 +12,37 @@ def test_scores(self): assert isinstance(resource, resources.Scores) assert resource.event_type_id == 1 + def test_scores_and_broadcast(self): + mock_response = create_mock_json("tests/resources/scoresandbroadcast.json") + resource = resources.Scores(**mock_response.json()) + + assert isinstance(resource, resources.Scores) + assert resource.event_type_id == 2 + + assert resource.broadcasts is not None + assert resource.broadcasts.tv == [] + assert resource.broadcasts.radio.url == "http://radio.betfair.com" + assert resource.broadcasts.bf_live_video.start_time is not None + assert resource.broadcasts.bf_live_video.end_time is not None + assert resource.broadcasts.bf_live_video.channel == ( + "http://livevideo.betfair.com/Default.do?mi=226657935" + ) + assert resource.broadcasts.is_live_video_available is True + assert resource.broadcasts.is_data_visualization_available is False + assert resource.broadcasts.is_paddock_view_available is False + assert resource.broadcasts.channel == "WEB" + + assert resource.match_info is not None + assert resource.match_info.surface == "" + assert resource.match_info.number_of_sets == "3" + + def test_scores_without_broadcast(self): + mock_response = create_mock_json("tests/resources/scores.json") + resource = resources.Scores(**mock_response.json()) + + assert resource.broadcasts is None + assert resource.match_info is None + def test_event_timeline(self): mock_response = create_mock_json("tests/resources/eventtimeline.json") resource = resources.EventTimeline(**mock_response.json())