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 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
+ ]
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})"