cexy 0.1.0.dev1__py3-none-any.whl
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.
- cexy/__init__.py +56 -0
- cexy/_async/__init__.py +0 -0
- cexy/_async/client.py +144 -0
- cexy/_async/pagination.py +58 -0
- cexy/_async/resources.py +478 -0
- cexy/_async/transport.py +165 -0
- cexy/_common.py +108 -0
- cexy/_decimal.py +41 -0
- cexy/_enum.py +31 -0
- cexy/_generated/__init__.py +2 -0
- cexy/_generated/models.py +1478 -0
- cexy/_generated/operations.py +183 -0
- cexy/_opmap.py +15 -0
- cexy/_ratelimit.py +72 -0
- cexy/_retry.py +38 -0
- cexy/_sync/__init__.py +1 -0
- cexy/_sync/client.py +146 -0
- cexy/_sync/pagination.py +59 -0
- cexy/_sync/resources.py +478 -0
- cexy/_sync/transport.py +167 -0
- cexy/_version.py +1 -0
- cexy/auth.py +106 -0
- cexy/errors.py +241 -0
- cexy/models.py +6 -0
- cexy/py.typed +0 -0
- cexy/ws.py +665 -0
- cexy-0.1.0.dev1.dist-info/METADATA +331 -0
- cexy-0.1.0.dev1.dist-info/RECORD +30 -0
- cexy-0.1.0.dev1.dist-info/WHEEL +4 -0
- cexy-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
cexy/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""CEXY.io Python SDK.
|
|
2
|
+
|
|
3
|
+
>>> import cexy
|
|
4
|
+
>>> client = cexy.Client() # public market data
|
|
5
|
+
>>> client.markets.orderbook("BTC/USDT", depth=10)
|
|
6
|
+
|
|
7
|
+
Private endpoints need an API key pair: ``cexy.Client(api_key=..., api_secret=...)``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from cexy._async.client import AsyncClient
|
|
11
|
+
from cexy._async.pagination import AsyncPage
|
|
12
|
+
from cexy._common import DEFAULT_BASE_URL, USER_AGENT
|
|
13
|
+
from cexy._sync.client import Client
|
|
14
|
+
from cexy._sync.pagination import Page
|
|
15
|
+
from cexy._version import __version__
|
|
16
|
+
from cexy.auth import Authenticator, HeaderKeyAuth
|
|
17
|
+
from cexy.errors import (
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
CexyApiError,
|
|
20
|
+
CexyConnectionError,
|
|
21
|
+
CexyError,
|
|
22
|
+
ConfigurationError,
|
|
23
|
+
ConflictError,
|
|
24
|
+
ForbiddenError,
|
|
25
|
+
MissingCredentialsError,
|
|
26
|
+
NotFoundError,
|
|
27
|
+
RateLimitError,
|
|
28
|
+
ServerError,
|
|
29
|
+
UnprocessableError,
|
|
30
|
+
ValidationError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"DEFAULT_BASE_URL",
|
|
35
|
+
"USER_AGENT",
|
|
36
|
+
"AsyncClient",
|
|
37
|
+
"AsyncPage",
|
|
38
|
+
"AuthenticationError",
|
|
39
|
+
"Authenticator",
|
|
40
|
+
"CexyApiError",
|
|
41
|
+
"CexyConnectionError",
|
|
42
|
+
"CexyError",
|
|
43
|
+
"Client",
|
|
44
|
+
"ConfigurationError",
|
|
45
|
+
"ConflictError",
|
|
46
|
+
"ForbiddenError",
|
|
47
|
+
"HeaderKeyAuth",
|
|
48
|
+
"MissingCredentialsError",
|
|
49
|
+
"NotFoundError",
|
|
50
|
+
"Page",
|
|
51
|
+
"RateLimitError",
|
|
52
|
+
"ServerError",
|
|
53
|
+
"UnprocessableError",
|
|
54
|
+
"ValidationError",
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
cexy/_async/__init__.py
ADDED
|
File without changes
|
cexy/_async/client.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""The async client (``AsyncClient``).
|
|
2
|
+
|
|
3
|
+
Async source; the synchronous ``cexy/_sync/client.py`` is generated from it by ``scripts/unasync.py``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Optional, Type
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from cexy._async.resources import (
|
|
14
|
+
AsyncAccount,
|
|
15
|
+
AsyncAssets,
|
|
16
|
+
AsyncExports,
|
|
17
|
+
AsyncFees,
|
|
18
|
+
AsyncMarkets,
|
|
19
|
+
AsyncNetworks,
|
|
20
|
+
AsyncPools,
|
|
21
|
+
AsyncTrading,
|
|
22
|
+
AsyncWallet,
|
|
23
|
+
)
|
|
24
|
+
from cexy._async.transport import AsyncTransport
|
|
25
|
+
from cexy._common import (
|
|
26
|
+
DEFAULT_BASE_URL,
|
|
27
|
+
DEFAULT_RATE_LIMIT_ANONYMOUS,
|
|
28
|
+
DEFAULT_RATE_LIMIT_WITH_KEY,
|
|
29
|
+
user_agent,
|
|
30
|
+
validate_base_url,
|
|
31
|
+
)
|
|
32
|
+
from cexy._generated import models as m
|
|
33
|
+
from cexy._opmap import operation
|
|
34
|
+
from cexy.auth import REDACTED, Authenticator, build_authenticator
|
|
35
|
+
from cexy.errors import ConfigurationError
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AsyncClient:
|
|
39
|
+
"""CEXY.io REST client (asyncio).
|
|
40
|
+
|
|
41
|
+
Public market data needs no credentials. For account and trading endpoints pass an
|
|
42
|
+
API key pair; both values are required together. Keys are sent only on endpoints that
|
|
43
|
+
need them, only as ``X-API-Key``/``X-API-Secret`` headers, and are redacted from
|
|
44
|
+
``repr``, logs and exceptions.
|
|
45
|
+
|
|
46
|
+
The client-side rate limiter defaults to 100 requests/minute without a key and 300 with
|
|
47
|
+
one (``rate_limit_per_minute`` overrides it); it adapts to ``X-RateLimit-*`` headers.
|
|
48
|
+
|
|
49
|
+
``base_url`` must be ``https://``. ``allow_insecure=True`` permits ``http://`` for a
|
|
50
|
+
loopback host only (local testing).
|
|
51
|
+
|
|
52
|
+
Use as ``async with AsyncClient() as client: ...`` or call ``await client.aclose()``.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
api_key: Optional[str] = None,
|
|
58
|
+
api_secret: Optional[str] = None,
|
|
59
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
60
|
+
timeout: float = 10,
|
|
61
|
+
max_retries: int = 3,
|
|
62
|
+
user_agent_suffix: Optional[str] = None,
|
|
63
|
+
*,
|
|
64
|
+
rate_limit_per_minute: Optional[float] = None,
|
|
65
|
+
authenticator: Optional[Authenticator] = None,
|
|
66
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
67
|
+
allow_insecure: bool = False,
|
|
68
|
+
) -> None:
|
|
69
|
+
auth: Optional[Authenticator]
|
|
70
|
+
try:
|
|
71
|
+
auth = build_authenticator(api_key, api_secret)
|
|
72
|
+
except ValueError as exc:
|
|
73
|
+
raise ConfigurationError(str(exc)) from None
|
|
74
|
+
if authenticator is not None:
|
|
75
|
+
if auth is not None:
|
|
76
|
+
raise ConfigurationError("pass either api_key/api_secret or authenticator, not both")
|
|
77
|
+
if not isinstance(authenticator, Authenticator):
|
|
78
|
+
raise ConfigurationError("authenticator must implement cexy.auth.Authenticator")
|
|
79
|
+
auth = authenticator
|
|
80
|
+
self._auth = auth
|
|
81
|
+
if rate_limit_per_minute is None:
|
|
82
|
+
# Server limits: ~120/min per IP for anonymous requests, ~600/min per key.
|
|
83
|
+
rate_limit_per_minute = DEFAULT_RATE_LIMIT_WITH_KEY if auth is not None else DEFAULT_RATE_LIMIT_ANONYMOUS
|
|
84
|
+
self._transport = AsyncTransport(
|
|
85
|
+
base_url=validate_base_url(base_url, allow_insecure),
|
|
86
|
+
auth=auth,
|
|
87
|
+
timeout=timeout,
|
|
88
|
+
max_retries=max_retries,
|
|
89
|
+
user_agent=user_agent(user_agent_suffix),
|
|
90
|
+
rate_limit_per_minute=rate_limit_per_minute,
|
|
91
|
+
http_client=http_client,
|
|
92
|
+
)
|
|
93
|
+
self.markets = AsyncMarkets(self._transport)
|
|
94
|
+
self.assets = AsyncAssets(self._transport)
|
|
95
|
+
self.networks = AsyncNetworks(self._transport)
|
|
96
|
+
self.fees = AsyncFees(self._transport)
|
|
97
|
+
self.pools = AsyncPools(self._transport)
|
|
98
|
+
self.account = AsyncAccount(self._transport)
|
|
99
|
+
self.exports = AsyncExports(self._transport)
|
|
100
|
+
self.wallet = AsyncWallet(self._transport)
|
|
101
|
+
self.trading = AsyncTrading(self._transport)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def base_url(self) -> str:
|
|
105
|
+
return self._transport.base_url
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def has_credentials(self) -> bool:
|
|
109
|
+
return self._auth is not None
|
|
110
|
+
|
|
111
|
+
@operation("server_time")
|
|
112
|
+
async def time(self) -> m.ServerTimeResponse:
|
|
113
|
+
"""The server clock (public)."""
|
|
114
|
+
payload = await self._transport.request("server_time")
|
|
115
|
+
return m.ServerTimeResponse.model_validate(payload["data"])
|
|
116
|
+
|
|
117
|
+
@operation("exchange_config")
|
|
118
|
+
async def config(self) -> m.ExchangeConfigResponse:
|
|
119
|
+
"""Public exchange configuration: maintenance mode, page sizes, intervals (public)."""
|
|
120
|
+
payload = await self._transport.request("exchange_config")
|
|
121
|
+
return m.ExchangeConfigResponse.model_validate(payload["data"])
|
|
122
|
+
|
|
123
|
+
async def aclose(self) -> None:
|
|
124
|
+
await self._transport.aclose()
|
|
125
|
+
|
|
126
|
+
async def __aenter__(self) -> AsyncClient:
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
async def __aexit__(
|
|
130
|
+
self,
|
|
131
|
+
exc_type: Optional[Type[BaseException]],
|
|
132
|
+
exc: Optional[BaseException],
|
|
133
|
+
tb: Optional[TracebackType],
|
|
134
|
+
) -> None:
|
|
135
|
+
await self.aclose()
|
|
136
|
+
|
|
137
|
+
def __repr__(self) -> str:
|
|
138
|
+
auth = REDACTED if self._auth is not None else None
|
|
139
|
+
return f"{type(self).__name__}(base_url={self.base_url!r}, credentials={auth!r})"
|
|
140
|
+
|
|
141
|
+
__str__ = __repr__
|
|
142
|
+
|
|
143
|
+
def __reduce__(self) -> str:
|
|
144
|
+
raise TypeError(f"{type(self).__name__} cannot be pickled (it may hold a secret)")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Cursor pagination: ``items`` + ``next_cursor`` + ``has_more``.
|
|
2
|
+
|
|
3
|
+
Async source; the synchronous ``cexy/_sync/pagination.py`` is generated from it by ``scripts/unasync.py``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, AsyncIterator, Callable, Generic, List, Optional, Type, TypeVar
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T", bound=BaseModel)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncPage(Generic[T]):
|
|
16
|
+
"""One page of results.
|
|
17
|
+
|
|
18
|
+
``async for item in page.auto_paging_iter()`` walks this page and every later one,
|
|
19
|
+
fetching lazily. ``await page.next_page()`` fetches just the next page.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
items: List[T],
|
|
25
|
+
next_cursor: Optional[str],
|
|
26
|
+
has_more: bool,
|
|
27
|
+
fetch: Callable[[str], Any],
|
|
28
|
+
) -> None:
|
|
29
|
+
self.items = items
|
|
30
|
+
self.next_cursor = next_cursor
|
|
31
|
+
self.has_more = has_more
|
|
32
|
+
self._fetch = fetch
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def parse(cls, payload: Any, model: Type[T], fetch: Callable[[str], Any]) -> AsyncPage[T]:
|
|
36
|
+
items = [model.model_validate(x) for x in payload.get("items", [])]
|
|
37
|
+
cursor = payload.get("next_cursor")
|
|
38
|
+
has_more = bool(payload.get("has_more", False)) and bool(cursor)
|
|
39
|
+
return cls(items, cursor, has_more, fetch)
|
|
40
|
+
|
|
41
|
+
async def next_page(self) -> Optional[AsyncPage[T]]:
|
|
42
|
+
if not self.has_more or not self.next_cursor:
|
|
43
|
+
return None
|
|
44
|
+
page: AsyncPage[T] = await self._fetch(self.next_cursor)
|
|
45
|
+
return page
|
|
46
|
+
|
|
47
|
+
async def auto_paging_iter(self) -> AsyncIterator[T]:
|
|
48
|
+
page: Optional[AsyncPage[T]] = self
|
|
49
|
+
while page is not None:
|
|
50
|
+
for item in page.items:
|
|
51
|
+
yield item
|
|
52
|
+
page = await page.next_page()
|
|
53
|
+
|
|
54
|
+
def __len__(self) -> int:
|
|
55
|
+
return len(self.items)
|
|
56
|
+
|
|
57
|
+
def __repr__(self) -> str:
|
|
58
|
+
return f"{type(self).__name__}(items={len(self.items)}, has_more={self.has_more})"
|