Skip to content

Commit fc226fc

Browse files
Merge pull request vuejs#5 from American-Soccer-Analysis/python-xg
Adding get_games function to Python package
2 parents 3a160eb + 05f7324 commit fc226fc

File tree

14 files changed

+183
-80
lines changed

14 files changed

+183
-80
lines changed

.github/workflows/R-check-release.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
# For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag.
22
# https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions
33
name: R-CMD-check
4-
on: [push]
4+
on:
5+
push:
6+
paths:
7+
- "R-package/"
8+
- ".github/workflows/R-check-release.yml"
59

610
jobs:
711
R-CMD-check:
8-
runs-on: macOS-latest
12+
runs-on: macos-latest
913
steps:
1014
- name: Check out repository code
1115
uses: actions/checkout@v2

.github/workflows/python-tests.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
name: Python tests
22

3-
on: [push]
3+
on:
4+
push:
5+
paths:
6+
- "python-package/"
7+
- ".github/workflows/python-tests.yml"
48

59
jobs:
610
python:
@@ -20,9 +24,6 @@ jobs:
2024
python -m pip install --upgrade pip
2125
cd python-package && pip install --editable .
2226
if [ -f requirements.dev.txt ]; then pip install -r requirements.dev.txt; fi
23-
- name: Test with pytest
24-
run: |
25-
pytest python-package/tests/
2627
- name: Behave tests
2728
run: |
2829
cd python-package/tests

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
# itscalledsoccer
22

3-
R and Python packages that wrap the ASA API
3+
R and Python packages that wrap the American Soccer Analysis API.
4+
5+
See the READMEs in the respective package directories for usage information:
6+
7+
- [Python](python-package/README.md)
8+
- [R](R-package/README.md)

