refactor: enhance error handling and logging in API interactions, improve exception classes

This commit is contained in:
deesiigneer
2026-02-01 14:57:16 +00:00
parent e22a22b777
commit 83d4308906
9 changed files with 669 additions and 112 deletions

View File

@@ -1,13 +1,53 @@
"""pyspapi - API wrapper for SP servers written in Python
TODO: заполнить описание"""
"""
SPWorlds API Wrapper
~~~~~~~~~~~~~~~~~~~
High-level client for interacting with the SPWorlds API.
:copyright: (c) 2022-present deesiigneer
:license: MIT, see LICENSE for more details.
"""
import importlib.metadata
from .spworlds import SPAPI
__all__ = [SPAPI]
from pyspapi.exceptions import (
BadRequestError,
ClientError,
ForbiddenError,
HTTPError,
InsufficientBalanceError,
NetworkError,
NotFoundError,
RateLimitError,
ServerError,
SPAPIError,
TimeoutError,
UnauthorizedError,
ValidationError,
)
from pyspapi.spworlds import SPAPI
__all__ = [
"SPAPI",
"BadRequestError",
"ClientError",
"ForbiddenError",
"HTTPError",
"InsufficientBalanceError",
"NetworkError",
"NotFoundError",
"RateLimitError",
"ServerError",
"SPAPIError",
"TimeoutError",
"UnauthorizedError",
"ValidationError",
]
__title__: str = "pyspapi"
__author__: str = "deesiigneer"
__url__: str = "https://github.com/deesiigneer/pyspapi"
__description__: str = "API wrapper for SP servers written in Python."
__license__: str = "MIT"
__url__: str = "https://github.com/deesiigneer/pyspapi"
__copyright__: str = "2022-present deesiigneer"
__version__: str = importlib.metadata.version("pyspapi")

View File

@@ -1,3 +1,3 @@
from .api import APISession
__all__ = [APISession]
__all__ = ["APISession"]

View File

