nexode-sdk 0.1.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.
Files changed (34) hide show
  1. nexode_sdk-0.1.0/.gitignore +10 -0
  2. nexode_sdk-0.1.0/PKG-INFO +96 -0
  3. nexode_sdk-0.1.0/README.md +78 -0
  4. nexode_sdk-0.1.0/pyproject.toml +37 -0
  5. nexode_sdk-0.1.0/src/nexode/__init__.py +67 -0
  6. nexode_sdk-0.1.0/src/nexode/_resolver.py +57 -0
  7. nexode_sdk-0.1.0/src/nexode/_transport.py +36 -0
  8. nexode_sdk-0.1.0/src/nexode/auth.py +71 -0
  9. nexode_sdk-0.1.0/src/nexode/client.py +77 -0
  10. nexode_sdk-0.1.0/src/nexode/enums.py +43 -0
  11. nexode_sdk-0.1.0/src/nexode/errors.py +65 -0
  12. nexode_sdk-0.1.0/src/nexode/models/__init__.py +27 -0
  13. nexode_sdk-0.1.0/src/nexode/models/_base.py +5 -0
  14. nexode_sdk-0.1.0/src/nexode/models/activity.py +48 -0
  15. nexode_sdk-0.1.0/src/nexode/models/api_key.py +15 -0
  16. nexode_sdk-0.1.0/src/nexode/models/market.py +25 -0
  17. nexode_sdk-0.1.0/src/nexode/models/nexode.py +140 -0
  18. nexode_sdk-0.1.0/src/nexode/models/order.py +24 -0
  19. nexode_sdk-0.1.0/src/nexode/models/portfolio.py +27 -0
  20. nexode_sdk-0.1.0/src/nexode/models/token.py +15 -0
  21. nexode_sdk-0.1.0/src/nexode/models/trade.py +31 -0
  22. nexode_sdk-0.1.0/src/nexode/resources/__init__.py +21 -0
  23. nexode_sdk-0.1.0/src/nexode/resources/_base.py +6 -0
  24. nexode_sdk-0.1.0/src/nexode/resources/activity.py +30 -0
  25. nexode_sdk-0.1.0/src/nexode/resources/api_keys.py +18 -0
  26. nexode_sdk-0.1.0/src/nexode/resources/events.py +61 -0
  27. nexode_sdk-0.1.0/src/nexode/resources/markets.py +65 -0
  28. nexode_sdk-0.1.0/src/nexode/resources/orders.py +73 -0
  29. nexode_sdk-0.1.0/src/nexode/resources/portfolio.py +66 -0
  30. nexode_sdk-0.1.0/src/nexode/resources/tokens.py +12 -0
  31. nexode_sdk-0.1.0/src/nexode/resources/trades.py +28 -0
  32. nexode_sdk-0.1.0/tests/test_auth.py +76 -0
  33. nexode_sdk-0.1.0/tests/test_errors.py +38 -0
  34. nexode_sdk-0.1.0/tests/test_resources.py +268 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexode-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Nexode prediction-market API
