···11# purple-cli
2233-CLI tool to retrieve information from Twitch.
33+`purple-cli` is a tool to retrieve information from Twitch, in a format that can be
44+used to integrate with other scripts or applications.
55+66+It requires to have an application created in Twitch, with a client id and a client
77+secret that will be use to obtain the necessary credentials.
4859## Settings
61077-This project requires the following environment variables to work:
1111+This tool requires the following environment variables to work:
812913- `PURPLE_CLIENT_ID` Twitch application client id.
1014- `PURPLE_CLIENT_SECRET` Twitch application client secret.
+1-1
pyproject.toml
···11[project]
22name = "purple-cli"
33dynamic = ["version"]
44-description = "Add your description here"
44+description = "Tool to obtain information from your Twitch account.:"
55readme = "README.md"
66requires-python = ">=3.13"
77dependencies = [
+62-41
src/purple/api.py
···1010logger = logging.getLogger(__name__)
111112121313-def twitch_api_client(access_token: str) -> httpx.Client:
1414- """Create a simple client to access Twitch API."""
1515- base_url = "https://api.twitch.tv"
1616- headers = {
1717- "Client-ID": settings.client_id,
1818- "Authorization": f"Bearer {access_token}",
1919- }
2020- return httpx.Client(base_url=base_url, headers=headers)
1313+# Common
1414+# --------------------------------------------------------------------------------------
211522162317def raise_for_status(response: httpx.Response) -> None:
···3024 response.raise_for_status()
312532262727+# Twitch ID
2828+# --------------------------------------------------------------------------------------
2929+3030+3131+def twitch_id_client() -> httpx.Client:
3232+ """Create a simple client to access Twitch ID API."""
3333+ base_url = "https://id.twitch.tv"
3434+ return httpx.Client(base_url=base_url)
3535+3636+3337def retrieve_token(code: str) -> dict:
3438 """Retrieve the token information from Twitch using the given code."""
3539 logger.debug(f"Obtaining token information using code {code}")
3636- token_url = "https://id.twitch.tv/oauth2/token"
3740 params = {
3841 "client_id": settings.client_id,
3942 "client_secret": settings.client_secret,
···4144 "grant_type": "authorization_code",
4245 "redirect_uri": settings.redirect_uri,
4346 }
4444-4545- response = httpx.post(token_url, params=params)
4646- raise_for_status(response)
4747+ with twitch_id_client() as client:
4848+ response = client.post("/oauth2/token", params=params)
4949+ raise_for_status(response)
47504851 return response.json()
4952···6063 return f"{authorize_url}?{urllib.parse.urlencode(params)}"
616462656666+def validate_access_token(access_token: str) -> bool:
6767+ """Validate the given access token."""
6868+ headers = {"Authorization": f"OAuth {access_token}"}
6969+ with twitch_id_client() as client:
7070+ response = client.get("/oauth2/validate", headers=headers)
7171+ return response.status_code == 200
7272+7373+7474+def refresh_access_token(refresh_token: str) -> dict:
7575+ """Refresh the access token."""
7676+ params = {
7777+ "grant_type": "refresh_token",
7878+ "refresh_token": refresh_token,
7979+ "client_id": settings.client_id,
8080+ "client_secret": settings.client_secret,
8181+ }
8282+8383+ with twitch_id_client() as client:
8484+ response = client.post("/oauth2/token", params=params)
8585+ raise_for_status(response)
8686+8787+ data = response.json()
8888+ return data
8989+9090+9191+# Twitch API
9292+# --------------------------------------------------------------------------------------
9393+9494+9595+def twitch_api_client(access_token: str) -> httpx.Client:
9696+ """Create a simple client to access Twitch API."""
9797+ base_url = "https://api.twitch.tv"
9898+ headers = {
9999+ "Client-ID": settings.client_id,
100100+ "Authorization": f"Bearer {access_token}",
101101+ }
102102+ return httpx.Client(base_url=base_url, headers=headers)
103103+104104+63105def retrieve_user(access_token: str) -> dict:
64106 """Retrieve the user information."""
65107 with twitch_api_client(access_token) as client:
···109151 return followed
110152111153112112-def retireve_followed_streams(access_token: str, user_id: str) -> list[dict]:
154154+def retrieve_followed_streams(access_token: str, user_id: str) -> list[dict]:
113155 """Retrieve the list of followed streams."""
114156 page_size = 100
115115- params = {
157157+ params: dict[str, str | int] = {
116158 "user_id": user_id,
117159 "first": page_size,
118160 }
119161120162 streams: list[dict] = []
121121- total: int | None = None
163163+ first_query: bool = True
164164+ cursor: str | None = None
122165123123- while total is None or len(streams) > total:
166166+ while first_query or cursor:
124167 with twitch_api_client(access_token) as client:
125125- response = client.get("/helix/streams/followed")
168168+ response = client.get("/helix/streams/followed", params=params)
126169 raise_for_status(response)
127170 data = response.json()
128171129129- if total is None:
130130- total = data["total"]
172172+ if first_query:
173173+ first_query = False
131174132175 streams.extend(data["data"])
133176···136179 params["after"] = cursor
137180138181 return streams
139139-140140-141141-def validate_access_token(access_token: str) -> bool:
142142- """Validate the given access token."""
143143- headers = {"Authorization": f"OAuth {access_token}"}
144144- response = httpx.get("https://id.twitch.tv/oauth2/validate", headers=headers)
145145- return response.status_code == 200
146146-147147-148148-def refresh_access_token(refresh_token: str) -> dict:
149149- """Refresh the access token."""
150150- params = {
151151- "grant_type": "refresh_token",
152152- "refresh_token": refresh_token,
153153- "client_id": settings.client_id,
154154- "client_secret": settings.client_secret,
155155- }
156156- response = httpx.post("https://id.twitch.tv/oauth2/token", params=params)
157157- raise_for_status(response)
158158-159159- data = response.json()
160160- return data
+1-1
src/purple/auth.py
···4545 self.access_token_complete = token_data
4646 else:
4747 logger.error("Access token not found in response.")
4848- return web.Response(text="⚠️ Error, no se encuentra el parametro code.")
4848+ return web.Response(text="⚠️ Error, access token can't be obtained.")
49495050 # event to close the server
5151 logger.debug("Sending event to close web server...")