@@ -1,14 +1,31 @@
import asyncio
import json
from base64 import b64encode
from logging import getLogger
from typing import Optional, Any, Dict
from logging import NullHandler, getLogger
from typing import Any, Dict, Optional
import aiohttp
from ..exceptions import ValidationError, SPAPIError
from pyspapi.exceptions import (
BadRequestError,
ClientError,
ForbiddenError,
HTTPError,
InsufficientBalanceError,
NetworkError,
NotFoundError,
RateLimitError,
ServerError,
SPAPIError,
UnauthorizedError,
ValidationError,
)
from pyspapi.exceptions import (
TimeoutError as APITimeoutError,
)
log = getLogger("pyspapi")
log.addHandler(NullHandler())
class APISession(object):
@@ -22,6 +39,8 @@ class APISession(object):
raise_exception: bool = False,
proxy: str = None,
):
self._validate_credentials(card_id, token)
self.__url = "https://spworlds.ru/api/public/"
self.__id = card_id
self.__token = token
@@ -31,61 +50,294 @@ class APISession(object):
self.__raise_exception = raise_exception
self.__proxy = proxy
self.session: Optional[aiohttp.ClientSession] = None
self._session_owner = False
@staticmethod
def _validate_credentials(card_id: str, token: str) -> None:
if not card_id or not isinstance(card_id, str):
raise ValueError("card_id must be a non-empty string")
if not token or not isinstance(token, str):
raise ValueError("token must be a non-empty string")
async def __aenter__(self):
print("proxy=", self.__proxy)
try:
if not self.session:
self.session = aiohttp.ClientSession(
json_serialize=json.dumps,
timeout=aiohttp.ClientTimeout(total=self.__timeout),
proxy=self.__proxy,
)
self._session_owner = True
log.debug(f"[pyspapi] Session created with timeout={self.__timeout}s")
else:
self._session_owner = False
except Exception as e:
log.error(f"[pyspapi] Failed to create session: {e}")
raise
return self
async def __aexit__(self, *err):
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self._session_owner and self.session:
try:
await self.session.close()
log.debug("[pyspapi] Session closed")
except Exception as e:
log.error(f"[pyspapi] Error closing session: {e}")
self.session = None
return False
def _get_auth_header(self) -> str:
credentials = f"{self.__id}:{self.__token}"
encoded = b64encode(credentials.encode("utf-8")).decode("utf-8")
return f"Bearer {encoded}"
def _get_headers(self) -> Dict[str, str]:
return {
"Authorization": self._get_auth_header(),
"User-Agent": "https://github.com/deesiigneer/pyspapi",
"Content-Type": "application/json",
}
def _parse_error_response(self, content: str) -> Dict[str, Any]:
try:
return json.loads(content)
except json.JSONDecodeError:
return {"raw_response": content}
def _format_error_message(
self, error_data: Dict[str, Any], status_code: int
) -> str:
message = (
error_data.get("message")
or error_data.get("detail")
or error_data.get("msg")
or f"HTTP {status_code}"
)
if "error" in error_data:
message = f"{message} (error: {error_data['error']})"
return message
def _log_error_with_details(
self,
method: str,
endpoint: str,
status_code: int,
error_data: Dict[str, Any],
content: str,
) -> None:
message = self._format_error_message(error_data, status_code)
log.error(
f"[pyspapi] HTTP {status_code}: {method.upper()} {endpoint} | {message}"
)
def _should_retry(self, status_code: int, attempt: int) -> bool:
if attempt > self.__retries:
return False
return status_code in {408, 429, 500, 502, 503, 504}
async def _handle_http_error(
self,
method: str,
endpoint: str,
status_code: int,
content: str,
) -> None:
error_data = self._parse_error_response(content)
self._log_error_with_details(method, endpoint, status_code, error_data, content)
if not self.__raise_exception:
return
error_message = self._format_error_message(error_data, status_code)
if status_code == 400:
error_code = error_data.get("error", "")
if "notEnoughBalance" in error_code:
raise InsufficientBalanceError(details=error_data)
raise BadRequestError(details=error_data)
elif status_code == 401:
raise UnauthorizedError(details=error_data)
elif status_code == 403:
raise ForbiddenError(details=error_data)
elif status_code == 404:
raise NotFoundError(resource=endpoint, details=error_data)
elif status_code == 422:
raise ValidationError(error_data)
elif status_code == 429:
retry_after = error_data.get("retry_after")
raise RateLimitError(retry_after=retry_after, details=error_data)
elif 400 <= status_code < 500:
raise ClientError(
status_code=status_code,
message=error_message,
response_body=content,
details=error_data,
)
elif 500 <= status_code < 600:
raise ServerError(
status_code=status_code,
message=error_message,
response_body=content,
details=error_data,
)
else:
raise HTTPError(
status_code=status_code,
message=error_message,
response_body=content,
details=error_data,
)
async def request(
self, method: str, endpoint: str, data: Optional[Dict] = None
) -> Any:
url = self.__url + endpoint
headers = {
"Authorization": f"Bearer {str(b64encode(str(f'{self.__id}:{self.__token}').encode('utf-8')), 'utf-8')}",
"User-Agent": "https://github.com/deesiigneer/pyspapi",
"Content-Type": "application/json",
}
headers = self._get_headers()
attempt = 0
while True:
attempt += 1
if attempt > 1:
log.warning(
f"[pyspapi] Repeat attempt {attempt}: {method.upper()} {url}"
f"[pyspapi] Retry attempt {attempt}/{self.__retries + 1}: {method.upper()} {endpoint}"
)
try:
async with self.session.request(
method, url, json=data, headers=headers
) as resp:
response_text = await resp.text()
if resp.status == 422:
errors = await resp.json()
log.error(f"[pyspapi] Validation error: {errors}")
try:
errors = json.loads(response_text)
except json.JSONDecodeError:
errors = {"raw_response": response_text}
error_msg = self._format_error_message(errors, 422)
log.error(
f"[pyspapi] Validation error (422): {method.upper()} {endpoint} | {error_msg}"
)
if self.__raise_exception:
raise ValidationError(errors)
return None
if resp.status >= 400:
content = await resp.text()
log.error(f"[pyspapi] API error {resp.status}: {content}")
if self.__raise_exception:
raise SPAPIError(resp.status, content)
await self._handle_http_error(
method, endpoint, resp.status, response_text
)
if self._should_retry(resp.status, attempt):
await asyncio.sleep(self.__sleep_timeout * attempt)
continue
return None
try:
return await resp.json()
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
log.exception(f"[pyspapi] Connection error: {e} \n attempt {attempt}")
if attempt > self.__retries:
except json.JSONDecodeError as e:
log.error(
f"[pyspapi] Failed to parse JSON response: {e} | Status: {resp.status}"
)
if self.__raise_exception:
raise SPAPIError(
status_code=resp.status,
message="Invalid JSON in response",
details={
"error": str(e),
"response": response_text[:500],
},
)
return None
except asyncio.TimeoutError:
log.warning(
f"[pyspapi] Request timeout ({self.__timeout}s): {method.upper()} {endpoint} | Attempt {attempt}/{self.__retries + 1}"
)
if self._should_retry(408, attempt):
await asyncio.sleep(self.__sleep_timeout * attempt)
continue
log.error("[pyspapi] Max retries reached for timeout")
if self.__raise_exception:
raise APITimeoutError(
timeout=self.__timeout,
endpoint=endpoint,
details={"method": method, "attempt": attempt},
)
return None
except aiohttp.ClientSSLError as e:
log.error(f"[pyspapi] SSL error: {e} | {method.upper()} {endpoint}")
if self.__raise_exception:
raise NetworkError(
message=f"SSL error: {str(e)}",
details={
"method": method,
"endpoint": endpoint,
"error": str(e),
},
)
return None
except (aiohttp.ClientConnectorError, aiohttp.ClientOSError) as e:
log.warning(
f"[pyspapi] Connection error: {e} | {method.upper()} {endpoint} | Attempt {attempt}/{self.__retries + 1}"
)
if self._should_retry(0, attempt):
await asyncio.sleep(self.__sleep_timeout * attempt)
continue
log.error("[pyspapi] Max retries reached for connection error")
if self.__raise_exception:
raise NetworkError(
message=f"Connection error: {str(e)}",
details={
"method": method,
"endpoint": endpoint,
"error": str(e),
"attempt": attempt,
},
)
return None
except aiohttp.ClientError as e:
log.error(f"[pyspapi] Client error: {e} | {method.upper()} {endpoint}")
if self.__raise_exception:
raise NetworkError(
message=f"HTTP client error: {str(e)}",
details={
"method": method,
"endpoint": endpoint,
"error": str(e),
},
)
return None
except SPAPIError:
raise
except Exception as e:
log.exception(
f"[pyspapi] Unexpected error: {e} | {method.upper()} {endpoint}"
)
if self.__raise_exception:
raise SPAPIError(
message=f"Unexpected error: {str(e)}",
details={
"error": str(e),
"method": method,
"endpoint": endpoint,
},
)
return None
await asyncio.sleep(self.__sleep_timeout)
async def get(self, endpoint: str) -> Any:
async with self:

