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.
- nexode_sdk-0.1.0/.gitignore +10 -0
- nexode_sdk-0.1.0/PKG-INFO +96 -0
- nexode_sdk-0.1.0/README.md +78 -0
- nexode_sdk-0.1.0/pyproject.toml +37 -0
- nexode_sdk-0.1.0/src/nexode/__init__.py +67 -0
- nexode_sdk-0.1.0/src/nexode/_resolver.py +57 -0
- nexode_sdk-0.1.0/src/nexode/_transport.py +36 -0
- nexode_sdk-0.1.0/src/nexode/auth.py +71 -0
- nexode_sdk-0.1.0/src/nexode/client.py +77 -0
- nexode_sdk-0.1.0/src/nexode/enums.py +43 -0
- nexode_sdk-0.1.0/src/nexode/errors.py +65 -0
- nexode_sdk-0.1.0/src/nexode/models/__init__.py +27 -0
- nexode_sdk-0.1.0/src/nexode/models/_base.py +5 -0
- nexode_sdk-0.1.0/src/nexode/models/activity.py +48 -0
- nexode_sdk-0.1.0/src/nexode/models/api_key.py +15 -0
- nexode_sdk-0.1.0/src/nexode/models/market.py +25 -0
- nexode_sdk-0.1.0/src/nexode/models/nexode.py +140 -0
- nexode_sdk-0.1.0/src/nexode/models/order.py +24 -0
- nexode_sdk-0.1.0/src/nexode/models/portfolio.py +27 -0
- nexode_sdk-0.1.0/src/nexode/models/token.py +15 -0
- nexode_sdk-0.1.0/src/nexode/models/trade.py +31 -0
- nexode_sdk-0.1.0/src/nexode/resources/__init__.py +21 -0
- nexode_sdk-0.1.0/src/nexode/resources/_base.py +6 -0
- nexode_sdk-0.1.0/src/nexode/resources/activity.py +30 -0
- nexode_sdk-0.1.0/src/nexode/resources/api_keys.py +18 -0
- nexode_sdk-0.1.0/src/nexode/resources/events.py +61 -0
- nexode_sdk-0.1.0/src/nexode/resources/markets.py +65 -0
- nexode_sdk-0.1.0/src/nexode/resources/orders.py +73 -0
- nexode_sdk-0.1.0/src/nexode/resources/portfolio.py +66 -0
- nexode_sdk-0.1.0/src/nexode/resources/tokens.py +12 -0
- nexode_sdk-0.1.0/src/nexode/resources/trades.py +28 -0
- nexode_sdk-0.1.0/tests/test_auth.py +76 -0
- nexode_sdk-0.1.0/tests/test_errors.py +38 -0
- nexode_sdk-0.1.0/tests/test_resources.py +268 -0
|
@@ -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
|
+
]
|