python-package/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [Stadia](#stadia)
1414
- [Managers](#managers)
1515
- [Referees](#referees)
16+
- [Games](#games)
1617
- [Maintainers](#maintainers)
1718
- [Contributing](#contributing)
1819
- [License](#license)
@@ -41,6 +42,8 @@ asa = AmericanSoccerAnalysis()
4142

4243
### Referees
4344

45+
### Games
46+
4447
## Maintainers
4548

4649
## Contributing

python-package/itscalledsoccer/client.py

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import requests
2-
from typing import Dict, List, Any, Union
2+
from typing import Dict, List, Union
33
from cachecontrol import CacheControl
44
from fuzzywuzzy import fuzz, process
55
import pandas as pd
@@ -12,6 +12,7 @@ class AmericanSoccerAnalysis:
1212
API_VERSION = "v1"
1313
BASE_URL = f"https://app.americansocceranalysis.com/api/{API_VERSION}/"
1414
LEAGUES = ["nwsl", "mls", "uslc", "usl1", "nasl"]
15+
MAX_API_LIMIT = 1000
1516

1617
def __init__(self) -> None:
1718
"""Class constructor"""
@@ -103,27 +104,28 @@ def _convert_names_to_ids(
103104
ids.append(self._convert_name_to_id(type, n))
104105
return ids
105106

106-
def _check_leagues(self, leagues: Union[str, List[str]]):
107+
def _check_leagues(self, leagues: Union[str, List[str]]) -> None:
107108
"""Validates the leagues parameter
108109
109110
:param leagues: league abbreviation or list of league abbreviations
110111
"""
111112
if isinstance(leagues, list):
112-
if not all(l in leagues for l in self.LEAGUES):
113-
print(
114-
f"Leagues are limited only to the following options: {self.LEAGUES.join(',')}."
115-
)
116-
exit()
113+
for l in leagues:
114+
if l not in self.LEAGUES:
115+
print(
116+
f"Leagues are limited only to the following options: {self.LEAGUES}."
117+
)
118+
raise SystemExit(1)
117119
else:
118120
if leagues not in self.LEAGUES:
119121
print(
120-
f"Leagues are limited only to the following options: {self.LEAGUES.join(',')}."
122+
f"Leagues are limited only to the following options: {self.LEAGUES}."
121123
)
122-
exit()
124+
raise SystemExit(1)
123125

124126
def _check_ids_names(
125127
self, ids: Union[str, List[str]], names: Union[str, List[str]]
126-
):
128+
) -> None:
127129
"""Makes sure only ids or names are passed to a function and verifies
128130
they are the right data type.
129131
@@ -132,17 +134,17 @@ def _check_ids_names(
132134
"""
133135
if ids and names:
134136
print("Please specify only IDs or names, not both.")
135-
exit()
137+
raise SystemExit(1)
136138

137139
if ids:
138140
if not isinstance(ids, str) and not isinstance(ids, list):
139141
print("IDs must be passed as a string or list of strings.")
140-
exit()
142+
raise SystemExit(1)
141143

142144
if names:
143145
if not isinstance(names, str) and not isinstance(names, list):
144146
print("Names must be passed as a string or list of names.")
145-
exit()
147+
raise SystemExit(1)
146148

147149
def _filter_entity(
148150
self,
@@ -151,7 +153,7 @@ def _filter_entity(
151153
leagues: Union[str, List[str]],
152154
ids: Union[str, List[str]] = None,
153155
names: Union[str, List[str]] = None,
154-
) -> List[Dict[str, Any]]:
156+
) -> pd.DataFrame:
155157
"""Filters a dataframe based on the arguments given.
156158
157159
:param entity_all: a dataframe containing the complete set of data
@@ -180,20 +182,53 @@ def _filter_entity(
180182
if converted_ids:
181183
entity = entity[entity[f"{entity_type}_id"].isin(converted_ids)]
182184

183-
return entity.to_json(orient="records")
185+
return entity
186+
187+
def _execute_query(self, url: str, params: Dict[str,List[str]]) -> pd.DataFrame:
188+
"""Executes a query while handling the max number of responses from the API
189+
190+
:param url: the API endpoint to call
191+
:param params: URL query strings
192+
:returns: Dataframe
193+
"""
194+
temp_response = self._single_request(url, params)
195+
response = temp_response
196+
197+
if (isinstance(response, pd.DataFrame)):
198+
offset = self.MAX_API_LIMIT
199+
200+
while(len(temp_response) == self.MAX_API_LIMIT):
201+
params["offset"] = offset
202+
temp_response = self._execute_query(url, params)
203+
response = response.append(temp_response)
204+
offset = offset + self.MAX_API_LIMIT
205+
206+
return response
207+
208+
def _single_request(self, url: str, params: Dict[str, List[str]]) -> pd.DataFrame:
209+
"""Handles single call to the API
210+
211+
:param url: the API endpoint to call
212+
:param params: URL query strings
213+
:returns: Dataframe
214+
"""
215+
response = self.session.get(url=url, params=params)
216+
response.raise_for_status()
217+
resp_df = pd.read_json(json.dumps(response.json()))
218+
return resp_df
184219

185220
def get_stadia(
186221
self,
187222
leagues: Union[str, List[str]],
188223
ids: Union[str, List[str]] = None,
189224
names: Union[str, List[str]] = None,
190-
) -> List[Dict[str, Any]]:
225+
) -> pd.DataFrame:
191226
"""Get information associated with stadia
192227
193228
:param leagues: league abbreviation or a list of league abbreviations
194229
:param ids: a single stadium id or a list of stadia ids (optional)
195230
:param names: a single stadium name or a list of stadia names (optional)
196-
:returns: list of dictionaries
231+
:returns: Dataframe
197232
"""
198233
stadia = self._filter_entity(self.stadia, "stadium", leagues, ids, names)
199234
return stadia
@@ -203,13 +238,13 @@ def get_referees(
203238
leagues: Union[str, List[str]],
204239
ids: Union[str, List[str]] = None,
205240
names: Union[str, List[str]] = None,
206-
) -> List[Dict[str, Any]]:
241+
) -> pd.DataFrame:
207242
"""Get information associated with referees
208243
209244
:param leagues: league abbreviation or a list of league abbreviations
210245
:param ids: a single referee id or a list of referee ids (optional)
211246
:param names: a single referee name or a list of referee names (optional)
212-
:returns: list of dictionaries
247+
:returns: Dataframe
213248
"""
214249
referees = self._filter_entity(self.referees, "referee", leagues, ids, names)
215250
return referees
@@ -219,13 +254,13 @@ def get_managers(
219254
leagues: Union[str, List[str]],
220255
ids: Union[str, List[str]] = None,
221256
names: Union[str, List[str]] = None,
222-
) -> List[Dict[str, Any]]:
257+
) -> pd.DataFrame:
223258
"""Get information associated with managers
224259
225260
:param leagues: league abbreviation or a list of league abbreviations
226261
:param ids: a single referee id or a list of referee ids (optional)
227262
:param names: a single referee name or a list of referee names (optional)
228-
:returns: list of dictionaries
263+
:returns: Dataframe
229264
"""
230265
managers = self._filter_entity(self.managers, "manager", leagues, ids, names)
231266
return managers
@@ -235,13 +270,13 @@ def get_teams(
235270
leagues: Union[str, List[str]],
236271
ids: Union[str, List[str]] = None,
237272
names: Union[str, List[str]] = None,
238-
) -> List[Dict[str, Any]]:
273+
) -> pd.DataFrame:
239274
"""Get information associated with teams
240275
241276
:param leagues: league abbreviation or a list of league abbreviations
242277
:param ids: a single team id or a list of team ids (optional)
243278
:param names: a single team name or a list of team names (optional)
244-
:returns: list of dictionaries
279+
:returns: Dataframe
245280
"""
246281
teams = self._filter_entity(self.teams, "team", leagues, ids, names)
247282
return teams
@@ -251,13 +286,63 @@ def get_players(
251286
leagues: Union[str, List[str]],
252287
ids: Union[str, List[str]] = None,
253288
names: Union[str, List[str]] = None,
254-
) -> List[Dict[str, Any]]:
289+
) -> pd.DataFrame:
255290
"""Get information associated with players
256291
257292
:param league: league abbreviation or a list of league abbreviations
258293
:param ids: a single player id or a list of player ids (optional)
259294
:param names: a single player name or a list of player names (optional)
260-
:returns: list of dictionaries
295+
:returns: Dataframe
261296
"""
262297
players = self._filter_entity(self.players, "player", leagues, ids, names)
263298
return players
299+
300+
def get_games(
301+
self,
302+
leagues: Union[str, List[str]],
303+
game_ids: Union[str, List[str]] = None,
304+
team_ids: Union[str, List[str]] = None,
305+
team_names: Union[str, List[str]] = None,
306+
seasons: Union[str, List[str]] = None,
307+
stages: Union[str, List[str]] = None,
308+
) -> pd.DataFrame:
309+
"""Get information related to games
310+
311+
:param leagues: league abbreviation or a list of league abbreviations
312+
:param game_ids: a single game id or a list of game ids
313+
:param team_ids: a single team id or a list of team ids
314+
:param team_names: a single team name or a list of team names
315+
:param seasons: a single year of a league season or a list of years
316+
:param stages: a single stage of competition in which a game took place or list of stages
317+
:returns: Dataframe
318+
"""
319+
self._check_leagues(leagues)
320+
self._check_ids_names(team_ids, team_names)
321+
322+
query = {}
323+
324+
if game_ids:
325+
query["game_id"] = game_ids
326+
if team_names:
327+
query["team_id"] = self._convert_names_to_ids("team",team_names)
328+
if team_ids:
329+
query["team_id"] = team_ids
330+
if seasons:
331+
query["season_name"] = seasons
332+
if stages:
333+
query["stage_name"] = stages
334+
335+
games = pd.DataFrame([])
336+
if isinstance(leagues, str):
337+
games_url = f"{self.base_url}{leagues}/games"
338+
response = self._execute_query(games_url, query)
339+
340+
games = games.append(response)
341+
elif isinstance(leagues, list):
342+
for league in leagues:
343+
games_url = f"{self.base_url}{league}/games"
344+
response = self._execute_query(games_url, query)
345+
346+
games = games.append(response)
347+
348+
return games.sort_values(by=["date_time_utc"], ascending=False)

python-package/requirements.dev.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ CacheControl==0.12.6
33
fuzzywuzzy==0.18.0
44
python-Levenshtein==0.12.2
55
behave==1.2.6
6-
pytest==6.2.4
7-
pandas==1.3.1
6+
pandas==1.3.1
7+
black==21.11.b1

python-package/setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from distutils.core import setup
1+
from setuptools import setup
22

33
with open("README.md", "r") as f:
44
long_description = f.read()
55

66
setup(
77
name="itscalledsoccer",
88
version="0.0.1",
9-
description="Programmatically interact with the ASA API",
9+
description="Programmatically interact with the American Soccer Analysis API",
1010
long_description=long_description,
1111
author="American Soccer Analysis",
1212
author_email="[email protected]",

python-package/tests/__init__.py

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Feature: Client
2+
3+
Scenario Outline: When I create a client, it has metadata
4+
Given I have an ASA client
5+
Then the API_VERSION should be "v1"
6+
And the BASE_URL should be "https://app.americansocceranalysis.com/api/v1/"
7+
And the MAX_API_LIMIT should be "1000"
8+
And "<league>" should be in LEAGUES
9+
10+
Examples: Leagues
11+
| league |
12+
| nwsl |
13+
| mls |
14+
| uslc |
15+
| usl1 |
16+
| nasl |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from itscalledsoccer.client import AmericanSoccerAnalysis
2+
from unittest.mock import patch, Mock
3+
4+
5+
@patch.object(AmericanSoccerAnalysis, "_get_all", return_value={})
6+
def before_all(context, mock_all_ids: Mock):
7+
context.soccer = AmericanSoccerAnalysis()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from behave import *
2+
3+
4+
@given("I have an ASA client")
5+
def step_impl(context):
6+
pass
7+
8+
9+
@then(u'the API_VERSION should be "{value}"')
10+
def step_impl(context, value):
11+
assert context.soccer.API_VERSION == value
12+
13+
14+
@then(u'the BASE_URL should be "{value}"')
15+
def step_impl(context, value):
16+
assert context.soccer.BASE_URL == value
17+
18+
@then(u'the MAX_API_LIMIT should be "{value}"')
19+
def step_impl(context, value):
20+
assert context.soccer.MAX_API_LIMIT == int(value)
21+
22+
23+
@then(u'"{league}" should be in LEAGUES')
24+
def step_impl(context, league):
25+
print(context.soccer.LEAGUES)
26+
assert league in context.soccer.LEAGUES

0 commit comments

Comments
 (0)