View File

@@ -1,25 +1,197 @@
from typing import Any, Dict, Optional
class SPAPIError(Exception):
"""
Базовая ошибка для всех исключений, связанных с API SPWorlds.
"""
def __init__(self, status_code: int, message: str):
def __init__(
self,
status_code: Optional[int] = None,
message: str = "",
details: Optional[Dict[str, Any]] = None,
):
self.status_code = status_code
self.message = message
super().__init__(f"[{status_code}] {message}")
self.details = details or {}
error_msg = f"[{status_code}] {message}" if status_code else message
super().__init__(error_msg)
def __str__(self):
return f"SPAPIError: [{self.status_code}] {self.message}"
if self.status_code:
return f"{self.__class__.__name__}: [{self.status_code}] {self.message}"
return f"{self.__class__.__name__}: {self.message}"
def __repr__(self):
return f"{self.__class__.__name__}(status_code={self.status_code}, message={self.message!r}, details={self.details!r})"
class ValidationError(SPAPIError):
"""
Ошибка валидации.
Ошибка валидации (HTTP 422).
"""
def __init__(self, errors):
def __init__(self, errors: Dict[str, Any]):
self.errors = errors
super().__init__(422, f"Validation failed: {errors}")
super().__init__(
status_code=422,
message="Validation failed",
details={"validation_errors": errors},
)
def __str__(self):
return f"ValidationError: {self.errors}"
return f"{self.__class__.__name__}: {self.errors}"
class NetworkError(SPAPIError):
"""
Ошибка сетевого соединения.
"""
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
super().__init__(message=message, details=details)
class TimeoutError(SPAPIError):
"""
Ошибка истечения времени ожидания.
"""
def __init__(
self,
timeout: float,
endpoint: str = "",
details: Optional[Dict[str, Any]] = None,
):
msg = f"Request timeout after {timeout}s"
if endpoint:
msg += f" for endpoint: {endpoint}"
super().__init__(
message=msg, details=details or {"timeout": timeout, "endpoint": endpoint}
)
class HTTPError(SPAPIError):
"""
Ошибка HTTP (4xx, 5xx).
"""
def __init__(
self,
status_code: int,
message: str = "",
response_body: str = "",
details: Optional[Dict[str, Any]] = None,
):
self.response_body = response_body
super().__init__(
status_code=status_code,
message=message or f"HTTP {status_code}",
details=details or {"response_body": response_body},
)
class ClientError(HTTPError):
"""
Ошибка клиента (4xx).
"""
pass
class ServerError(HTTPError):
"""
Ошибка сервера (5xx).
"""
pass
class RateLimitError(ClientError):
"""
Превышен лимит запросов (HTTP 429).
"""
def __init__(
self,
retry_after: Optional[int] = None,
details: Optional[Dict[str, Any]] = None,
):
self.retry_after = retry_after
msg = "Rate limit exceeded"
if retry_after:
msg += f". Retry after {retry_after}s"
super().__init__(
status_code=429,
message=msg,
details=details or {"retry_after": retry_after},
)
class UnauthorizedError(ClientError):
"""
Ошибка аутентификации (HTTP 401).
"""
def __init__(self, details: Optional[Dict[str, Any]] = None):
super().__init__(
status_code=401,
message="Unauthorized. Invalid or missing credentials.",
details=details,
)
class ForbiddenError(ClientError):
"""
Ошибка доступа (HTTP 403).
"""
def __init__(self, details: Optional[Dict[str, Any]] = None):
super().__init__(
status_code=403,
message="Forbidden. Access denied.",
details=details,
)
class NotFoundError(ClientError):
"""
Ресурс не найден (HTTP 404).
"""
def __init__(self, resource: str = "", details: Optional[Dict[str, Any]] = None):
msg = "Resource not found"
if resource:
msg += f": {resource}"
super().__init__(
status_code=404,
message=msg,
details=details or {"resource": resource},
)
class BadRequestError(ClientError):
"""
Некорректный запрос (HTTP 400).
"""
def __init__(self, details: Optional[Dict[str, Any]] = None):
super().__init__(
status_code=400,
message="Bad request. Invalid request parameters.",
details=details,
)
class InsufficientBalanceError(ClientError):
"""
Недостаточно средств на счете.
"""
def __init__(self, details: Optional[Dict[str, Any]] = None):
super().__init__(
status_code=400,
message="Insufficient balance. Not enough funds to complete the transaction.",
details=details or {"error": "error.public.transactions.notEnoughBalance"},
)

