1+ import asyncio
2+ from typing import Optional , Dict , Any
3+
4+ import httpx
5+
6+ from .exceptions import SWAPIError , SWAPIAuthError
7+
8+
9+ class SWAPIClient :
10+ """
11+ SWAPIClient — async, auto-login, auto-retry po 401.
12+
13+ Funkcje:
14+ - login() — pozyskanie tokena
15+ - _request() — jedno miejsce dla GET/POST/PATCH/DELETE
16+ - auto-refresh tokena przy 401 (bez refresh tokenów — ponowne logowanie)
17+ - concurrency-safe dzięki asyncio.Lock
18+ - obsługa JSON + błędów
19+ """
20+
21+ def __init__ (
22+ self ,
23+ base_url : str ,
24+ username : str ,
25+ password : str ,
26+ client_id : str ,
27+ auth_token : str ,
28+ timeout : float = 20.0 ,
29+ ):
30+ self .base_url = base_url .rstrip ("/" )
31+ self .username = username
32+ self .password = password
33+ self .client_id = client_id
34+ self .auth_token = auth_token
35+ self .timeout = timeout
36+
37+ self .token : Optional [str ] = None
38+ self ._login_lock = asyncio .Lock () # tylko jeden login naraz
39+
40+ # --------------------------------------------------------
41+ # LOGIN
42+ # --------------------------------------------------------
43+ async def login (self ) -> str :
44+ """
45+ Logowanie do SWAPI. Zakładamy endpoint /_/security/login:
46+
47+ POST /_/security/login
48+ { "username": "...", "password": "..." }
49+ -> { "token": "..." }
50+
51+ Dostosuj jeśli endpoint działa inaczej.
52+ """
53+ url = f"{ self .base_url } /_/security/login"
54+
55+ async with httpx .AsyncClient (timeout = self .timeout ) as client :
56+ resp = await client .post (url , json = {
57+ "clientId" : self .client_id ,
58+ "authToken" : self .auth_token ,
59+ "login" : self .username ,
60+ "password" : self .password ,
61+ })
62+
63+ if resp .status_code >= 400 :
64+ raise SWAPIAuthError (
65+ f"Login failed ({ resp .status_code } ): { resp .text } "
66+ )
67+
68+ data = resp .json ()
69+ token = data .get ("token" )
70+ if not token :
71+ raise SWAPIAuthError ("Login response missing 'token' field" )
72+
73+ self .token = token
74+ return token
75+
76+ # --------------------------------------------------------
77+ # HEADERS
78+ # --------------------------------------------------------
79+ async def _headers (self ) -> Dict [str , str ]:
80+ headers = {
81+ "Accept" : "application/json" ,
82+ }
83+ if self .token :
84+ headers ["Authorization" ] = f"Bearer { self .token } "
85+ return headers
86+
87+ # --------------------------------------------------------
88+ # GŁÓWNY REQUEST
89+ # --------------------------------------------------------
90+ async def _request (self , method : str , endpoint : str , ** kwargs ) -> Dict [str , Any ]:
91+ """
92+ - Dodaje base_url
93+ - Dodaje Authorization
94+ - Jeśli token jest None → login()
95+ - Jeśli 401 → retry z loginem
96+ """
97+ url = f"{ self .base_url } { endpoint } "
98+
99+ async with httpx .AsyncClient (timeout = self .timeout ) as client :
100+
101+ # 1) Jeśli pierwszy request → login()
102+ if not self .token :
103+ async with self ._login_lock :
104+ if not self .token : # drugi request czekał na lock
105+ await self .login ()
106+
107+ # 2) Pierwsze podejście
108+ resp = await client .request (
109+ method ,
110+ url ,
111+ headers = await self ._headers (),
112+ ** kwargs
113+ )
114+
115+ # 3) Jeśli token wygasł → ponawiamy login
116+ if resp .status_code == 401 :
117+ async with self ._login_lock :
118+ await self .login ()
119+
120+ # 4) Retry z nowym tokenem
121+ resp = await client .request (
122+ method ,
123+ url ,
124+ headers = await self ._headers (),
125+ ** kwargs
126+ )
127+
128+ # 5) Obsługa błędów
129+ if resp .status_code >= 400 :
130+ raise SWAPIError (
131+ f"SWAPI HTTP { resp .status_code } on { method } { url } : { resp .text } "
132+ )
133+
134+ # 6) OBSŁUGA JSON
135+ if not resp .text :
136+ return {}
137+ try :
138+ return resp .json ()
139+ except Exception :
140+ raise SWAPIError (
141+ f"Invalid JSON response from SWAPI: { resp .text } "
142+ )
143+
144+ # --------------------------------------------------------
145+ # PUBLIC API METHODS
146+ # --------------------------------------------------------
147+ async def get (self , endpoint : str , params : dict = None ) -> Dict [str , Any ]:
148+ return await self ._request ("GET" , endpoint , params = params )
149+
150+ async def post (self , endpoint : str , json : dict = None ) -> Dict [str , Any ]:
151+ return await self ._request ("POST" , endpoint , json = json )
152+
153+ async def patch (self , endpoint : str , json : dict = None ) -> Dict [str , Any ]:
154+ return await self ._request ("PATCH" , endpoint , json = json )
155+
156+ async def delete (self , endpoint : str , params : dict = None ) -> Dict [str , Any ]:
157+ return await self ._request ("DELETE" , endpoint , params = params )
0 commit comments