5
+ Project-URL: Homepage, https://nexode.io
6
+ Project-URL: Documentation, https://docs.nexode.io
7
+ Project-URL: Source, https://github.com/k2flabs/nexode
8
+ Author: Nexode
9
+ License-Expression: MIT
10
+ Keywords: nexode,prediction-markets,sdk,trading
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: pydantic>=2.7
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # nexode-sdk
20
+
21
+ Official Python SDK for the [Nexode](https://nexode.io) prediction-market API.
22
+
23
+ ```bash
24
+ pip install nexode-sdk
25
+ ```
26
+
27
+ ```python
28
+ from nexode import Nexode, Outcome, OrderSide
29
+
30
+ nx = Nexode(api_key="nxd_...") # or set NEXODE_API_KEY
31
+
32
+ market = nx.markets.list()[0]
33
+ print(nx.markets.ticker(market.id, Outcome.YES).best_bid)
34
+
35
+ order = nx.orders.create(
36
+ market_id=market.id, outcome=Outcome.YES,
37
+ side=OrderSide.BUY, price="0.55", quantity=100,
38
+ )
39
+ nx.orders.cancel(order.id)
40
+ ```
41
+
42
+ Create an API key in **Settings → Developer**. The SDK exchanges it for
43
+ short-lived access tokens and refreshes them automatically.
44
+
45
+ You work in terms of a **market** and an **outcome** (`yes`/`no`); the SDK
46
+ handles the routing. `nx.markets` and `nx.events` browse markets,
47
+ `nx.orders`/`nx.trades` trade and report fills, and `nx.mints`/`nx.redeems`/
48
+ `nx.burns` cover account activity.
49
+
50
+ Browsing is filtered and sorted server-side:
51
+
52
+ ```python
53
+ from nexode import EventSort
54
+
55
+ events = nx.events.list(
56
+ category="sports",
57
+ sort=EventSort.TRENDING, # ending / trending / new / markets / name
58
+ hide_stale_settled=True,
59
+ limit=20,
60
+ )
61
+
62
+ event = nx.events.by_slug("nba-finals") # markets too: nx.markets.by_slug(...)
63
+ ```
64
+
65
+ Outcomes carry display labels — on a game market `yes`/`no` might read
66
+ "Lakers"/"Celtics". The SDK maps both ways:
67
+
68
+ ```python
69
+ market.labels # {Outcome.YES: "Lakers", Outcome.NO: "Celtics"}
70
+ market.outcome_label(Outcome.YES) # "Lakers" (falls back to "Yes"/"No")
71
+ market.outcome_for_label("Celtics") # Outcome.NO — trade by team name
72
+ market.winning_label # label of the resolved outcome, or None
73
+ ```
74
+
75
+ `nx.api_keys` lists and revokes keys on your account; create them in
76
+ **Settings → Developer**.
77
+
78
+ `nx.portfolio.snapshot()` returns the raw inputs for PnL in one call — trades,
79
+ open orders, mints, redeems, burns, the markets involved, and current tickers:
80
+
81
+ ```python
82
+ s = nx.portfolio.snapshot()
83
+ # s.trades[i].market_id / .outcome / .price / .quantity
84
+ # s.markets[market_id].collateral_price # cost basis / resolution
85
+ # s.tickers[f"{market_id}:{outcome}"].best_bid # mark open positions
86
+ ```
87
+
88
+ See the full reference at [docs.nexode.io](https://docs.nexode.io).
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ cd nexode-sdk
94
+ pip install -e '.[dev]'
95
+ pytest
96
+ ```
@@ -0,0 +1,78 @@
1
+ # nexode-sdk
2
+
3
+ Official Python SDK for the [Nexode](https://nexode.io) prediction-market API.
4
+
5
+ ```bash
6
+ pip install nexode-sdk
7
+ ```
8
+
9
+ ```python
10
+ from nexode import Nexode, Outcome, OrderSide
11
+
12
+ nx = Nexode(api_key="nxd_...") # or set NEXODE_API_KEY
13
+
14
+ market = nx.markets.list()[0]
15
+ print(nx.markets.ticker(market.id, Outcome.YES).best_bid)
16
+
17
+ order = nx.orders.create(
18
+ market_id=market.id, outcome=Outcome.YES,
19
+ side=OrderSide.BUY, price="0.55", quantity=100,
20
+ )
21
+ nx.orders.cancel(order.id)
22
+ ```
23
+
24
+ Create an API key in **Settings → Developer**. The SDK exchanges it for
25
+ short-lived access tokens and refreshes them automatically.
26
+
27
+ You work in terms of a **market** and an **outcome** (`yes`/`no`); the SDK
28
+ handles the routing. `nx.markets` and `nx.events` browse markets,
29
+ `nx.orders`/`nx.trades` trade and report fills, and `nx.mints`/`nx.redeems`/
30
+ `nx.burns` cover account activity.
31
+
32
+ Browsing is filtered and sorted server-side:
33
+
34
+ ```python
35
+ from nexode import EventSort
36
+
37
+ events = nx.events.list(
38
+ category="sports",
39
+ sort=EventSort.TRENDING, # ending / trending / new / markets / name
40
+ hide_stale_settled=True,
41
+ limit=20,
42
+ )
43
+
44
+ event = nx.events.by_slug("nba-finals") # markets too: nx.markets.by_slug(...)
45
+ ```
46
+
47
+ Outcomes carry display labels — on a game market `yes`/`no` might read
48
+ "Lakers"/"Celtics". The SDK maps both ways:
49
+
50
+ ```python
51
+ market.labels # {Outcome.YES: "Lakers", Outcome.NO: "Celtics"}
52
+ market.outcome_label(Outcome.YES) # "Lakers" (falls back to "Yes"/"No")
53
+ market.outcome_for_label("Celtics") # Outcome.NO — trade by team name
54
+ market.winning_label # label of the resolved outcome, or None
55
+ ```
56
+
57
+ `nx.api_keys` lists and revokes keys on your account; create them in
58
+ **Settings → Developer**.
59
+
60
+ `nx.portfolio.snapshot()` returns the raw inputs for PnL in one call — trades,
61
+ open orders, mints, redeems, burns, the markets involved, and current tickers:
62
+
63
+ ```python
64
+ s = nx.portfolio.snapshot()
65
+ # s.trades[i].market_id / .outcome / .price / .quantity
66
+ # s.markets[market_id].collateral_price # cost basis / resolution
67
+ # s.tickers[f"{market_id}:{outcome}"].best_bid # mark open positions
68
+ ```
69
+
70
+ See the full reference at [docs.nexode.io](https://docs.nexode.io).
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ cd nexode-sdk
76
+ pip install -e '.[dev]'
77
+ pytest
78
+ ```
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nexode-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Nexode prediction-market API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Nexode" }]
13
+ keywords = ["nexode", "prediction-markets", "trading", "sdk"]
14
+ dependencies = [
15
+ "httpx>=0.27",
16
+ "pydantic>=2.7",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=8",
22
+ "pytest-asyncio>=0.23",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://nexode.io"
27
+ Documentation = "https://docs.nexode.io"
28
+ Source = "https://github.com/k2flabs/nexode"
29
+
30
+ # Import package is `nexode` (PyPI distribution is `nexode-sdk`):
31
+ # pip install nexode-sdk -> from nexode import Nexode
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/nexode"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ asyncio_mode = "auto"
@@ -0,0 +1,67 @@
1
+ from .client import Nexode
2
+ from .enums import EventSort, MarketStatus, OrderSide, OrderStatus, Outcome, TimeInForce
3
+ from .errors import (
4
+ AuthError,
5
+ NexodeError,
6
+ NotFoundError,
7
+ RateLimitError,
8
+ ServerError,
9
+ UnprocessableError,
10
+ ValidationError,
11
+ )
12
+ from .models import (
13
+ ApiKey,
14
+ BookSnapshot,
15
+ Burn,
16
+ Event,
17
+ Instrument,
18
+ Market,
19
+ MarketEvent,
20
+ MarketTrade,
21
+ Mint,
22
+ Order,
23
+ PortfolioSnapshot,
24
+ PriceLevel,
25
+ Redeem,
26
+ Ticker,
27
+ Token,
28
+ Trade,
29
+ )
30
+
31
+ __all__ = [
32
+ "Nexode",
33
+ # enums
34
+ "Outcome",
35
+ "OrderSide",
36
+ "OrderStatus",
37
+ "MarketStatus",
38
+ "EventSort",
39
+ "TimeInForce",
40
+ # models
41
+ "Market",
42
+ "Event",
43
+ "MarketEvent",
44
+ "Instrument",
45
+ "Order",
46
+ "Trade",
47
+ "MarketTrade",
48
+ "Ticker",
49
+ "PriceLevel",
50
+ "BookSnapshot",
51
+ "Token",
52
+ "Mint",
53
+ "Redeem",
54
+ "Burn",
55
+ "PortfolioSnapshot",
56
+ "ApiKey",
57
+ # errors
58
+ "NexodeError",
59
+ "AuthError",
60
+ "ValidationError",
61
+ "UnprocessableError",
62
+ "NotFoundError",
63
+ "RateLimitError",
64
+ "ServerError",
65
+ ]
66
+
67
+ __version__ = "0.1.0"
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Iterable, Optional, Tuple
4
+
5
+ from ._transport import Transport
6
+ from .enums import Outcome
7
+
8
+ _OUTCOME_KEYS = (
9
+ (Outcome.YES, "atomix_yes_market_id"),
10
+ (Outcome.NO, "atomix_no_market_id"),
11
+ )
12
+
13
+
14
+ class Resolver:
15
+ """Translates between a (market, outcome) and its order book, both ways.
16
+
17
+ The platform runs a separate order book per outcome; the SDK hides that by
18
+ routing on (market, outcome). Lookups are cached, so a market resolves once.
19
+ """
20
+
21
+ def __init__(self, nexode: Transport) -> None:
22
+ self._n = nexode
23
+ self._to_book: Dict[Tuple[str, Outcome], str] = {}
24
+ self._from_book: Dict[str, Tuple[str, Outcome]] = {}
25
+
26
+ def order_book_id(self, market_id: str, outcome) -> str:
27
+ outcome = Outcome(outcome)
28
+ if (market_id, outcome) not in self._to_book:
29
+ self._load_market(market_id)
30
+ try:
31
+ return self._to_book[(market_id, outcome)]
32
+ except KeyError:
33
+ raise ValueError(f"market {market_id!r} has no {outcome.value} order book")
34
+
35
+ def resolve(self, order_book_ids: Iterable[str]) -> Dict[str, Tuple[str, Outcome]]:
36
+ ids = list(order_book_ids)
37
+ unknown = sorted({i for i in ids if i not in self._from_book})
38
+ if unknown:
39
+ body = self._n.get(
40
+ "/v0/markets/by-atomix-id", params={"ids": ",".join(unknown)}
41
+ )
42
+ for m in body["mappings"]:
43
+ self._index(m["atomix_market_id"], m["market"]["id"], Outcome(m["outcome"]))
44
+ return {i: self._from_book[i] for i in ids if i in self._from_book}
45
+
46
+ def market_outcome(self, order_book_id: str) -> Optional[Tuple[str, Outcome]]:
47
+ return self.resolve([order_book_id]).get(order_book_id)
48
+
49
+ def _load_market(self, market_id: str) -> None:
50
+ metadata = self._n.get(f"/v0/markets/{market_id}")["market"].get("metadata") or {}
51
+ for outcome, key in _OUTCOME_KEYS:
52
+ if metadata.get(key):
53
+ self._index(metadata[key], market_id, outcome)
54
+
55
+ def _index(self, order_book_id: str, market_id: str, outcome: Outcome) -> None:
56
+ self._to_book[(market_id, outcome)] = order_book_id
57
+ self._from_book[order_book_id] = (market_id, outcome)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from .errors import error_from_response
8
+
9
+
10
+ class Transport:
11
+ """Issues requests against a fixed base URL and returns the flattened body.
12
+
13
+ Builds absolute URLs (base + path) rather than relying on httpx base_url
14
+ merging, so a base that includes a path prefix (e.g. ``.../atomix``) is
15
+ preserved even though request paths start with ``/``.
16
+ """
17
+
18
+ def __init__(self, client: httpx.Client, base_url: str) -> None:
19
+ self._client = client
20
+ self._base = base_url.rstrip("/")
21
+
22
+ def request(self, method: str, path: str, **kwargs: Any) -> dict:
23
+ response = self._client.request(method, self._base + path, **kwargs)
24
+ body = response.json() if response.content else {}
25
+ if response.status_code >= 400:
26
+ raise error_from_response(response.status_code, body)
27
+ return body
28
+
29
+ def get(self, path: str, **kwargs: Any) -> dict:
30
+ return self.request("GET", path, **kwargs)
31
+
32
+ def post(self, path: str, **kwargs: Any) -> dict:
33
+ return self.request("POST", path, **kwargs)
34
+
35
+ def delete(self, path: str, **kwargs: Any) -> dict:
36
+ return self.request("DELETE", path, **kwargs)
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Generator
5
+
6
+ import httpx
7
+
8
+ from .errors import AuthError
9
+
10
+ # Re-exchange this many seconds before the token's stated expiry.
11
+ _EXPIRY_SKEW_SECONDS = 30
12
+
13
+
14
+ class ApiKeyAuth(httpx.Auth):
15
+ """Exchanges a long-lived API key for short-lived access tokens, caches
16
+ them, and re-exchanges transparently on expiry or 401."""
17
+
18
+ def __init__(self, api_key: str, *, exchange_url: str) -> None:
19
+ self._api_key = api_key
20
+ self._exchange_url = exchange_url
21
+ self._token: str | None = None
22
+ self._expires_at: float = 0.0
23
+
24
+ def sync_auth_flow(
25
+ self, request: httpx.Request
26
+ ) -> Generator[httpx.Request, httpx.Response, None]:
27
+ if not self._fresh():
28
+ response = yield self._exchange()
29
+ response.read()
30
+ self._store(response)
31
+ self._apply(request)
32
+ response = yield request
33
+ if response.status_code == 401:
34
+ response = yield self._exchange()
35
+ response.read()
36
+ self._store(response)
37
+ self._apply(request)
38
+ yield request
39
+
40
+ async def async_auth_flow(
41
+ self, request: httpx.Request
42
+ ) -> Generator[httpx.Request, httpx.Response, None]:
43
+ if not self._fresh():
44
+ response = yield self._exchange()
45
+ await response.aread()
46
+ self._store(response)
47
+ self._apply(request)
48
+ response = yield request
49
+ if response.status_code == 401:
50
+ response = yield self._exchange()
51
+ await response.aread()
52
+ self._store(response)
53
+ self._apply(request)
54
+ yield request
55
+
56
+ def _fresh(self) -> bool:
57
+ return self._token is not None and time.time() < self._expires_at - _EXPIRY_SKEW_SECONDS
58
+
59
+ def _exchange(self) -> httpx.Request:
60
+ # Absolute URL so the exchange always targets the auth endpoint.
61
+ return httpx.Request("POST", self._exchange_url, json={"api_key": self._api_key})
62
+
63
+ def _apply(self, request: httpx.Request) -> None:
64
+ request.headers["Authorization"] = f"Bearer {self._token}"
65
+
66
+ def _store(self, response: httpx.Response) -> None:
67
+ if response.status_code != 200:
68
+ raise AuthError("API key exchange failed.", status=response.status_code)
69
+ data = response.json()
70
+ self._token = data["access_token"]
71
+ self._expires_at = float(data["access_expires_at"])
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Optional
5
+
6
+ import httpx
7
+
8
+ from ._resolver import Resolver
9
+ from ._transport import Transport
10
+ from .auth import ApiKeyAuth
11
+ from .resources import (
12
+ ApiKeys,
13
+ Burns,
14
+ Events,
15
+ Markets,
16
+ Mints,
17
+ Orders,
18
+ Portfolio,
19
+ Redeems,
20
+ Tokens,
21
+ Trades,
22
+ )
23
+
24
+ DEFAULT_API_BASE = "https://api.nexode.io"
25
+ DEFAULT_ATOMIX_BASE = "https://api.nexode.io/atomix"
26
+
27
+
28
+ class Nexode:
29
+ """Synchronous Nexode client.
30
+
31
+ A single API key authenticates the whole platform — prediction markets,
32
+ the order book, and your account activity.
33
+
34
+ Args:
35
+ api_key: A ``nxd_...`` key. Falls back to ``$NEXODE_API_KEY``.
36
+ api_base: Override the Nexode API base URL.
37
+ atomix_base: Override the order-book (Atomix) base URL.
38
+ timeout: Per-request timeout in seconds.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ api_key: Optional[str] = None,
44
+ *,
45
+ api_base: str = DEFAULT_API_BASE,
46
+ atomix_base: str = DEFAULT_ATOMIX_BASE,
47
+ timeout: float = 30.0,
48
+ ) -> None:
49
+ api_key = api_key or os.environ.get("NEXODE_API_KEY")
50
+ if not api_key:
51
+ raise ValueError("api_key is required — pass api_key=... or set NEXODE_API_KEY.")
52
+
53
+ auth = ApiKeyAuth(api_key, exchange_url=f"{api_base.rstrip('/')}/v0/api-keys/exchange")
54
+ self._http = httpx.Client(auth=auth, timeout=timeout)
55
+ nexode = Transport(self._http, api_base)
56
+ atomix = Transport(self._http, atomix_base)
57
+ resolver = Resolver(nexode)
58
+
59
+ self.markets = Markets(nexode, atomix, resolver)
60
+ self.events = Events(nexode)
61
+ self.orders = Orders(atomix, resolver)
62
+ self.trades = Trades(atomix, resolver)
63
+ self.tokens = Tokens(atomix)
64
+ self.mints = Mints(nexode)
65
+ self.redeems = Redeems(nexode)
66
+ self.burns = Burns(nexode)
67
+ self.portfolio = Portfolio(atomix=atomix, nexode=nexode, resolver=resolver)
68
+ self.api_keys = ApiKeys(nexode)
69
+
70
+ def close(self) -> None:
71
+ self._http.close()
72
+
73
+ def __enter__(self) -> "Nexode":
74
+ return self
75
+
76
+ def __exit__(self, *_exc: object) -> None:
77
+ self.close()
@@ -0,0 +1,43 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Outcome(str, Enum):
5
+ YES = "yes"
6
+ NO = "no"
7
+
8
+
9
+ class OrderSide(str, Enum):
10
+ BUY = "buy"
11
+ SELL = "sell"
12
+
13
+
14
+ class OrderStatus(str, Enum):
15
+ OPEN = "open"
16
+ PARTIALLY_FILLED = "partially_filled"
17
+ FILLED = "filled"
18
+ CANCELLED = "cancelled"
19
+
20
+
21
+ class MarketStatus(str, Enum):
22
+ ACTIVE = "active"
23
+ RESOLVED = "resolved"
24
+ VOIDED = "voided"
25
+
26
+
27
+ class EventSort(str, Enum):
28
+ """Sort order for the events list. Every mode keeps active events ahead
29
+ of overdue and settled ones; the mode picks the tiebreaker."""
30
+
31
+ ENDING = "ending"
32
+ TRENDING = "trending"
33
+ NEW = "new"
34
+ MARKETS = "markets"
35
+ NAME = "name"
36
+
37
+
38
+ class TimeInForce(str, Enum):
39
+ GTC = "gtc"
40
+ IOC = "ioc"
41
+ FOK = "fok"
42
+ GTD = "gtd"
43
+ DAY = "day"
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping, Optional, Type
4
+
5
+
6
+ class NexodeError(Exception):
7
+ def __init__(
8
+ self,
9
+ message: str,
10
+ *,
11
+ status: Optional[int] = None,
12
+ trace_id: Optional[str] = None,
13
+ ) -> None:
14
+ super().__init__(message)
15
+ self.message = message
16
+ self.status = status
17
+ self.trace_id = trace_id
18
+
19
+
20
+ class AuthError(NexodeError):
21
+ """Invalid or expired credentials (401/403)."""
22
+
23
+
24
+ class ValidationError(NexodeError):
25
+ """The request was malformed (400)."""
26
+
27
+
28
+ class UnprocessableError(NexodeError):
29
+ """A well-formed request rejected by a business rule — insufficient funds,
30
+ missing trade delegation, market halted, etc. (422)."""
31
+
32
+
33
+ class NotFoundError(NexodeError):
34
+ """The resource does not exist (404)."""
35
+
36
+
37
+ class RateLimitError(NexodeError):
38
+ """Too many requests (429)."""
39
+
40
+
41
+ class ServerError(NexodeError):
42
+ """The server failed to handle the request (5xx)."""
43
+
44
+
45
+ def error_from_response(status: int, body: Mapping[str, Any]) -> NexodeError:
46
+ match status:
47
+ case 400:
48
+ cls: Type[NexodeError] = ValidationError
49
+ case 401 | 403:
50
+ cls = AuthError
51
+ case 404:
52
+ cls = NotFoundError
53
+ case 422:
54
+ cls = UnprocessableError
55
+ case 429:
56
+ cls = RateLimitError
57
+ case s if s >= 500:
58
+ cls = ServerError
59
+ case _:
60
+ cls = NexodeError
61
+ return cls(
62
+ body.get("message") or f"HTTP {status}",
63
+ status=status,
64
+ trace_id=body.get("trace_id"),
65
+ )
@@ -0,0 +1,27 @@
1
+ from .activity import Burn, Mint, Redeem
2
+ from .api_key import ApiKey
3
+ from .market import BookSnapshot, PriceLevel, Ticker
4
+ from .nexode import Event, Instrument, Market, MarketEvent
5
+ from .order import Order
6
+ from .portfolio import PortfolioSnapshot
7
+ from .token import Token
8
+ from .trade import MarketTrade, Trade
9
+
10
+ __all__ = [
11
+ "Market",
12
+ "Event",
13
+ "MarketEvent",
14
+ "Instrument",
15
+ "Order",
16
+ "Trade",
17
+ "MarketTrade",
18
+ "Ticker",
19
+ "PriceLevel",
20
+ "BookSnapshot",
21
+ "Token",
22
+ "Mint",
23
+ "Redeem",
24
+ "Burn",
25
+ "PortfolioSnapshot",
26
+ "ApiKey",
27
+ ]
@@ -0,0 +1,5 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class Model(BaseModel):
5
+ model_config = ConfigDict(extra="ignore", frozen=True)