coincircuit 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .DS_Store
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: coincircuit
3
+ Version: 0.2.0
4
+ Summary: Official Python SDK for the CoinCircuit API
5
+ License-Expression: MIT
6
+ Keywords: blockchain,coincircuit,crypto,payments,x402
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.8
19
+ Requires-Dist: httpx>=0.24.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
22
+ Requires-Dist: pytest>=7.0; extra == 'dev'
23
+ Requires-Dist: respx>=0.20; extra == 'dev'
@@ -0,0 +1,91 @@
1
+ # coincircuit
2
+
3
+ Official CoinCircuit Python SDK. Supports both synchronous and asynchronous usage.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install coincircuit
9
+ ```
10
+
11
+ ## Usage (Sync)
12
+
13
+ ```python
14
+ from coincircuit import CoinCircuit
15
+
16
+ cc = CoinCircuit(api_key="api_live_...")
17
+
18
+ # Create a payment session
19
+ session = cc.payments.create(
20
+ amount="10.00",
21
+ currency="USD",
22
+ asset="USDC",
23
+ chain="base",
24
+ customer={"email": "buyer@example.com"},
25
+ )
26
+
27
+ # List transactions
28
+ result = cc.transactions.list(page=1, size=20)
29
+ for tx in result.data:
30
+ print(tx)
31
+
32
+ # Auto-paginate
33
+ for tx in cc.transactions.list_auto_paginate():
34
+ print(tx)
35
+
36
+ # Settle x402 gasless payment
37
+ result = cc.x402.settle(
38
+ scheme="eip3009",
39
+ chain="base",
40
+ asset="USDC",
41
+ payload={
42
+ "from": "0xSender...",
43
+ "to": "0xDeposit...",
44
+ "value": "1000000",
45
+ "validAfter": "0",
46
+ "validBefore": "1774055094",
47
+ "nonce": "0xrandom32bytes...",
48
+ "signature": "0xsigned...",
49
+ },
50
+ )
51
+ ```
52
+
53
+ ## Usage (Async)
54
+
55
+ ```python
56
+ from coincircuit import AsyncCoinCircuit
57
+
58
+ async with AsyncCoinCircuit(api_key="api_live_...") as cc:
59
+ session = await cc.payments.create(
60
+ amount="10.00",
61
+ currency="USD",
62
+ )
63
+ ```
64
+
65
+ ## Resources
66
+
67
+ | Resource | Methods |
68
+ |----------|---------|
69
+ | `cc.payments` | create, list, get, generate_deposit_address, estimate |
70
+ | `cc.x402` | settle, verify |
71
+ | `cc.invoices` | create, list, get |
72
+ | `cc.transactions` | list, get |
73
+ | `cc.payouts` | create, list, get, estimate_fees |
74
+ | `cc.balance` | get |
75
+ | `cc.blockchain` | get_supported_assets, get_confirmations, get_gas_fees |
76
+ | `cc.customers` | create, list, get |
77
+ | `cc.refunds` | create_for_session, create_for_invoice, list, get, estimate |
78
+ | `cc.settlements` | list, get |
79
+ | `cc.rates` | convert, list |
80
+ | `cc.payment_pages` | create, list, update, delete, sessions |
81
+ | `cc.bank_accounts` | create, list, get, update, delete, banks |
82
+ | `cc.crypto_addresses` | create, list, get, update, delete, validate |
83
+
84
+ ## Links
85
+
86
+ - [API Reference](https://coincircuit.io/api-reference)
87
+ - [Dashboard](https://dashboard.coincircuit.io)
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,168 @@
1
+ """CoinCircuit Python SDK.
2
+
3
+ Usage (synchronous)::
4
+
5
+ from coincircuit import CoinCircuit
6
+
7
+ cc = CoinCircuit(api_key="sk_live_...")
8
+ session = cc.payments.create(
9
+ title="Order #123",
10
+ description="Widget purchase",
11
+ amount="10.00",
12
+ currency="USD",
13
+ customer={"email": "buyer@example.com"},
14
+ asset="USDC",
15
+ chain="base",
16
+ )
17
+
18
+ Usage (asynchronous)::
19
+
20
+ from coincircuit import AsyncCoinCircuit
21
+
22
+ async_cc = AsyncCoinCircuit(api_key="sk_live_...")
23
+ session = await async_cc.payments.create(...)
24
+ """
25
+
26
+ from .client import AsyncHttpClient, HttpClient
27
+ from .errors import CoinCircuitApiError, CoinCircuitAuthError, CoinCircuitError
28
+ from .pagination import PaginatedResponse
29
+ from .resources.balance import AsyncBalanceResource, BalanceResource
30
+ from .resources.blockchain import AsyncBlockchainResource, BlockchainResource
31
+ from .resources.customers import AsyncCustomersResource, CustomersResource
32
+ from .resources.invoices import AsyncInvoicesResource, InvoicesResource
33
+ from .resources.payments import AsyncPaymentsResource, PaymentsResource
34
+ from .resources.payouts import AsyncPayoutsResource, PayoutsResource
35
+ from .resources.rates import AsyncRatesResource, RatesResource
36
+ from .resources.refunds import AsyncRefundsResource, RefundsResource
37
+ from .resources.settlements import AsyncSettlementsResource, SettlementsResource
38
+ from .resources.transactions import AsyncTransactionsResource, TransactionsResource
39
+ from .resources.x402 import AsyncX402Resource, X402Resource
40
+ from .resources.payment_pages import AsyncPaymentPagesResource, PaymentPagesResource
41
+ from .resources.bank_accounts import AsyncBankAccountsResource, BankAccountsResource
42
+ from .resources.crypto_addresses import AsyncCryptoAddressesResource, CryptoAddressesResource
43
+
44
+ __version__ = "1.0.0"
45
+
46
+ __all__ = [
47
+ # Main clients
48
+ "CoinCircuit",
49
+ "AsyncCoinCircuit",
50
+ # Errors
51
+ "CoinCircuitError",
52
+ "CoinCircuitApiError",
53
+ "CoinCircuitAuthError",
54
+ # Pagination
55
+ "PaginatedResponse",
56
+ # Version
57
+ "__version__",
58
+ ]
59
+
60
+
61
+ class CoinCircuit:
62
+ """Synchronous CoinCircuit API client.
63
+
64
+ Provides access to all CoinCircuit API resources through
65
+ namespace attributes (e.g. ``cc.payments``, ``cc.invoices``).
66
+
67
+ Args:
68
+ api_key: Your CoinCircuit API key (e.g. "sk_live_...").
69
+ base_url: API base URL. Defaults to https://api.coincircuit.io.
70
+ timeout: Request timeout in seconds. Defaults to 30.
71
+ max_retries: Max retry attempts for transient errors. Defaults to 2.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ api_key: str,
77
+ *,
78
+ base_url: str = "https://api.coincircuit.io",
79
+ timeout: float = 30.0,
80
+ max_retries: int = 2,
81
+ ) -> None:
82
+ self._client = HttpClient(
83
+ api_key=api_key,
84
+ base_url=base_url,
85
+ timeout=timeout,
86
+ max_retries=max_retries,
87
+ )
88
+
89
+ # Resource namespaces
90
+ self.payments = PaymentsResource(self._client)
91
+ self.x402 = X402Resource(self._client)
92
+ self.invoices = InvoicesResource(self._client)
93
+ self.transactions = TransactionsResource(self._client)
94
+ self.payouts = PayoutsResource(self._client)
95
+ self.balance = BalanceResource(self._client)
96
+ self.blockchain = BlockchainResource(self._client)
97
+ self.customers = CustomersResource(self._client)
98
+ self.refunds = RefundsResource(self._client)
99
+ self.settlements = SettlementsResource(self._client)
100
+ self.rates = RatesResource(self._client)
101
+ self.payment_pages = PaymentPagesResource(self._client)
102
+ self.bank_accounts = BankAccountsResource(self._client)
103
+ self.crypto_addresses = CryptoAddressesResource(self._client)
104
+
105
+ def close(self) -> None:
106
+ """Close the underlying HTTP client and release resources."""
107
+ self._client.close()
108
+
109
+ def __enter__(self) -> "CoinCircuit":
110
+ return self
111
+
112
+ def __exit__(self, *args: object) -> None:
113
+ self.close()
114
+
115
+
116
+ class AsyncCoinCircuit:
117
+ """Asynchronous CoinCircuit API client.
118
+
119
+ Provides the same resource namespaces as ``CoinCircuit`` but all
120
+ methods are coroutines.
121
+
122
+ Args:
123
+ api_key: Your CoinCircuit API key (e.g. "sk_live_...").
124
+ base_url: API base URL. Defaults to https://api.coincircuit.io.
125
+ timeout: Request timeout in seconds. Defaults to 30.
126
+ max_retries: Max retry attempts for transient errors. Defaults to 2.
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ api_key: str,
132
+ *,
133
+ base_url: str = "https://api.coincircuit.io",
134
+ timeout: float = 30.0,
135
+ max_retries: int = 2,
136
+ ) -> None:
137
+ self._client = AsyncHttpClient(
138
+ api_key=api_key,
139
+ base_url=base_url,
140
+ timeout=timeout,
141
+ max_retries=max_retries,
142
+ )
143
+
144
+ # Resource namespaces
145
+ self.payments = AsyncPaymentsResource(self._client)
146
+ self.x402 = AsyncX402Resource(self._client)
147
+ self.invoices = AsyncInvoicesResource(self._client)
148
+ self.transactions = AsyncTransactionsResource(self._client)
149
+ self.payouts = AsyncPayoutsResource(self._client)
150
+ self.balance = AsyncBalanceResource(self._client)
151
+ self.blockchain = AsyncBlockchainResource(self._client)
152
+ self.customers = AsyncCustomersResource(self._client)
153
+ self.refunds = AsyncRefundsResource(self._client)
154
+ self.settlements = AsyncSettlementsResource(self._client)
155
+ self.rates = AsyncRatesResource(self._client)
156
+ self.payment_pages = AsyncPaymentPagesResource(self._client)
157
+ self.bank_accounts = AsyncBankAccountsResource(self._client)
158
+ self.crypto_addresses = AsyncCryptoAddressesResource(self._client)
159
+
160
+ async def close(self) -> None:
161
+ """Close the underlying async HTTP client and release resources."""
162
+ await self._client.close()
163
+
164
+ async def __aenter__(self) -> "AsyncCoinCircuit":
165
+ return self
166
+
167
+ async def __aexit__(self, *args: object) -> None:
168
+ await self.close()
@@ -0,0 +1,333 @@
1
+ """Low-level HTTP client with retries, auth, and envelope unwrapping."""
2
+
3
+ import time
4
+ from typing import Any, Dict, List, Optional, Tuple, Union
5
+
6
+ import httpx
7
+
8
+ from .errors import CoinCircuitApiError, CoinCircuitAuthError, CoinCircuitError
9
+ from .pagination import PaginatedResponse
10
+
11
+ DEFAULT_BASE_URL = "https://api.coincircuit.io"
12
+ DEFAULT_TIMEOUT = 30.0
13
+ MAX_RETRIES = 2
14
+ RETRYABLE_STATUS_CODES = (429, 500, 502, 503, 504)
15
+
16
+
17
+ def _build_headers(api_key: str) -> Dict[str, str]:
18
+ return {
19
+ "x-api-key": api_key,
20
+ "Content-Type": "application/json",
21
+ "Accept": "application/json",
22
+ }
23
+
24
+
25
+ def _parse_response(response: httpx.Response) -> Any:
26
+ """Parse the response, raise on errors, unwrap the envelope."""
27
+ status = response.status_code
28
+
29
+ # Try to decode JSON body
30
+ try:
31
+ body = response.json()
32
+ except Exception:
33
+ body = {}
34
+
35
+ # Auth errors (401, 403)
36
+ if status in (401, 403):
37
+ message = body.get("message", "Authentication failed")
38
+ if isinstance(message, list):
39
+ message = "; ".join(message)
40
+ raise CoinCircuitAuthError(
41
+ message=message,
42
+ status_code=status,
43
+ body=body,
44
+ )
45
+
46
+ # Other error status codes
47
+ if status >= 400:
48
+ message = body.get("message", f"API request failed with status {status}")
49
+ if isinstance(message, list):
50
+ message = "; ".join(message)
51
+ raise CoinCircuitApiError(
52
+ message=message,
53
+ status_code=status,
54
+ body=body,
55
+ )
56
+
57
+ # Envelope unwrapping: {"success": bool, "message": str, "data": T}
58
+ # If the body matches the envelope format, return just "data".
59
+ if isinstance(body, dict) and "success" in body:
60
+ if not body.get("success", True):
61
+ raise CoinCircuitApiError(
62
+ message=body.get("message", "Request was not successful"),
63
+ status_code=status,
64
+ body=body,
65
+ )
66
+ # Return data if present, otherwise the full body
67
+ if "data" in body:
68
+ return body["data"]
69
+ return body
70
+
71
+ return body
72
+
73
+
74
+ def _parse_paginated_response(response: httpx.Response) -> PaginatedResponse:
75
+ """Parse a paginated response, returning a PaginatedResponse wrapper."""
76
+ status = response.status_code
77
+
78
+ try:
79
+ body = response.json()
80
+ except Exception:
81
+ body = {}
82
+
83
+ if status in (401, 403):
84
+ message = body.get("message", "Authentication failed")
85
+ if isinstance(message, list):
86
+ message = "; ".join(message)
87
+ raise CoinCircuitAuthError(
88
+ message=message,
89
+ status_code=status,
90
+ body=body,
91
+ )
92
+
93
+ if status >= 400:
94
+ message = body.get("message", f"API request failed with status {status}")
95
+ if isinstance(message, list):
96
+ message = "; ".join(message)
97
+ raise CoinCircuitApiError(
98
+ message=message,
99
+ status_code=status,
100
+ body=body,
101
+ )
102
+
103
+ if isinstance(body, dict) and "success" in body:
104
+ if not body.get("success", True):
105
+ raise CoinCircuitApiError(
106
+ message=body.get("message", "Request was not successful"),
107
+ status_code=status,
108
+ body=body,
109
+ )
110
+
111
+ data = body.get("data", [])
112
+ meta = body.get("meta", {"page": 1, "size": len(data), "total": len(data), "totalPages": 1})
113
+ return PaginatedResponse(data=data, meta=meta)
114
+
115
+
116
+ def _backoff_delay(attempt: int) -> float:
117
+ """Exponential backoff: 0.5s, 1.0s, 2.0s, etc."""
118
+ return 0.5 * (2 ** attempt)
119
+
120
+
121
+ class HttpClient:
122
+ """Synchronous HTTP client for the CoinCircuit API.
123
+
124
+ Handles authentication, retries with exponential backoff, and
125
+ response envelope unwrapping.
126
+ """
127
+
128
+ def __init__(
129
+ self,
130
+ api_key: str,
131
+ base_url: str = DEFAULT_BASE_URL,
132
+ timeout: float = DEFAULT_TIMEOUT,
133
+ max_retries: int = MAX_RETRIES,
134
+ ) -> None:
135
+ if not api_key:
136
+ raise CoinCircuitError("api_key is required")
137
+
138
+ self.api_key = api_key
139
+ self.base_url = base_url.rstrip("/")
140
+ self.max_retries = max_retries
141
+ self._client = httpx.Client(
142
+ base_url=self.base_url,
143
+ headers=_build_headers(api_key),
144
+ timeout=timeout,
145
+ )
146
+
147
+ def request(
148
+ self,
149
+ method: str,
150
+ path: str,
151
+ params: Optional[Dict[str, Any]] = None,
152
+ json_body: Optional[Dict[str, Any]] = None,
153
+ ) -> Any:
154
+ """Make a request, retry on transient errors, unwrap envelope."""
155
+ # Strip None values from params
156
+ if params:
157
+ params = {k: v for k, v in params.items() if v is not None}
158
+
159
+ last_error: Optional[Exception] = None
160
+ for attempt in range(self.max_retries + 1):
161
+ try:
162
+ response = self._client.request(
163
+ method=method,
164
+ url=path,
165
+ params=params,
166
+ json=json_body,
167
+ )
168
+ if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
169
+ time.sleep(_backoff_delay(attempt))
170
+ continue
171
+ return _parse_response(response)
172
+ except (CoinCircuitAuthError, CoinCircuitApiError):
173
+ raise
174
+ except httpx.HTTPError as exc:
175
+ last_error = exc
176
+ if attempt < self.max_retries:
177
+ time.sleep(_backoff_delay(attempt))
178
+ continue
179
+ raise CoinCircuitError(f"HTTP request failed: {exc}") from exc
180
+
181
+ # Should not reach here, but just in case
182
+ raise CoinCircuitError(f"Request failed after {self.max_retries + 1} attempts")
183
+
184
+ def request_paginated(
185
+ self,
186
+ method: str,
187
+ path: str,
188
+ params: Optional[Dict[str, Any]] = None,
189
+ ) -> PaginatedResponse:
190
+ """Make a request and return a PaginatedResponse."""
191
+ if params:
192
+ params = {k: v for k, v in params.items() if v is not None}
193
+
194
+ last_error: Optional[Exception] = None
195
+ for attempt in range(self.max_retries + 1):
196
+ try:
197
+ response = self._client.request(
198
+ method=method,
199
+ url=path,
200
+ params=params,
201
+ )
202
+ if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
203
+ time.sleep(_backoff_delay(attempt))
204
+ continue
205
+ return _parse_paginated_response(response)
206
+ except (CoinCircuitAuthError, CoinCircuitApiError):
207
+ raise
208
+ except httpx.HTTPError as exc:
209
+ last_error = exc
210
+ if attempt < self.max_retries:
211
+ time.sleep(_backoff_delay(attempt))
212
+ continue
213
+ raise CoinCircuitError(f"HTTP request failed: {exc}") from exc
214
+
215
+ raise CoinCircuitError(f"Request failed after {self.max_retries + 1} attempts")
216
+
217
+ def close(self) -> None:
218
+ """Close the underlying HTTP client."""
219
+ self._client.close()
220
+
221
+ def __enter__(self) -> "HttpClient":
222
+ return self
223
+
224
+ def __exit__(self, *args: Any) -> None:
225
+ self.close()
226
+
227
+
228
+ class AsyncHttpClient:
229
+ """Asynchronous HTTP client for the CoinCircuit API.
230
+
231
+ Same behaviour as ``HttpClient`` but uses ``httpx.AsyncClient``.
232
+ """
233
+
234
+ def __init__(
235
+ self,
236
+ api_key: str,
237
+ base_url: str = DEFAULT_BASE_URL,
238
+ timeout: float = DEFAULT_TIMEOUT,
239
+ max_retries: int = MAX_RETRIES,
240
+ ) -> None:
241
+ if not api_key:
242
+ raise CoinCircuitError("api_key is required")
243
+
244
+ self.api_key = api_key
245
+ self.base_url = base_url.rstrip("/")
246
+ self.max_retries = max_retries
247
+ self._client = httpx.AsyncClient(
248
+ base_url=self.base_url,
249
+ headers=_build_headers(api_key),
250
+ timeout=timeout,
251
+ )
252
+
253
+ async def request(
254
+ self,
255
+ method: str,
256
+ path: str,
257
+ params: Optional[Dict[str, Any]] = None,
258
+ json_body: Optional[Dict[str, Any]] = None,
259
+ ) -> Any:
260
+ """Make an async request, retry on transient errors, unwrap envelope."""
261
+ import asyncio
262
+
263
+ if params:
264
+ params = {k: v for k, v in params.items() if v is not None}
265
+
266
+ last_error: Optional[Exception] = None
267
+ for attempt in range(self.max_retries + 1):
268
+ try:
269
+ response = await self._client.request(
270
+ method=method,
271
+ url=path,
272
+ params=params,
273
+ json=json_body,
274
+ )
275
+ if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
276
+ await asyncio.sleep(_backoff_delay(attempt))
277
+ continue
278
+ return _parse_response(response)
279
+ except (CoinCircuitAuthError, CoinCircuitApiError):
280
+ raise
281
+ except httpx.HTTPError as exc:
282
+ last_error = exc
283
+ if attempt < self.max_retries:
284
+ await asyncio.sleep(_backoff_delay(attempt))
285
+ continue
286
+ raise CoinCircuitError(f"HTTP request failed: {exc}") from exc
287
+
288
+ raise CoinCircuitError(f"Request failed after {self.max_retries + 1} attempts")
289
+
290
+ async def request_paginated(
291
+ self,
292
+ method: str,
293
+ path: str,
294
+ params: Optional[Dict[str, Any]] = None,
295
+ ) -> PaginatedResponse:
296
+ """Make an async request and return a PaginatedResponse."""
297
+ import asyncio
298
+
299
+ if params:
300
+ params = {k: v for k, v in params.items() if v is not None}
301
+
302
+ last_error: Optional[Exception] = None
303
+ for attempt in range(self.max_retries + 1):
304
+ try:
305
+ response = await self._client.request(
306
+ method=method,
307
+ url=path,
308
+ params=params,
309
+ )
310
+ if response.status_code in RETRYABLE_STATUS_CODES and attempt < self.max_retries:
311
+ await asyncio.sleep(_backoff_delay(attempt))
312
+ continue
313
+ return _parse_paginated_response(response)
314
+ except (CoinCircuitAuthError, CoinCircuitApiError):
315
+ raise
316
+ except httpx.HTTPError as exc:
317
+ last_error = exc
318
+ if attempt < self.max_retries:
319
+ await asyncio.sleep(_backoff_delay(attempt))
320
+ continue
321
+ raise CoinCircuitError(f"HTTP request failed: {exc}") from exc
322
+
323
+ raise CoinCircuitError(f"Request failed after {self.max_retries + 1} attempts")
324
+
325
+ async def close(self) -> None:
326
+ """Close the underlying async HTTP client."""
327
+ await self._client.aclose()
328
+
329
+ async def __aenter__(self) -> "AsyncHttpClient":
330
+ return self
331
+
332
+ async def __aexit__(self, *args: Any) -> None:
333
+ await self.close()
@@ -0,0 +1,47 @@
1
+ """CoinCircuit SDK error classes."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class CoinCircuitError(Exception):
7
+ """Base error for all CoinCircuit SDK errors."""
8
+
9
+ def __init__(self, message: str) -> None:
10
+ self.message = message
11
+ super().__init__(self.message)
12
+
13
+
14
+ class CoinCircuitApiError(CoinCircuitError):
15
+ """Raised when the CoinCircuit API returns a non-success response."""
16
+
17
+ def __init__(
18
+ self,
19
+ message: str,
20
+ status_code: int,
21
+ body: Optional[Dict[str, Any]] = None,
22
+ ) -> None:
23
+ self.status_code = status_code
24
+ self.body = body or {}
25
+ super().__init__(message)
26
+
27
+ def __str__(self) -> str:
28
+ return f"CoinCircuitApiError({self.status_code}): {self.message}"
29
+
30
+ def __repr__(self) -> str:
31
+ return (
32
+ f"CoinCircuitApiError(message={self.message!r}, "
33
+ f"status_code={self.status_code!r})"
34
+ )
35
+
36
+
37
+ class CoinCircuitAuthError(CoinCircuitApiError):
38
+ """Raised when the API returns 401 Unauthorized or 403 Forbidden."""
39
+
40
+ def __str__(self) -> str:
41
+ return f"CoinCircuitAuthError({self.status_code}): {self.message}"
42
+
43
+ def __repr__(self) -> str:
44
+ return (
45
+ f"CoinCircuitAuthError(message={self.message!r}, "
46
+ f"status_code={self.status_code!r})"
47
+ )