-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfsapi_simple.py
More file actions
117 lines (87 loc) · 3.86 KB
/
Copy pathfsapi_simple.py
File metadata and controls
117 lines (87 loc) · 3.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""footballsoccerapi.com — the simple version.
One file, three functions, no classes, nothing to install. Drop it next to
your script or notebook and import it.
from fsapi_simple import fsapi_get
matches = fsapi_get("/v1/matches", country="England", limit=5)
The package in footballsoccerapi/ does more — typed exceptions, cursor
walking, batch chunking — and you should use it if you are building something
that has to keep working. This is for the other case: a notebook, a script, a
quick check of whether the data is what you need before committing to it.
Set your key at the top, or in the environment as FSAPI_KEY.
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Dict, Iterator, Optional
# Your key. Or leave it empty and set FSAPI_KEY in the environment.
FSAPI_KEY = ""
BASE = "https://api.footballsoccerapi.com"
def fsapi_get(path: str, **params: Any) -> Any:
"""Ask for something. Returns the data.
>>> fsapi_get("/v1/matches", country="England", limit=5)
"""
return fsapi_call(path, **params)["data"]
def fsapi_call(path: str, **params: Any) -> Dict[str, Any]:
"""The same, keeping the meta.
The meta says how old the archive is and how many rows matched, which is
worth having when you are about to quote a number at somebody.
"""
key = FSAPI_KEY or os.environ.get("FSAPI_KEY", "")
if not key:
raise RuntimeError(
"No API key. Either set FSAPI_KEY at the top of fsapi_simple.py, or put it "
"in the environment:\n"
" FSAPI_KEY=fsa_live_... python your-script.py\n"
"A free key takes a minute: https://footballsoccerapi.com/free-key"
)
clean = {
k: ("true" if v is True else "false" if v is False else v)
for k, v in params.items()
if v is not None and v != ""
}
url = BASE + "/" + path.lstrip("/")
if clean:
url += "?" + urllib.parse.urlencode(clean)
req = urllib.request.Request(
url, headers={"X-API-Key": key, "Accept": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=30) as res:
payload = json.loads(res.read().decode("utf-8"))
except urllib.error.HTTPError as err:
try:
detail = json.loads(err.read().decode("utf-8")).get("error", {})
except Exception:
# Not JSON means something other than the API answered — a proxy,
# or a challenge page. Worth saying, it points at the right layer.
raise RuntimeError(
f"The API returned {err.code} without a JSON body. Something between "
"you and it may have intercepted the request."
) from err
message = detail.get("message", f"Request failed with {err.code}")
# The API says what plan a refusal needs, so repeat it rather than
# leaving a bare 403.
if "needs_plan" in detail:
message += (f"\nNeeds the {detail['needs_plan']} plan; "
f"you have {detail.get('your_plan', 'none')}.")
raise RuntimeError(message) from err
except urllib.error.URLError as err:
raise RuntimeError(f"Could not reach the API: {err.reason}") from err
return {"data": payload.get("data"), "meta": payload.get("meta", {})}
def fsapi_walk(path: str, **params: Any) -> Iterator[Dict[str, Any]]:
"""Every row matching the filters, paging for you.
Uses the cursor, so a whole season streams without holding it in memory and
without you counting pages.
"""
cursor: Optional[str] = None
params.setdefault("limit", 500)
while True:
res = fsapi_call(path, cursor=cursor, **params)
for row in res["data"] or []:
yield row
cursor = res["meta"].get("next_cursor")
if not cursor:
return