11from __future__ import annotations
22
33import json
4+ import re
45import warnings
56import xml .etree .ElementTree as ET
67from http import HTTPStatus
2021 ServerError ,
2122 TransportError ,
2223 UnauthorizedError ,
24+ UserLockedOutError ,
2325)
2426from .structures import RequestArgs , RequestParams
2527from .utils import user_to_xml , xml_to_json
2628
2729try :
2830 import httpx
2931except ImportError : # pragma: no cover - depends on installed extra
30- httpx = None # type: ignore[assignment]
32+ httpx = None # type: ignore[assignment] # ty:ignore[invalid-assignment]
3133
3234try :
3335 import requests
3436except ImportError : # pragma: no cover - depends on installed extra
35- requests = None # type: ignore[assignment]
37+ requests = None # type: ignore[assignment] # ty:ignore[invalid-assignment]
3638
3739
3840class Mellophone :
3941 """Единый клиент Mellophone с синхронными и асинхронными методами."""
4042
43+ _LOCKOUT_RE = re .compile (
44+ r"locked out for too many unsuccessful login attempts\.\s*Time to unlock:\s*(\d+)\s*s\." ,
45+ re .IGNORECASE ,
46+ )
47+
4148 base_url : str
4249 token_set_settings : Optional [str ]
4350 token_user_manage : Optional [str ]
@@ -128,6 +135,13 @@ def _build_url(self, path: str, params: Optional[Dict[str, Any]] = None) -> str:
128135 clean = {k : v for k , v in params .items () if v is not None }
129136 return f"{ self .base_url } { path } ?{ urlencode (clean )} "
130137
138+ @staticmethod
139+ def _match_lockout_error (response_text : str ) -> Optional [int ]:
140+ match = Mellophone ._LOCKOUT_RE .search (response_text )
141+ if match is None :
142+ return None
143+ return int (match .group (1 ))
144+
131145 @staticmethod
132146 def _raise_for_status (response : Any ) -> None :
133147 """Выбрасывает доменное исключение для неуспешных HTTP-статусов."""
@@ -139,6 +153,9 @@ def _raise_for_status(response: Any) -> None:
139153 if status_code == HTTPStatus .UNAUTHORIZED :
140154 raise UnauthorizedError (status_code , response_text )
141155 if status_code == HTTPStatus .FORBIDDEN :
156+ unlock_in_seconds = Mellophone ._match_lockout_error (response_text )
157+ if unlock_in_seconds is not None :
158+ raise UserLockedOutError (status_code , response_text , unlock_in_seconds )
142159 raise ForbiddenError (status_code , response_text )
143160 if status_code == HTTPStatus .NOT_FOUND :
144161 raise NotFoundError (status_code , response_text )
0 commit comments