View File

@@ -1,9 +1,10 @@
from base64 import b64encode
from hashlib import sha256
from hmac import new, compare_digest
from hmac import compare_digest, new
from typing import Optional
from .api import APISession
from pyspapi.api import APISession
from pyspapi.exceptions import InsufficientBalanceError
from pyspapi.types import User
from pyspapi.types.me import Account
from pyspapi.types.payment import Item
@@ -44,7 +45,7 @@ class SPAPI(APISession):
:type retries: int
:param raise_exception: Поднимать исключения при ошибке, если True.
:type raise_exception: bool
:param proxy: Прокся!
:param proxy: Прокси для подключения к API. По умолчанию None.
:type proxy: str
"""
super().__init__(
@@ -67,7 +68,15 @@ class SPAPI(APISession):
:return: Текущий баланс карты.
:rtype: int
"""
return int((await super().get("card"))["balance"])
try:
response = await super().get("card")
if response is None:
return None
return int(response.get("balance", 0))
except (KeyError, ValueError, TypeError) as e:
log = __import__("logging").getLogger("pyspapi")
log.error(f"Failed to parse balance response: {e}")
return None
@property
async def webhook(self) -> Optional[str]:
@@ -77,7 +86,15 @@ class SPAPI(APISession):
:return: URL вебхука.
:rtype: str
"""
return str((await super().get("card"))["webhook"])
try:
response = await super().get("card")
if response is None:
return None
return str(response.get("webhook", ""))
except (KeyError, ValueError, TypeError) as e:
log = __import__("logging").getLogger("pyspapi")
log.error(f"Failed to parse webhook response: {e}")
return None
@property
async def me(self) -> Optional[Account]:
@@ -87,17 +104,25 @@ class SPAPI(APISession):
:return: Объект Account, представляющий аккаунт текущего пользователя.
:rtype: :class:`Account`
"""
try:
me = await super().get("accounts/me")
if me is None:
return None
return Account(
account_id=me["id"],
username=me["username"],
minecraftuuid=me["minecraftUUID"],
status=me["status"],
roles=me["roles"],
cities=me["cities"],
cards=me["cards"],
created_at=me["createdAt"],
account_id=me.get("id"),
username=me.get("username"),
minecraftuuid=me.get("minecraftUUID"),
status=me.get("status"),
roles=me.get("roles", []),
cities=me.get("cities", []),
cards=me.get("cards", []),
created_at=me.get("createdAt"),
)
except (KeyError, TypeError) as e:
log = __import__("logging").getLogger("pyspapi")
log.error(f"Failed to parse account response: {e}")
return None
async def get_user(self, discord_id: int) -> Optional[User]:
"""
@@ -109,11 +134,22 @@ class SPAPI(APISession):
:return: Объект User, представляющий пользователя.
:rtype: :class:`User`
"""
if not discord_id:
raise ValueError("discord_id must be a non-empty integer")
try:
user = await super().get(f"users/{discord_id}")
if user:
if user is None:
return None
cards = await super().get(f"accounts/{user['username']}/cards")
if cards is None:
cards = []
return User(user["username"], user["uuid"], cards)
else:
except (KeyError, TypeError) as e:
log = __import__("logging").getLogger("pyspapi")
log.error(f"Failed to parse user response: {e}")
return None
async def create_transaction(
@@ -132,9 +168,27 @@ class SPAPI(APISession):
:return: Баланс после транзакции.
:rtype: int
"""
data = {"receiver": receiver, "amount": amount, "comment": comment}
if not receiver:
raise ValueError("receiver must be a non-empty string")
if not isinstance(amount, int) or amount <= 0:
raise ValueError("amount must be a positive integer")
return int((await super().post("transactions", data))["balance"])
try:
data = {"receiver": receiver, "amount": amount, "comment": comment}
response = await super().post("transactions", data)
if response is None:
return None
return int(response.get("balance", 0))
except (KeyError, ValueError, TypeError) as e:
log = __import__("logging").getLogger("pyspapi")
log.error(f"Failed to create transaction: {e}")
return None
except InsufficientBalanceError as ibe:
log = __import__("logging").getLogger("pyspapi")
log.error(f"Insufficient balance for transaction: {ibe}")
return None
async def create_payment(
self, webhook_url: str, redirect_url: str, data: str, items: list[Item]
@@ -153,14 +207,29 @@ class SPAPI(APISession):
:return: URL для платежа или None при ошибке.
:rtype: str
"""
data = {
if not webhook_url or not redirect_url:
raise ValueError("webhook_url and redirect_url must be non-empty strings")
if not items or len(items) == 0:
raise ValueError("items must contain at least one item")
try:
payload = {
"items": items,
"redirectUrl": redirect_url,
"webhookUrl": webhook_url,
"data": data,
}
return str((await super().post("payments", data))["url"])
response = await super().post("payments", payload)
if response is None:
return None
return str(response.get("url", ""))
except (KeyError, ValueError, TypeError) as e:
log = __import__("logging").getLogger("pyspapi")
log.error(f"Failed to create payment: {e}")
return None
async def update_webhook(self, url: str) -> Optional[dict]:
"""
@@ -169,9 +238,21 @@ class SPAPI(APISession):
:param url: Новый URL вебхука.
:return: Ответ API в виде словаря или None при ошибке.
"""
data = {"url": url}
if not url:
raise ValueError("url must be a non-empty string")
return await super().put("card/webhook", data)
try:
data = {"url": url}
response = await super().put("card/webhook", data)
if response is None:
return None
return response
except (KeyError, TypeError) as e:
log = __import__("logging").getLogger("pyspapi")
log.error(f"Failed to update webhook: {e}")
return None
def webhook_verify(self, data: str, header: str) -> bool:
"""

View File

@@ -1,5 +1,5 @@
from .me import Account
from .payment import Item
from .users import Cards, User
from pyspapi.types.me import Account
from pyspapi.types.payment import Item
from pyspapi.types.users import Cards, User
__all__ = [Account, Item, Cards, User]
__all__ = ["Account", "Item", "Cards", "User"]

View File

@@ -1,6 +1,16 @@
class City:
def __init__(self, city_id=None, name=None, x=None, z=None, nether_x=None, nether_z=None, lane=None, role=None,
created_at=None):
def __init__(
self,
city_id=None,
name=None,
x=None,
z=None,
nether_x=None,
nether_z=None,
lane=None,
role=None,
created_at=None,
):
self._id = city_id
self._name = name
self._x = x
@@ -48,11 +58,7 @@ class City:
return self._created_at
def __repr__(self):
return (
f"City(id={self._id}, name={self._name}, x={self._x}, z={self._z}, "
f"nether_x={self._nether_x}, nether_z={self._nether_z}, lane={self._lane}, role={self._role}, "
f"created_at={self._created_at})"
)
return f"<{self.__class__.__name__}(id={self._id!r}, name={self._name!r}, lane={self._lane!r}, role={self._role!r})>"
class Card:
@@ -79,11 +85,21 @@ class Card:
return self._color
def __repr__(self):
return f"Card(id={self._id}, name={self._name}, number={self._number}, color={self._color})"
return f"<{self.__class__.__name__}(id={self._id!r}, name={self._name!r}, number={self._number!r})>"
class Account:
def __init__(self, account_id, username, minecraftuuid, status, roles, created_at, cards, cities):
def __init__(
self,
account_id,
username,
minecraftuuid,
status,
roles,
created_at,
cards,
cities,
):
self._id = account_id
self._username = username
self._minecraftuuid = minecraftuuid
@@ -91,15 +107,15 @@ class Account:
self._roles = roles
self._cities = [
City(
city_id=city['city_id'],
name=city['name'],
x=city['x'],
z=city['z'],
nether_x=city['nether_x'],
nether_z=city['nether_z'],
lane=city['lane'],
role=city['role'],
created_at=city['created_at'],
city_id=city["city"]["id"],
name=city["city"]["name"],
x=city["city"]["x"],
z=city["city"]["z"],
nether_x=city["city"]["netherX"],
nether_z=city["city"]["netherZ"],
lane=city["city"]["lane"],
role=city["role"],
created_at=city["createdAt"],
)
for city in cities
]
@@ -147,6 +163,7 @@ class Account:
return self._created_at
def __repr__(self):
return (f"Account(id={self._id}, username={self._username}, minecraftUUID={self._minecraftuuid}, "
f"status={self._status}, roles={self._roles}, cities={self._cities}, cards={self._cards}, "
f"created_at={self._created_at})")
return (
f"<{self.__class__.__name__}(id={self._id!r}, username={self._username!r}, status={self._status!r}, "
f"roles={self._roles}, cities={self._cities}, cards={self._cards})>"
)

View File

@@ -9,10 +9,13 @@ class Item:
def name(self):
return self._name
def __repr__(self):
return f"<{self.__class__.__name__}(name={self._name!r}, count={self._count!r}, price={self._price!r}, comment={self._comment!r})>"
def to_json(self):
return {
"name": self._name,
"count": self._count,
"price": self._price,
"comment": self._comment
"comment": self._comment,
}

View File

@@ -1,5 +1,4 @@
class Cards:
class UserCards:
def __init__(self, name, number):
self._name: str = name
self._number: str = number
@@ -12,20 +11,16 @@ class Cards:
def number(self):
return self._number
# def __repr__(self):
# return "%s(%s)" % (
# self.__class__.__name__,
# self.__dict__
# )
def __repr__(self):
return f"<{self.__class__.__name__}(id={self._id!r}, name={self._name!r}, number={self._number!r})>"
class User:
def __init__(self, username, uuid, cards):
self._username: str = username
self._uuid: str = uuid
self._cards = [
Cards(
UserCards(
name=card["name"],
number=card["number"],
)
@@ -45,7 +40,4 @@ class User:
return self._cards
def __repr__(self):
return "%s(%s)" % (
self.__class__.__name__,
self.__dict__
)
return "%s(%s)" % (self.__class__.__name__, self.__dict__)