phemex-py 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.
- phemex_py-0.1.0/.claude/settings.json +18 -0
- phemex_py-0.1.0/.coverage +0 -0
- phemex_py-0.1.0/.env.example +3 -0
- phemex_py-0.1.0/.gitignore +17 -0
- phemex_py-0.1.0/.python-version +1 -0
- phemex_py-0.1.0/CHANGELOG.md +20 -0
- phemex_py-0.1.0/JUSTFILE +27 -0
- phemex_py-0.1.0/LICENSE.txt +14 -0
- phemex_py-0.1.0/PKG-INFO +135 -0
- phemex_py-0.1.0/README.md +109 -0
- phemex_py-0.1.0/pyproject.toml +57 -0
- phemex_py-0.1.0/src/phemex_py/__init__.py +10 -0
- phemex_py-0.1.0/src/phemex_py/client.py +196 -0
- phemex_py-0.1.0/src/phemex_py/core/__init__.py +0 -0
- phemex_py-0.1.0/src/phemex_py/core/datetime.py +70 -0
- phemex_py-0.1.0/src/phemex_py/core/fields.py +1808 -0
- phemex_py-0.1.0/src/phemex_py/core/models.py +351 -0
- phemex_py-0.1.0/src/phemex_py/core/products.py +11 -0
- phemex_py-0.1.0/src/phemex_py/core/requests.py +135 -0
- phemex_py-0.1.0/src/phemex_py/exceptions.py +31 -0
- phemex_py-0.1.0/src/phemex_py/products.json +65736 -0
- phemex_py-0.1.0/src/phemex_py/py.typed +0 -0
- phemex_py-0.1.0/src/phemex_py/usdm_rest/__init__.py +3 -0
- phemex_py-0.1.0/src/phemex_py/usdm_rest/endpoints.py +696 -0
- phemex_py-0.1.0/src/phemex_py/usdm_rest/models.py +1098 -0
- phemex_py-0.1.0/tests/__integration__/conftest.py +51 -0
- phemex_py-0.1.0/tests/__integration__/usdm/test_async.py +252 -0
- phemex_py-0.1.0/tests/__integration__/usdm/test_sync.py +249 -0
- phemex_py-0.1.0/tests/conftest.py +8 -0
- phemex_py-0.1.0/tests/core/test_datetime.py +63 -0
- phemex_py-0.1.0/tests/core/test_models.py +89 -0
- phemex_py-0.1.0/tests/core/test_requests.py +22 -0
- phemex_py-0.1.0/uv.lock +399 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(just:*)",
|
|
5
|
+
"Bash(pytest:*)",
|
|
6
|
+
"Bash(PYTHONPATH=src pytest:*)",
|
|
7
|
+
"Bash(python:*)",
|
|
8
|
+
"Bash(PYTHONPATH=src python -m pytest:*)",
|
|
9
|
+
"Bash(PYTHONPATH=src python:*)",
|
|
10
|
+
"Bash(find:*)",
|
|
11
|
+
"Bash(PYTHONPATH=src python3:*)",
|
|
12
|
+
"Bash(test:*)",
|
|
13
|
+
"Bash(source .venv/bin/activate)",
|
|
14
|
+
"Bash(PYTHONPATH=src .venv/bin/python -m pytest:*)",
|
|
15
|
+
"Bash(PYTHONPATH=src .venv/bin/python:*)"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-02-22)
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Sync and async clients for the Phemex API (`PhemexClient`, `AsyncPhemexClient`)
|
|
10
|
+
- HMAC-SHA256 request signing with configurable expiry
|
|
11
|
+
- USD-M Perpetual REST API coverage:
|
|
12
|
+
- Market data: product information, order book, klines, trades, tickers
|
|
13
|
+
- Orders: place, amend, cancel, bulk cancel, cancel all
|
|
14
|
+
- Positions: query, query with PnL, switch mode, set leverage, assign balance
|
|
15
|
+
- Account: risk units
|
|
16
|
+
- History: open/closed orders, closed positions, user trades, order history, trade history
|
|
17
|
+
- Funding: fee history, funding rates
|
|
18
|
+
- Fully typed request/response models via Pydantic v2
|
|
19
|
+
- Structured error handling with `PhemexError`
|
|
20
|
+
- PEP 561 typed package support (`py.typed`)
|
phemex_py-0.1.0/JUSTFILE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Default recipe - list all available recipes
|
|
2
|
+
list:
|
|
3
|
+
@just --list
|
|
4
|
+
|
|
5
|
+
# Run all tests with coverage reporting
|
|
6
|
+
test:
|
|
7
|
+
PYTHONPATH=src pytest -v --cov
|
|
8
|
+
|
|
9
|
+
# Run unit tests (excluding integration tests) with verbose output and fail-fast
|
|
10
|
+
test-unit:
|
|
11
|
+
pytest -m "not integration" -v --ff
|
|
12
|
+
|
|
13
|
+
# Run integration tests with verbose output and fail-fast
|
|
14
|
+
test-integration:
|
|
15
|
+
pytest -m "integration" -v --ff
|
|
16
|
+
|
|
17
|
+
# Run all tests in debug mode, stopping after the first failure and showing the full traceback
|
|
18
|
+
test-debug:
|
|
19
|
+
pytest -x --ff -v -m "not integration" && pytest -x --ff -v -m "integration"
|
|
20
|
+
|
|
21
|
+
# Run only the tests that failed in the previous test run, with verbose output and fail-fast
|
|
22
|
+
test-failed:
|
|
23
|
+
pytest --lf -x -v
|
|
24
|
+
|
|
25
|
+
# Analyze the codebase using cloc, excluding certain directories
|
|
26
|
+
cloc:
|
|
27
|
+
@cloc . --vcs=git --exclude-dir=products,tests
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright 2026 Connor Stone
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
4
|
+
documentation files (the “Software”), to deal in the Software without restriction, including without limitation the
|
|
5
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
6
|
+
persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
|
9
|
+
Software.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
12
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
13
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
14
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
phemex_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phemex-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed, production-grade Python SDK for the Phemex crypto exchange API
|
|
5
|
+
Project-URL: Homepage, https://github.com/cstone/phemex-py
|
|
6
|
+
Project-URL: Repository, https://github.com/cstone/phemex-py
|
|
7
|
+
Project-URL: Issues, https://github.com/cstone/phemex-py/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/cstone/phemex-py/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Connor Stone
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE.txt
|
|
12
|
+
Keywords: api,crypto,exchange,futures,phemex,sdk,trading
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: httpx>=0.28.1
|
|
24
|
+
Requires-Dist: pydantic>=2.12.5
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# phemex-py
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/phemex-py/)
|
|
30
|
+
[](https://pypi.org/project/phemex-py/)
|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
|
|
33
|
+
Typed, production-grade Python SDK for the [Phemex](https://phemex.com) crypto exchange API.
|
|
34
|
+
|
|
35
|
+
- Sync and async clients (built on [httpx](https://www.python-httpx.org/))
|
|
36
|
+
- Fully typed request/response models (built on [Pydantic](https://docs.pydantic.dev/))
|
|
37
|
+
- USD-M perpetual futures: orders, positions, market data, funding rates, and more
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install phemex-py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### Authentication
|
|
48
|
+
|
|
49
|
+
You need a Phemex API key and secret. Create one from your [Phemex account settings](https://phemex.com/account/apiKeys).
|
|
50
|
+
|
|
51
|
+
Set them as environment variables or pass them directly:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export PHEMEX_API_KEY="your-api-key"
|
|
55
|
+
export PHEMEX_API_SECRET="your-api-secret"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Sync Client
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from phemex_py import PhemexClient
|
|
62
|
+
|
|
63
|
+
with PhemexClient(
|
|
64
|
+
base_url="https://api.phemex.com",
|
|
65
|
+
api_key="your-api-key",
|
|
66
|
+
api_secret="your-api-secret",
|
|
67
|
+
) as client:
|
|
68
|
+
# Get server time
|
|
69
|
+
server_time = client.server_time()
|
|
70
|
+
|
|
71
|
+
# Get product info
|
|
72
|
+
products = client.usdm_rest.product_information()
|
|
73
|
+
|
|
74
|
+
# Get ticker
|
|
75
|
+
ticker = client.usdm_rest.ticker(symbol="BTCUSDT")
|
|
76
|
+
|
|
77
|
+
# Get open orders
|
|
78
|
+
orders = client.usdm_rest.open_orders(symbol="BTCUSDT")
|
|
79
|
+
|
|
80
|
+
# Get account positions
|
|
81
|
+
positions = client.usdm_rest.positions()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Async Client
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
from phemex_py import AsyncPhemexClient
|
|
89
|
+
|
|
90
|
+
async def main():
|
|
91
|
+
async with AsyncPhemexClient(
|
|
92
|
+
base_url="https://api.phemex.com",
|
|
93
|
+
api_key="your-api-key",
|
|
94
|
+
api_secret="your-api-secret",
|
|
95
|
+
) as client:
|
|
96
|
+
ticker = await client.usdm_rest.ticker(symbol="BTCUSDT")
|
|
97
|
+
positions = await client.usdm_rest.positions()
|
|
98
|
+
|
|
99
|
+
asyncio.run(main())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Testnet
|
|
103
|
+
|
|
104
|
+
Use the testnet base URL for paper trading:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
client = PhemexClient(
|
|
108
|
+
base_url="https://testnet-api.phemex.com",
|
|
109
|
+
api_key="your-testnet-key",
|
|
110
|
+
api_secret="your-testnet-secret",
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API Coverage
|
|
115
|
+
|
|
116
|
+
### USD-M Perpetual REST API
|
|
117
|
+
|
|
118
|
+
| Category | Endpoints |
|
|
119
|
+
|----------|-----------|
|
|
120
|
+
| Market Data | `product_information`, `order_book`, `klines`, `trades`, `ticker`, `tickers` |
|
|
121
|
+
| Orders | `place_order`, `amend_order`, `cancel_order`, `bulk_cancel`, `cancel_all` |
|
|
122
|
+
| Positions | `positions`, `positions_with_pnl`, `switch_position_mode`, `set_leverage`, `assign_position_balance` |
|
|
123
|
+
| Account | `risk_units` |
|
|
124
|
+
| History | `open_orders`, `closed_orders`, `closed_positions`, `user_trades`, `order_history`, `lookup_order`, `trade_history` |
|
|
125
|
+
| Funding | `funding_fee_history`, `funding_rates` |
|
|
126
|
+
|
|
127
|
+
## Links
|
|
128
|
+
|
|
129
|
+
- [Phemex API Documentation](https://phemex-docs.github.io/)
|
|
130
|
+
- [GitHub Repository](https://github.com/cstone/phemex-py)
|
|
131
|
+
- [PyPI Package](https://pypi.org/project/phemex-py/)
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
[MIT](LICENSE.txt)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# phemex-py
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/phemex-py/)
|
|
4
|
+
[](https://pypi.org/project/phemex-py/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Typed, production-grade Python SDK for the [Phemex](https://phemex.com) crypto exchange API.
|
|
8
|
+
|
|
9
|
+
- Sync and async clients (built on [httpx](https://www.python-httpx.org/))
|
|
10
|
+
- Fully typed request/response models (built on [Pydantic](https://docs.pydantic.dev/))
|
|
11
|
+
- USD-M perpetual futures: orders, positions, market data, funding rates, and more
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install phemex-py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Authentication
|
|
22
|
+
|
|
23
|
+
You need a Phemex API key and secret. Create one from your [Phemex account settings](https://phemex.com/account/apiKeys).
|
|
24
|
+
|
|
25
|
+
Set them as environment variables or pass them directly:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export PHEMEX_API_KEY="your-api-key"
|
|
29
|
+
export PHEMEX_API_SECRET="your-api-secret"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Sync Client
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from phemex_py import PhemexClient
|
|
36
|
+
|
|
37
|
+
with PhemexClient(
|
|
38
|
+
base_url="https://api.phemex.com",
|
|
39
|
+
api_key="your-api-key",
|
|
40
|
+
api_secret="your-api-secret",
|
|
41
|
+
) as client:
|
|
42
|
+
# Get server time
|
|
43
|
+
server_time = client.server_time()
|
|
44
|
+
|
|
45
|
+
# Get product info
|
|
46
|
+
products = client.usdm_rest.product_information()
|
|
47
|
+
|
|
48
|
+
# Get ticker
|
|
49
|
+
ticker = client.usdm_rest.ticker(symbol="BTCUSDT")
|
|
50
|
+
|
|
51
|
+
# Get open orders
|
|
52
|
+
orders = client.usdm_rest.open_orders(symbol="BTCUSDT")
|
|
53
|
+
|
|
54
|
+
# Get account positions
|
|
55
|
+
positions = client.usdm_rest.positions()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Async Client
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from phemex_py import AsyncPhemexClient
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
async with AsyncPhemexClient(
|
|
66
|
+
base_url="https://api.phemex.com",
|
|
67
|
+
api_key="your-api-key",
|
|
68
|
+
api_secret="your-api-secret",
|
|
69
|
+
) as client:
|
|
70
|
+
ticker = await client.usdm_rest.ticker(symbol="BTCUSDT")
|
|
71
|
+
positions = await client.usdm_rest.positions()
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Testnet
|
|
77
|
+
|
|
78
|
+
Use the testnet base URL for paper trading:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
client = PhemexClient(
|
|
82
|
+
base_url="https://testnet-api.phemex.com",
|
|
83
|
+
api_key="your-testnet-key",
|
|
84
|
+
api_secret="your-testnet-secret",
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## API Coverage
|
|
89
|
+
|
|
90
|
+
### USD-M Perpetual REST API
|
|
91
|
+
|
|
92
|
+
| Category | Endpoints |
|
|
93
|
+
|----------|-----------|
|
|
94
|
+
| Market Data | `product_information`, `order_book`, `klines`, `trades`, `ticker`, `tickers` |
|
|
95
|
+
| Orders | `place_order`, `amend_order`, `cancel_order`, `bulk_cancel`, `cancel_all` |
|
|
96
|
+
| Positions | `positions`, `positions_with_pnl`, `switch_position_mode`, `set_leverage`, `assign_position_balance` |
|
|
97
|
+
| Account | `risk_units` |
|
|
98
|
+
| History | `open_orders`, `closed_orders`, `closed_positions`, `user_trades`, `order_history`, `lookup_order`, `trade_history` |
|
|
99
|
+
| Funding | `funding_fee_history`, `funding_rates` |
|
|
100
|
+
|
|
101
|
+
## Links
|
|
102
|
+
|
|
103
|
+
- [Phemex API Documentation](https://phemex-docs.github.io/)
|
|
104
|
+
- [GitHub Repository](https://github.com/cstone/phemex-py)
|
|
105
|
+
- [PyPI Package](https://pypi.org/project/phemex-py/)
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
[MIT](LICENSE.txt)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "phemex-py"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Typed, production-grade Python SDK for the Phemex crypto exchange API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Connor Stone" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
keywords = ["phemex", "crypto", "exchange", "api", "sdk", "trading", "futures"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Office/Business :: Financial",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"httpx>=0.28.1",
|
|
25
|
+
"pydantic>=2.12.5",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/cstone/phemex-py"
|
|
30
|
+
Repository = "https://github.com/cstone/phemex-py"
|
|
31
|
+
Issues = "https://github.com/cstone/phemex-py/issues"
|
|
32
|
+
Changelog = "https://github.com/cstone/phemex-py/blob/main/CHANGELOG.md"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/phemex_py"]
|
|
40
|
+
|
|
41
|
+
[tool.uv]
|
|
42
|
+
package = true
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
pythonpath = ["src"]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
addopts = []
|
|
48
|
+
markers = ["integration: marks tests as hitting external APIs (run with -m integration)"]
|
|
49
|
+
asyncio_mode = "strict"
|
|
50
|
+
|
|
51
|
+
[dependency-groups]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=9.0.2",
|
|
54
|
+
"pytest-asyncio>=0.24.0",
|
|
55
|
+
"pytest-cov>=7.0.0",
|
|
56
|
+
"python-dotenv>=1.2.1",
|
|
57
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .client import PhemexClient, AsyncPhemexClient
|
|
4
|
+
from .exceptions import PhemexError
|
|
5
|
+
|
|
6
|
+
logging.getLogger("phemex_py").addHandler(logging.NullHandler())
|
|
7
|
+
logging.getLogger("phemex_py").setLevel(logging.WARNING)
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__all__ = ["PhemexClient", "AsyncPhemexClient", "PhemexError"]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
import hashlib
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .core.requests import Request, Extractor
|
|
9
|
+
from .exceptions import PhemexError
|
|
10
|
+
|
|
11
|
+
from .usdm_rest import USDMRest, AsyncUSDMRest
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_SENSITIVE_HEADERS = {"x-phemex-access-token", "x-phemex-request-signature"}
|
|
16
|
+
_EXPIRY = 60 # default expiry time in seconds for request signatures
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BasePhemexClient:
|
|
20
|
+
"""
|
|
21
|
+
Shared state and request preparation for sync and async Phemex clients.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, base_url: str, api_key: str, api_secret: str, expiry: int = _EXPIRY):
|
|
25
|
+
self.base_url = base_url.rstrip("/")
|
|
26
|
+
self.api_key = api_key
|
|
27
|
+
self.api_secret = api_secret.encode()
|
|
28
|
+
self.expiry = expiry
|
|
29
|
+
|
|
30
|
+
def _prepare(self, req: Request) -> tuple[str, dict, bytes | None]:
|
|
31
|
+
"""
|
|
32
|
+
Build the URL, signed headers, and encoded body for a request.
|
|
33
|
+
|
|
34
|
+
:return: (url, headers, content)
|
|
35
|
+
"""
|
|
36
|
+
query = req.build_query_string()
|
|
37
|
+
body_json = req.build_body_json()
|
|
38
|
+
|
|
39
|
+
expires = int(time.time()) + self.expiry
|
|
40
|
+
parts = [req.path]
|
|
41
|
+
if query:
|
|
42
|
+
parts.append(query)
|
|
43
|
+
parts.append(str(expires))
|
|
44
|
+
if body_json:
|
|
45
|
+
parts.append(body_json)
|
|
46
|
+
|
|
47
|
+
payload = "".join(parts)
|
|
48
|
+
signature = hmac.new(
|
|
49
|
+
self.api_secret,
|
|
50
|
+
payload.encode("utf-8"),
|
|
51
|
+
hashlib.sha256,
|
|
52
|
+
).hexdigest()
|
|
53
|
+
|
|
54
|
+
headers = {
|
|
55
|
+
"x-phemex-access-token": self.api_key,
|
|
56
|
+
"x-phemex-request-expiry": str(expires),
|
|
57
|
+
"x-phemex-request-signature": signature,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
url = f"{self.base_url}{req.path}"
|
|
61
|
+
if query:
|
|
62
|
+
url = f"{url}?{query}"
|
|
63
|
+
|
|
64
|
+
if body_json:
|
|
65
|
+
headers["Content-Type"] = "application/json"
|
|
66
|
+
|
|
67
|
+
safe_headers = {k: v for k, v in headers.items() if k not in _SENSITIVE_HEADERS}
|
|
68
|
+
logger.debug("REQUEST")
|
|
69
|
+
logger.debug(f"Request: {req.method} {url}")
|
|
70
|
+
logger.debug(f"Headers: {safe_headers}")
|
|
71
|
+
logger.debug(f"Body: {body_json or None}")
|
|
72
|
+
|
|
73
|
+
content = body_json.encode("utf-8") if body_json else None
|
|
74
|
+
return url, headers, content
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _handle_response(resp: httpx.Response, req: Request, url: str, body_json: str | None):
|
|
78
|
+
"""
|
|
79
|
+
Raise PhemexError on HTTP errors and return parsed JSON on success.
|
|
80
|
+
"""
|
|
81
|
+
logger.debug("RESPONSE")
|
|
82
|
+
logger.debug(f"Status Code: {resp.status_code}")
|
|
83
|
+
logger.debug(f"Response Text: {resp.text}")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
resp.raise_for_status()
|
|
87
|
+
except httpx.HTTPStatusError as e:
|
|
88
|
+
raise PhemexError(
|
|
89
|
+
message="Phemex API request failed",
|
|
90
|
+
cause=e,
|
|
91
|
+
context={
|
|
92
|
+
"request": {
|
|
93
|
+
"method": req.method,
|
|
94
|
+
"url": url,
|
|
95
|
+
"body": body_json or None,
|
|
96
|
+
},
|
|
97
|
+
"response": {
|
|
98
|
+
"status_code": resp.status_code,
|
|
99
|
+
"text": resp.text,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return resp.json()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PhemexClient(BasePhemexClient):
|
|
108
|
+
"""
|
|
109
|
+
Sync client for Phemex API (https://phemex-docs.github.io/). Built using httpx.Client.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, base_url: str, api_key: str, api_secret: str, expiry: int = 60):
|
|
113
|
+
super().__init__(base_url, api_key, api_secret, expiry)
|
|
114
|
+
self.session = httpx.Client()
|
|
115
|
+
self.usdm_rest = USDMRest(self)
|
|
116
|
+
|
|
117
|
+
def __enter__(self):
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def __exit__(self, *args):
|
|
121
|
+
self.close()
|
|
122
|
+
|
|
123
|
+
def close(self):
|
|
124
|
+
"""Close the underlying HTTP session."""
|
|
125
|
+
self.session.close()
|
|
126
|
+
|
|
127
|
+
def request(self, req: Request):
|
|
128
|
+
"""
|
|
129
|
+
Make an authenticated request to Phemex API.
|
|
130
|
+
|
|
131
|
+
:param req: Request object
|
|
132
|
+
:return: Parsed JSON response.
|
|
133
|
+
"""
|
|
134
|
+
url, headers, content = self._prepare(req)
|
|
135
|
+
resp = self.session.request(method=req.method, url=url, headers=headers, content=content)
|
|
136
|
+
return self._handle_response(resp, req, url, req.build_body_json())
|
|
137
|
+
|
|
138
|
+
# ----------------------------------------
|
|
139
|
+
# Common endpoints shared across APIs
|
|
140
|
+
# ----------------------------------------
|
|
141
|
+
|
|
142
|
+
def server_time(self, ms: bool = True) -> int:
|
|
143
|
+
"""
|
|
144
|
+
Fetch current Phemex server time (ms by default). For details, see:
|
|
145
|
+
https://phemex-docs.github.io/#query-server-time-2
|
|
146
|
+
"""
|
|
147
|
+
req = Request.get("/public/time")
|
|
148
|
+
resp = self.request(req)
|
|
149
|
+
timestamp = Extractor(resp).key("data", "serverTime").extract()
|
|
150
|
+
return timestamp if ms else timestamp // 1000
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class AsyncPhemexClient(BasePhemexClient):
|
|
154
|
+
"""
|
|
155
|
+
Async client for Phemex API (https://phemex-docs.github.io/). Built using httpx.AsyncClient.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, base_url: str, api_key: str, api_secret: str, expiry: int = 60):
|
|
159
|
+
super().__init__(base_url, api_key, api_secret, expiry)
|
|
160
|
+
self.session = httpx.AsyncClient()
|
|
161
|
+
self.usdm_rest = AsyncUSDMRest(self)
|
|
162
|
+
|
|
163
|
+
async def __aenter__(self):
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
async def __aexit__(self, *args):
|
|
167
|
+
await self.close()
|
|
168
|
+
|
|
169
|
+
async def close(self):
|
|
170
|
+
"""Close the underlying async HTTP session."""
|
|
171
|
+
await self.session.aclose()
|
|
172
|
+
|
|
173
|
+
async def request(self, req: Request):
|
|
174
|
+
"""
|
|
175
|
+
Make an authenticated request to Phemex API.
|
|
176
|
+
|
|
177
|
+
:param req: Request object
|
|
178
|
+
:return: Parsed JSON response.
|
|
179
|
+
"""
|
|
180
|
+
url, headers, content = self._prepare(req)
|
|
181
|
+
resp = await self.session.request(method=req.method, url=url, headers=headers, content=content)
|
|
182
|
+
return self._handle_response(resp, req, url, req.build_body_json())
|
|
183
|
+
|
|
184
|
+
# ----------------------------------------
|
|
185
|
+
# Common endpoints shared across APIs
|
|
186
|
+
# ----------------------------------------
|
|
187
|
+
|
|
188
|
+
async def server_time(self, ms: bool = True) -> int:
|
|
189
|
+
"""
|
|
190
|
+
Fetch current Phemex server time (ms by default). For details, see:
|
|
191
|
+
https://phemex-docs.github.io/#query-server-time-2
|
|
192
|
+
"""
|
|
193
|
+
req = Request.get("/public/time")
|
|
194
|
+
resp = await self.request(req)
|
|
195
|
+
timestamp = Extractor(resp).key("data", "serverTime").extract()
|
|
196
|
+
return timestamp if ms else timestamp // 1000
|
|
File without changes
|