anaxer 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.
- anaxer-0.1.0/.gitignore +9 -0
- anaxer-0.1.0/LICENSE +21 -0
- anaxer-0.1.0/PKG-INFO +135 -0
- anaxer-0.1.0/README.md +117 -0
- anaxer-0.1.0/pyproject.toml +69 -0
- anaxer-0.1.0/scripts/assert_wheel_deps.py +37 -0
- anaxer-0.1.0/src/anaxer/__init__.py +46 -0
- anaxer-0.1.0/src/anaxer/_client_async.py +175 -0
- anaxer-0.1.0/src/anaxer/_client_sync.py +62 -0
- anaxer-0.1.0/src/anaxer/_config.py +51 -0
- anaxer-0.1.0/src/anaxer/_connect.py +33 -0
- anaxer-0.1.0/src/anaxer/errors.py +65 -0
- anaxer-0.1.0/src/anaxer/models.py +309 -0
- anaxer-0.1.0/src/anaxer/rest/__init__.py +1 -0
- anaxer-0.1.0/src/anaxer/rest/_endpoints.py +303 -0
- anaxer-0.1.0/src/anaxer/rest/_http.py +176 -0
- anaxer-0.1.0/src/anaxer/rest/_pagination.py +60 -0
- anaxer-0.1.0/src/anaxer/ws/__init__.py +1 -0
- anaxer-0.1.0/src/anaxer/ws/_connection.py +453 -0
- anaxer-0.1.0/src/anaxer/ws/_subscription.py +91 -0
- anaxer-0.1.0/src/anaxer/ws/_transport.py +57 -0
- anaxer-0.1.0/test/mock_ws.py +124 -0
- anaxer-0.1.0/test/test_contract_examples.py +254 -0
- anaxer-0.1.0/test/test_models_compat.py +68 -0
- anaxer-0.1.0/test/test_reconnect.py +293 -0
- anaxer-0.1.0/test/test_rest.py +222 -0
- anaxer-0.1.0/test/test_types.py +52 -0
- anaxer-0.1.0/test/test_ws.py +328 -0
anaxer-0.1.0/.gitignore
ADDED
anaxer-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anaxer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
anaxer-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anaxer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Anaxer Solana real-time data API.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: pydantic>=2.6
|
|
10
|
+
Requires-Dist: websockets<14,>=12
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
13
|
+
Requires-Dist: mypy>=1.9; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# anaxer
|
|
20
|
+
|
|
21
|
+
Official Python SDK for the [Anaxer](https://anaxer.com) Solana real-time data API.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install anaxer
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Python 3.10+.
|
|
28
|
+
|
|
29
|
+
## Quickstart — async streaming
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
import anaxer
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
async with anaxer.connect(api_key="...") as client:
|
|
37
|
+
# Plan limits from the server `connected` frame
|
|
38
|
+
print(client.connected.connections_per_stream)
|
|
39
|
+
|
|
40
|
+
async with client.stream("creations", exclude_mayhem=True) as sub:
|
|
41
|
+
async for event in sub:
|
|
42
|
+
print("new launch:", event.symbol, event.mint)
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`stream()` returns a `Subscription[T]` handle (async-iterable + async context manager).
|
|
49
|
+
Prefer `async with client.stream(...) as sub:` so leaving the block always sends
|
|
50
|
+
`unsubscribe`. Bare `async for` / `break` also unsubscribes (via `GeneratorExit`).
|
|
51
|
+
|
|
52
|
+
### Channels
|
|
53
|
+
|
|
54
|
+
| Channel | Yields | Filters (keyword-only, snake_case) |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `trades` | `SwapV1` | `sources`, `mints`, `wallets`, `min_volume_usd`, `max_volume_usd`, `sol_only` |
|
|
57
|
+
| `creations` | `CreatedV1` | `sources`, `enriched`, `exclude_mayhem` |
|
|
58
|
+
| `graduations` | `GraduatedV1` | `sources`, `exclude_mayhem`, `min_liquidity_sol` |
|
|
59
|
+
| `prices` | `PriceUpdateV1` | `sources`, `mints` |
|
|
60
|
+
|
|
61
|
+
`transfers` is not wired in v1 (server ingestion deferred). `TransferV1` is still
|
|
62
|
+
exported as a type.
|
|
63
|
+
|
|
64
|
+
### Side-channel errors
|
|
65
|
+
|
|
66
|
+
Reconnect-triggering / non-fatal codes (e.g. `slow_consumer`) are delivered on
|
|
67
|
+
`client.on_error(callback)` and/or `async for err in client.errors` — they never
|
|
68
|
+
interrupt an open `Subscription`'s `async for`. Subscription-fatal codes
|
|
69
|
+
(`invalid_filters`, …) raise from that subscription only. `unauthorized` is
|
|
70
|
+
terminal.
|
|
71
|
+
|
|
72
|
+
## Quickstart — sync REST
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from anaxer import Client
|
|
76
|
+
|
|
77
|
+
client = Client(api_key="...")
|
|
78
|
+
price = client.tokens.price("3PFaeFMXiRVYueDCnHHSKndKDDpKZhbhFjQ13JXYpump")
|
|
79
|
+
print(price.price.usd)
|
|
80
|
+
client.close()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`Client` is REST-only — it has no `stream()` method. Use it when you do not want a
|
|
84
|
+
WebSocket. `AsyncClient` (from `anaxer.connect`) also exposes the same REST surface
|
|
85
|
+
via `await`.
|
|
86
|
+
|
|
87
|
+
### REST surface
|
|
88
|
+
|
|
89
|
+
- `tokens.get` / `batch` / `price` / `batch_price` / `trades`
|
|
90
|
+
- `creations` / `iter_creations`
|
|
91
|
+
- `graduations` / `iter_graduations`
|
|
92
|
+
- `iter_trades(mint, ...)`
|
|
93
|
+
- `launchpads.stats`
|
|
94
|
+
|
|
95
|
+
`programs()` is omitted in v1 (response type not in the public schemas).
|
|
96
|
+
|
|
97
|
+
List endpoints return a `Page` (`data`, `next`, `window`). Use `iter_*` to walk
|
|
98
|
+
`next` across pages. Wire field `from` is exposed in Python as `from_`
|
|
99
|
+
(reserved word) with a Pydantic alias so round-trips keep the wire key.
|
|
100
|
+
|
|
101
|
+
Auth is always `Authorization: Bearer <api_key>` (same as the TypeScript SDK).
|
|
102
|
+
|
|
103
|
+
## No gap recovery in v1
|
|
104
|
+
|
|
105
|
+
The WebSocket protocol has no sequence/cursor. After a disconnect the client
|
|
106
|
+
reconnects and re-subscribes, but events during the gap are not replayed.
|
|
107
|
+
|
|
108
|
+
## Config
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from anaxer import Reconnect, connect
|
|
112
|
+
|
|
113
|
+
async with connect(
|
|
114
|
+
api_key="...",
|
|
115
|
+
base_url="https://api.anaxer.com", # local: http://localhost:3010
|
|
116
|
+
reconnect=Reconnect(base_delay=0.5, max_delay=30.0, max_retries=None),
|
|
117
|
+
heartbeat_interval=15.0, # 0 disables
|
|
118
|
+
rest_max_retries=2,
|
|
119
|
+
) as client:
|
|
120
|
+
...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Errors
|
|
124
|
+
|
|
125
|
+
All failures surface as `anaxer.AnaxerError` with `.code`, `.message`, `.status`
|
|
126
|
+
(HTTP only), and `.subscription_id` (subscription-fatal WS errors only).
|
|
127
|
+
|
|
128
|
+
SDK-local codes: `connection_closed`, `timeout`, `invalid_payload` (client-side
|
|
129
|
+
parse failure — distinct from the wire's `invalid_message`).
|
|
130
|
+
|
|
131
|
+
## Links
|
|
132
|
+
|
|
133
|
+
- Wire contracts: [`docs/api/ws-v1.md`](../../docs/api/ws-v1.md), [`docs/api/rest-v1.md`](../../docs/api/rest-v1.md)
|
|
134
|
+
- System doc: [`docs/system/sdk-py.md`](../../docs/system/sdk-py.md)
|
|
135
|
+
- TypeScript sibling: [`@anaxer/sdk`](../../packages/sdk/README.md)
|
anaxer-0.1.0/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# anaxer
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Anaxer](https://anaxer.com) Solana real-time data API.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install anaxer
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Requires Python 3.10+.
|
|
10
|
+
|
|
11
|
+
## Quickstart — async streaming
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
import anaxer
|
|
16
|
+
|
|
17
|
+
async def main():
|
|
18
|
+
async with anaxer.connect(api_key="...") as client:
|
|
19
|
+
# Plan limits from the server `connected` frame
|
|
20
|
+
print(client.connected.connections_per_stream)
|
|
21
|
+
|
|
22
|
+
async with client.stream("creations", exclude_mayhem=True) as sub:
|
|
23
|
+
async for event in sub:
|
|
24
|
+
print("new launch:", event.symbol, event.mint)
|
|
25
|
+
break
|
|
26
|
+
|
|
27
|
+
asyncio.run(main())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`stream()` returns a `Subscription[T]` handle (async-iterable + async context manager).
|
|
31
|
+
Prefer `async with client.stream(...) as sub:` so leaving the block always sends
|
|
32
|
+
`unsubscribe`. Bare `async for` / `break` also unsubscribes (via `GeneratorExit`).
|
|
33
|
+
|
|
34
|
+
### Channels
|
|
35
|
+
|
|
36
|
+
| Channel | Yields | Filters (keyword-only, snake_case) |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `trades` | `SwapV1` | `sources`, `mints`, `wallets`, `min_volume_usd`, `max_volume_usd`, `sol_only` |
|
|
39
|
+
| `creations` | `CreatedV1` | `sources`, `enriched`, `exclude_mayhem` |
|
|
40
|
+
| `graduations` | `GraduatedV1` | `sources`, `exclude_mayhem`, `min_liquidity_sol` |
|
|
41
|
+
| `prices` | `PriceUpdateV1` | `sources`, `mints` |
|
|
42
|
+
|
|
43
|
+
`transfers` is not wired in v1 (server ingestion deferred). `TransferV1` is still
|
|
44
|
+
exported as a type.
|
|
45
|
+
|
|
46
|
+
### Side-channel errors
|
|
47
|
+
|
|
48
|
+
Reconnect-triggering / non-fatal codes (e.g. `slow_consumer`) are delivered on
|
|
49
|
+
`client.on_error(callback)` and/or `async for err in client.errors` — they never
|
|
50
|
+
interrupt an open `Subscription`'s `async for`. Subscription-fatal codes
|
|
51
|
+
(`invalid_filters`, …) raise from that subscription only. `unauthorized` is
|
|
52
|
+
terminal.
|
|
53
|
+
|
|
54
|
+
## Quickstart — sync REST
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from anaxer import Client
|
|
58
|
+
|
|
59
|
+
client = Client(api_key="...")
|
|
60
|
+
price = client.tokens.price("3PFaeFMXiRVYueDCnHHSKndKDDpKZhbhFjQ13JXYpump")
|
|
61
|
+
print(price.price.usd)
|
|
62
|
+
client.close()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`Client` is REST-only — it has no `stream()` method. Use it when you do not want a
|
|
66
|
+
WebSocket. `AsyncClient` (from `anaxer.connect`) also exposes the same REST surface
|
|
67
|
+
via `await`.
|
|
68
|
+
|
|
69
|
+
### REST surface
|
|
70
|
+
|
|
71
|
+
- `tokens.get` / `batch` / `price` / `batch_price` / `trades`
|
|
72
|
+
- `creations` / `iter_creations`
|
|
73
|
+
- `graduations` / `iter_graduations`
|
|
74
|
+
- `iter_trades(mint, ...)`
|
|
75
|
+
- `launchpads.stats`
|
|
76
|
+
|
|
77
|
+
`programs()` is omitted in v1 (response type not in the public schemas).
|
|
78
|
+
|
|
79
|
+
List endpoints return a `Page` (`data`, `next`, `window`). Use `iter_*` to walk
|
|
80
|
+
`next` across pages. Wire field `from` is exposed in Python as `from_`
|
|
81
|
+
(reserved word) with a Pydantic alias so round-trips keep the wire key.
|
|
82
|
+
|
|
83
|
+
Auth is always `Authorization: Bearer <api_key>` (same as the TypeScript SDK).
|
|
84
|
+
|
|
85
|
+
## No gap recovery in v1
|
|
86
|
+
|
|
87
|
+
The WebSocket protocol has no sequence/cursor. After a disconnect the client
|
|
88
|
+
reconnects and re-subscribes, but events during the gap are not replayed.
|
|
89
|
+
|
|
90
|
+
## Config
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from anaxer import Reconnect, connect
|
|
94
|
+
|
|
95
|
+
async with connect(
|
|
96
|
+
api_key="...",
|
|
97
|
+
base_url="https://api.anaxer.com", # local: http://localhost:3010
|
|
98
|
+
reconnect=Reconnect(base_delay=0.5, max_delay=30.0, max_retries=None),
|
|
99
|
+
heartbeat_interval=15.0, # 0 disables
|
|
100
|
+
rest_max_retries=2,
|
|
101
|
+
) as client:
|
|
102
|
+
...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Errors
|
|
106
|
+
|
|
107
|
+
All failures surface as `anaxer.AnaxerError` with `.code`, `.message`, `.status`
|
|
108
|
+
(HTTP only), and `.subscription_id` (subscription-fatal WS errors only).
|
|
109
|
+
|
|
110
|
+
SDK-local codes: `connection_closed`, `timeout`, `invalid_payload` (client-side
|
|
111
|
+
parse failure — distinct from the wire's `invalid_message`).
|
|
112
|
+
|
|
113
|
+
## Links
|
|
114
|
+
|
|
115
|
+
- Wire contracts: [`docs/api/ws-v1.md`](../../docs/api/ws-v1.md), [`docs/api/rest-v1.md`](../../docs/api/rest-v1.md)
|
|
116
|
+
- System doc: [`docs/system/sdk-py.md`](../../docs/system/sdk-py.md)
|
|
117
|
+
- TypeScript sibling: [`@anaxer/sdk`](../../packages/sdk/README.md)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "anaxer"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Official Python SDK for the Anaxer Solana real-time data API."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"websockets>=12,<14",
|
|
11
|
+
"httpx>=0.27",
|
|
12
|
+
"pydantic>=2.6",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=8",
|
|
18
|
+
"pytest-asyncio>=0.23",
|
|
19
|
+
"mypy>=1.9",
|
|
20
|
+
"ruff>=0.4",
|
|
21
|
+
"build>=1.2",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/anaxer"]
|
|
30
|
+
|
|
31
|
+
[tool.mypy]
|
|
32
|
+
strict = true
|
|
33
|
+
mypy_path = "src"
|
|
34
|
+
packages = ["anaxer"]
|
|
35
|
+
plugins = ["pydantic.mypy"]
|
|
36
|
+
warn_unused_ignores = true
|
|
37
|
+
|
|
38
|
+
[[tool.mypy.overrides]]
|
|
39
|
+
module = "websockets.*"
|
|
40
|
+
ignore_missing_imports = true
|
|
41
|
+
|
|
42
|
+
[[tool.mypy.overrides]]
|
|
43
|
+
module = [
|
|
44
|
+
"test_ws",
|
|
45
|
+
"test_reconnect",
|
|
46
|
+
"test_rest",
|
|
47
|
+
"test_contract_examples",
|
|
48
|
+
"test_models_compat",
|
|
49
|
+
"mock_ws",
|
|
50
|
+
]
|
|
51
|
+
disallow_untyped_defs = false
|
|
52
|
+
check_untyped_defs = false
|
|
53
|
+
allow_untyped_defs = true
|
|
54
|
+
disable_error_code = ["type-arg", "no-untyped-def"]
|
|
55
|
+
|
|
56
|
+
# test_types.py intentionally uses type: ignore on negative cases — keep it under
|
|
57
|
+
# strict + warn_unused_ignores (do not loosen this module).
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
src = ["src", "test"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
ignore = ["S110", "PYI034"]
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
asyncio_mode = "auto"
|
|
68
|
+
testpaths = ["test"]
|
|
69
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Assert built wheel runtime Requires-Dist is exactly websockets/httpx/pydantic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import glob
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import zipfile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
wheels = sorted(glob.glob("dist/*.whl"))
|
|
13
|
+
if not wheels:
|
|
14
|
+
print("no wheel in dist/", file=sys.stderr)
|
|
15
|
+
return 1
|
|
16
|
+
wheel = wheels[-1]
|
|
17
|
+
zf = zipfile.ZipFile(wheel)
|
|
18
|
+
meta_name = next(n for n in zf.namelist() if n.endswith("METADATA"))
|
|
19
|
+
meta = zf.read(meta_name).decode()
|
|
20
|
+
deps: set[str] = set()
|
|
21
|
+
for line in meta.splitlines():
|
|
22
|
+
if not line.startswith("Requires-Dist:"):
|
|
23
|
+
continue
|
|
24
|
+
if "extra ==" in line:
|
|
25
|
+
continue
|
|
26
|
+
rest = line.split(":", 1)[1].strip()
|
|
27
|
+
deps.add(re.split(r"[><=!~; ]", rest)[0])
|
|
28
|
+
expected = {"websockets", "httpx", "pydantic"}
|
|
29
|
+
if deps != expected:
|
|
30
|
+
print(f"expected {expected}, got {deps}", file=sys.stderr)
|
|
31
|
+
return 1
|
|
32
|
+
print("OK", deps)
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Official Python SDK for the Anaxer Solana real-time data API."""
|
|
2
|
+
|
|
3
|
+
from anaxer._client_async import AsyncClient
|
|
4
|
+
from anaxer._client_sync import Client
|
|
5
|
+
from anaxer._config import Reconnect
|
|
6
|
+
from anaxer._connect import connect
|
|
7
|
+
from anaxer.errors import AnaxerError, AnaxerErrorCode
|
|
8
|
+
from anaxer.models import (
|
|
9
|
+
WIRE_VERSION,
|
|
10
|
+
ConnectedInfo,
|
|
11
|
+
CreatedV1,
|
|
12
|
+
GraduatedV1,
|
|
13
|
+
LaunchpadStatsV1,
|
|
14
|
+
PriceUpdateV1,
|
|
15
|
+
SubscriptionFiltersV1,
|
|
16
|
+
SwapV1,
|
|
17
|
+
TokenMetadataV1,
|
|
18
|
+
TransferV1,
|
|
19
|
+
Window,
|
|
20
|
+
)
|
|
21
|
+
from anaxer.rest._pagination import Page
|
|
22
|
+
from anaxer.ws._subscription import Subscription
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"WIRE_VERSION",
|
|
26
|
+
"AnaxerError",
|
|
27
|
+
"AnaxerErrorCode",
|
|
28
|
+
"AsyncClient",
|
|
29
|
+
"Client",
|
|
30
|
+
"ConnectedInfo",
|
|
31
|
+
"CreatedV1",
|
|
32
|
+
"GraduatedV1",
|
|
33
|
+
"LaunchpadStatsV1",
|
|
34
|
+
"Page",
|
|
35
|
+
"PriceUpdateV1",
|
|
36
|
+
"Reconnect",
|
|
37
|
+
"Subscription",
|
|
38
|
+
"SubscriptionFiltersV1",
|
|
39
|
+
"SwapV1",
|
|
40
|
+
"TokenMetadataV1",
|
|
41
|
+
"TransferV1",
|
|
42
|
+
"Window",
|
|
43
|
+
"connect",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""AsyncClient: eager WS + REST namespaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator, Callable
|
|
6
|
+
from typing import Any, Literal, overload
|
|
7
|
+
|
|
8
|
+
from anaxer._config import ClientConfig
|
|
9
|
+
from anaxer.errors import AnaxerError
|
|
10
|
+
from anaxer.models import (
|
|
11
|
+
ConnectedInfo,
|
|
12
|
+
CreatedV1,
|
|
13
|
+
GraduatedV1,
|
|
14
|
+
PriceUpdateV1,
|
|
15
|
+
SubscriptionFiltersV1,
|
|
16
|
+
SwapV1,
|
|
17
|
+
)
|
|
18
|
+
from anaxer.rest._endpoints import (
|
|
19
|
+
LaunchpadsAsyncApi,
|
|
20
|
+
TokensAsyncApi,
|
|
21
|
+
async_creations,
|
|
22
|
+
async_graduations,
|
|
23
|
+
iter_creations_async,
|
|
24
|
+
iter_graduations_async,
|
|
25
|
+
iter_trades_async,
|
|
26
|
+
)
|
|
27
|
+
from anaxer.rest._http import AsyncHttpClient
|
|
28
|
+
from anaxer.rest._pagination import Page
|
|
29
|
+
from anaxer.ws._connection import ErrorHub, WsConnection
|
|
30
|
+
from anaxer.ws._subscription import Subscription
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _ErrorsAsyncIterator:
|
|
34
|
+
def __init__(self, hub: ErrorHub) -> None:
|
|
35
|
+
self._q = hub.subscribe()
|
|
36
|
+
|
|
37
|
+
def __aiter__(self) -> _ErrorsAsyncIterator:
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
async def __anext__(self) -> AnaxerError:
|
|
41
|
+
item = await self._q.get()
|
|
42
|
+
if item is None:
|
|
43
|
+
raise StopAsyncIteration
|
|
44
|
+
return item
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncClient:
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
config: ClientConfig,
|
|
51
|
+
*,
|
|
52
|
+
http_transport: Any = None,
|
|
53
|
+
open_transport_fn: Any = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._config = config
|
|
56
|
+
self._error_hub = ErrorHub()
|
|
57
|
+
self._conn = WsConnection(
|
|
58
|
+
config, self._error_hub, open_transport_fn=open_transport_fn
|
|
59
|
+
)
|
|
60
|
+
self._http = AsyncHttpClient(
|
|
61
|
+
base_url=config.base_url,
|
|
62
|
+
api_key=config.api_key,
|
|
63
|
+
max_retries=config.rest_max_retries,
|
|
64
|
+
transport=http_transport,
|
|
65
|
+
)
|
|
66
|
+
self.tokens = TokensAsyncApi(self._http)
|
|
67
|
+
self.launchpads = LaunchpadsAsyncApi(self._http)
|
|
68
|
+
self._entered = False
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def connected(self) -> ConnectedInfo:
|
|
72
|
+
info = self._conn.connected
|
|
73
|
+
if info is None:
|
|
74
|
+
raise AnaxerError("connection_closed", "Client is not connected")
|
|
75
|
+
return info
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def errors(self) -> AsyncIterator[AnaxerError]:
|
|
79
|
+
"""Side-channel errors. Each access returns a new subscriber iterator."""
|
|
80
|
+
return _ErrorsAsyncIterator(self._error_hub)
|
|
81
|
+
|
|
82
|
+
def on_error(self, callback: Callable[[AnaxerError], None]) -> None:
|
|
83
|
+
self._error_hub.on_error(callback)
|
|
84
|
+
|
|
85
|
+
async def __aenter__(self) -> AsyncClient:
|
|
86
|
+
await self._conn.open()
|
|
87
|
+
self._entered = True
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
91
|
+
await self.aclose()
|
|
92
|
+
|
|
93
|
+
async def aclose(self) -> None:
|
|
94
|
+
await self._conn.close()
|
|
95
|
+
await self._http.aclose()
|
|
96
|
+
self._entered = False
|
|
97
|
+
|
|
98
|
+
@overload
|
|
99
|
+
def stream(
|
|
100
|
+
self,
|
|
101
|
+
channel: Literal["trades"],
|
|
102
|
+
*,
|
|
103
|
+
sources: list[str] | None = None,
|
|
104
|
+
mints: list[str] | None = None,
|
|
105
|
+
wallets: list[str] | None = None,
|
|
106
|
+
min_volume_usd: float | None = None,
|
|
107
|
+
max_volume_usd: float | None = None,
|
|
108
|
+
sol_only: bool | None = None,
|
|
109
|
+
) -> Subscription[SwapV1]: ...
|
|
110
|
+
|
|
111
|
+
@overload
|
|
112
|
+
def stream(
|
|
113
|
+
self,
|
|
114
|
+
channel: Literal["creations"],
|
|
115
|
+
*,
|
|
116
|
+
sources: list[str] | None = None,
|
|
117
|
+
enriched: bool | None = None,
|
|
118
|
+
exclude_mayhem: bool | None = None,
|
|
119
|
+
) -> Subscription[CreatedV1]: ...
|
|
120
|
+
|
|
121
|
+
@overload
|
|
122
|
+
def stream(
|
|
123
|
+
self,
|
|
124
|
+
channel: Literal["graduations"],
|
|
125
|
+
*,
|
|
126
|
+
sources: list[str] | None = None,
|
|
127
|
+
exclude_mayhem: bool | None = None,
|
|
128
|
+
min_liquidity_sol: float | None = None,
|
|
129
|
+
) -> Subscription[GraduatedV1]: ...
|
|
130
|
+
|
|
131
|
+
@overload
|
|
132
|
+
def stream(
|
|
133
|
+
self,
|
|
134
|
+
channel: Literal["prices"],
|
|
135
|
+
*,
|
|
136
|
+
sources: list[str] | None = None,
|
|
137
|
+
mints: list[str] | None = None,
|
|
138
|
+
) -> Subscription[PriceUpdateV1]: ...
|
|
139
|
+
|
|
140
|
+
def stream(self, channel: str, **filters: Any) -> Subscription[Any]:
|
|
141
|
+
if not self._entered and self._conn.state != "open":
|
|
142
|
+
# Allow stream() only after successful __aenter__ (eager open).
|
|
143
|
+
raise AnaxerError(
|
|
144
|
+
"connection_closed",
|
|
145
|
+
"Call stream() inside `async with anaxer.connect(...)`",
|
|
146
|
+
)
|
|
147
|
+
model = SubscriptionFiltersV1.model_validate(
|
|
148
|
+
{
|
|
149
|
+
"sources": filters.get("sources"),
|
|
150
|
+
"mints": filters.get("mints"),
|
|
151
|
+
"wallets": filters.get("wallets"),
|
|
152
|
+
"minVolumeUsd": filters.get("min_volume_usd"),
|
|
153
|
+
"maxVolumeUsd": filters.get("max_volume_usd"),
|
|
154
|
+
"enriched": filters.get("enriched"),
|
|
155
|
+
"excludeMayhem": filters.get("exclude_mayhem"),
|
|
156
|
+
"minLiquiditySol": filters.get("min_liquidity_sol"),
|
|
157
|
+
"solOnly": filters.get("sol_only"),
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
return self._conn.stream(channel, model) # type: ignore[arg-type]
|
|
161
|
+
|
|
162
|
+
async def creations(self, **opts: Any) -> Page[CreatedV1]:
|
|
163
|
+
return await async_creations(self._http, **opts)
|
|
164
|
+
|
|
165
|
+
def iter_creations(self, **opts: Any) -> AsyncIterator[CreatedV1]:
|
|
166
|
+
return iter_creations_async(self._http, **opts)
|
|
167
|
+
|
|
168
|
+
async def graduations(self, **opts: Any) -> Page[GraduatedV1]:
|
|
169
|
+
return await async_graduations(self._http, **opts)
|
|
170
|
+
|
|
171
|
+
def iter_graduations(self, **opts: Any) -> AsyncIterator[GraduatedV1]:
|
|
172
|
+
return iter_graduations_async(self._http, **opts)
|
|
173
|
+
|
|
174
|
+
def iter_trades(self, mint: str, **opts: Any) -> AsyncIterator[SwapV1]:
|
|
175
|
+
return iter_trades_async(self._http, mint, **opts)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Sync Client — REST only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from anaxer.models import CreatedV1, GraduatedV1, SwapV1
|
|
9
|
+
from anaxer.rest._endpoints import (
|
|
10
|
+
LaunchpadsApi,
|
|
11
|
+
TokensApi,
|
|
12
|
+
iter_creations_sync,
|
|
13
|
+
iter_graduations_sync,
|
|
14
|
+
iter_trades_sync,
|
|
15
|
+
sync_creations,
|
|
16
|
+
sync_graduations,
|
|
17
|
+
)
|
|
18
|
+
from anaxer.rest._http import SyncHttpClient
|
|
19
|
+
from anaxer.rest._pagination import Page
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Client:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
api_key: str,
|
|
26
|
+
*,
|
|
27
|
+
base_url: str = "https://api.anaxer.com",
|
|
28
|
+
rest_max_retries: int = 2,
|
|
29
|
+
http_transport: Any = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self._http = SyncHttpClient(
|
|
32
|
+
base_url=base_url,
|
|
33
|
+
api_key=api_key,
|
|
34
|
+
max_retries=rest_max_retries,
|
|
35
|
+
transport=http_transport,
|
|
36
|
+
)
|
|
37
|
+
self.tokens = TokensApi(self._http)
|
|
38
|
+
self.launchpads = LaunchpadsApi(self._http)
|
|
39
|
+
|
|
40
|
+
def close(self) -> None:
|
|
41
|
+
self._http.close()
|
|
42
|
+
|
|
43
|
+
def __enter__(self) -> Client:
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, *exc: object) -> None:
|
|
47
|
+
self.close()
|
|
48
|
+
|
|
49
|
+
def creations(self, **opts: Any) -> Page[CreatedV1]:
|
|
50
|
+
return sync_creations(self._http, **opts)
|
|
51
|
+
|
|
52
|
+
def iter_creations(self, **opts: Any) -> Iterator[CreatedV1]:
|
|
53
|
+
return iter_creations_sync(self._http, **opts)
|
|
54
|
+
|
|
55
|
+
def graduations(self, **opts: Any) -> Page[GraduatedV1]:
|
|
56
|
+
return sync_graduations(self._http, **opts)
|
|
57
|
+
|
|
58
|
+
def iter_graduations(self, **opts: Any) -> Iterator[GraduatedV1]:
|
|
59
|
+
return iter_graduations_sync(self._http, **opts)
|
|
60
|
+
|
|
61
|
+
def iter_trades(self, mint: str, **opts: Any) -> Iterator[SwapV1]:
|
|
62
|
+
return iter_trades_sync(self._http, mint, **opts)
|