p2bit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
p2bit/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ from .async_client import AsyncBybitClient
2
+ from .client import SyncBybitClient
3
+ from .exceptions import FailedRequestError
4
+ from ._types import AccountType, Domain, Endpoint, Endpoints, HttpMethod, Tld
5
+
6
+ VERSION = "0.0.1"
7
+
8
+ __all__ = [
9
+ "AsyncBybitClient",
10
+ "SyncBybitClient",
11
+ "FailedRequestError",
12
+ "AccountType",
13
+ "Domain",
14
+ "Endpoint",
15
+ "Endpoints",
16
+ "HttpMethod",
17
+ "Tld",
18
+ "VERSION",
19
+ ]
p2bit/_api.py ADDED
@@ -0,0 +1,25 @@
1
+ from typing import Any
2
+
3
+ from _types import AccountType
4
+
5
+
6
+ def build_balance_params(
7
+ *,
8
+ member_id: str | None = None,
9
+ account_type: AccountType = "FUND",
10
+ coins: list[str] | None = None,
11
+ with_bonus: bool = False,
12
+ ) -> dict[str, Any]:
13
+ if account_type == "UNIFIED" and not member_id:
14
+ raise ValueError("member_id is required for unified account type")
15
+
16
+ params: dict[str, Any] = {"accountType": account_type}
17
+
18
+ if member_id:
19
+ params["memberId"] = member_id
20
+ if coins:
21
+ params["coin"] = ",".join(coins)
22
+ if with_bonus:
23
+ params["withBonus"] = 1
24
+
25
+ return params
p2bit/_auth.py ADDED
@@ -0,0 +1,23 @@
1
+ import base64
2
+ import hashlib
3
+ import hmac
4
+
5
+ from Crypto.Hash import SHA256
6
+ from Crypto.PublicKey import RSA
7
+ from Crypto.Signature import PKCS1_v1_5
8
+
9
+
10
+ def sign(
11
+ secret: str,
12
+ message: str | bytes,
13
+ *,
14
+ use_rsa: bool = False,
15
+ ) -> str:
16
+ data = message if isinstance(message, bytes) else message.encode("utf-8")
17
+
18
+ if use_rsa:
19
+ digest = SHA256.new(data)
20
+ signature = PKCS1_v1_5.new(RSA.import_key(secret)).sign(digest)
21
+ return base64.b64encode(signature).decode()
22
+
23
+ return hmac.new(secret.encode("utf-8"), data, hashlib.sha256).hexdigest()
p2bit/_base.py ADDED
@@ -0,0 +1,34 @@
1
+ from _types import Domain, Tld
2
+
3
+
4
+ def resolve_base_url(testnet: bool, domain: Domain, tld: Tld) -> str:
5
+ if testnet:
6
+ return "https://api-testnet.bybit.com"
7
+ return f"https://api.{domain}.{tld}"
8
+
9
+
10
+ class BaseBybitClient:
11
+ def __init__(
12
+ self,
13
+ api_key: str,
14
+ api_secret: str,
15
+ *,
16
+ testnet: bool = False,
17
+ recv_window: int = 5000,
18
+ rsa: bool = False,
19
+ domain: Domain = "bybit",
20
+ tld: Tld = "com",
21
+ timeout: float = 10,
22
+ ) -> None:
23
+ if not api_key or not api_secret:
24
+ raise ValueError("api_key and api_secret are required")
25
+
26
+ self._api_key = api_key
27
+ self._api_secret = api_secret
28
+ self._testnet = testnet
29
+ self._recv_window = recv_window
30
+ self._rsa = rsa
31
+ self._domain = domain
32
+ self._tld = tld
33
+ self._timeout = timeout
34
+ self._base_url = resolve_base_url(testnet, domain, tld)
p2bit/_request.py ADDED
@@ -0,0 +1,158 @@
1
+ import json
2
+ import time
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from json import JSONDecodeError
6
+ from typing import Any, Protocol
7
+
8
+ from _auth import sign
9
+ from _types import HttpMethod
10
+ from exceptions import FailedRequestError
11
+
12
+
13
+ class JsonResponse(Protocol):
14
+ status_code: int
15
+ headers: Any
16
+ text: str
17
+
18
+ def json(self) -> Any: ...
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class PreparedRequest:
23
+ method: HttpMethod
24
+ path: str
25
+ headers: dict[str, str]
26
+ payload: str
27
+ endpoint: str
28
+
29
+
30
+ def sanitize_params(params: dict[str, Any]) -> None:
31
+ for key, value in params.items():
32
+ if isinstance(value, float) and value.is_integer():
33
+ params[key] = int(value)
34
+
35
+
36
+ def build_payload(method: HttpMethod, params: dict[str, Any]) -> str:
37
+ if method == "GET":
38
+ return "&".join(f"{key}={value}" for key, value in params.items())
39
+ return json.dumps(params)
40
+
41
+
42
+ def build_signature(
43
+ api_key: str,
44
+ api_secret: str,
45
+ recv_window: int,
46
+ payload: str,
47
+ timestamp: int,
48
+ *,
49
+ use_rsa: bool = False,
50
+ ) -> str:
51
+ sign_string = f"{timestamp}{api_key}{recv_window}{payload}"
52
+ return sign(api_secret, sign_string, use_rsa=use_rsa)
53
+
54
+
55
+ def build_headers(
56
+ api_key: str,
57
+ signature: str,
58
+ timestamp: int,
59
+ recv_window: int,
60
+ ) -> dict[str, str]:
61
+ return {
62
+ "X-BAPI-API-KEY": api_key,
63
+ "X-BAPI-SIGN": signature,
64
+ "X-BAPI-SIGN-TYPE": "2",
65
+ "X-BAPI-TIMESTAMP": str(timestamp),
66
+ "X-BAPI-RECV-WINDOW": str(recv_window),
67
+ "Content-Type": "application/json",
68
+ }
69
+
70
+
71
+ def build_path(endpoint: str, method: HttpMethod, payload: str) -> str:
72
+ path = endpoint.lstrip("/")
73
+ if method == "GET" and payload:
74
+ return f"{path}?{payload}"
75
+ return path
76
+
77
+
78
+ def build_full_url(base_url: str, path: str) -> str:
79
+ return f"{base_url}/{path}"
80
+
81
+
82
+ def prepare_request(
83
+ api_key: str,
84
+ api_secret: str,
85
+ recv_window: int,
86
+ endpoint: str,
87
+ method: HttpMethod,
88
+ params: dict[str, Any] | None,
89
+ *,
90
+ use_rsa: bool = False,
91
+ ) -> PreparedRequest:
92
+ params = dict(params or {})
93
+ sanitize_params(params)
94
+
95
+ timestamp = int(time.time() * 1000)
96
+ payload = build_payload(method, params)
97
+ signature = build_signature(
98
+ api_key, api_secret, recv_window, payload, timestamp, use_rsa=use_rsa
99
+ )
100
+ headers = build_headers(api_key, signature, timestamp, recv_window)
101
+
102
+ return PreparedRequest(
103
+ method=method,
104
+ path=build_path(endpoint, method, payload),
105
+ headers=headers,
106
+ payload=payload,
107
+ endpoint=endpoint,
108
+ )
109
+
110
+
111
+ def _failed_request(
112
+ response: JsonResponse,
113
+ endpoint: str,
114
+ payload: str,
115
+ message: str,
116
+ status_code: int | str,
117
+ ) -> FailedRequestError:
118
+ return FailedRequestError(
119
+ request=f"{endpoint}: {payload}",
120
+ message=message,
121
+ status_code=status_code,
122
+ time=datetime.now(timezone.utc).strftime("%H:%M:%S"),
123
+ resp_headers=response.headers,
124
+ )
125
+
126
+
127
+ def process_response(response: JsonResponse, endpoint: str, payload: str) -> Any:
128
+ if response.status_code != 200:
129
+ if response.status_code == 403:
130
+ message = (
131
+ "Access denied error. Possible causes: 1) your IP is located "
132
+ "in the US or Mainland China, 2) IP banned due to ratelimit violation"
133
+ )
134
+ elif response.status_code == 401:
135
+ message = (
136
+ "Unauthorized. Possible causes: 1) incorrect API key and/or secret, "
137
+ "2) incorrect environment: Mainnet vs Testnet"
138
+ )
139
+ else:
140
+ message = f"HTTP status code is: {response.status_code}, expected: 200"
141
+ raise _failed_request(response, endpoint, payload, message, response.status_code)
142
+
143
+ try:
144
+ body = response.json()
145
+ except JSONDecodeError:
146
+ raise _failed_request(
147
+ response, endpoint, payload, "Could not decode JSON.", response.status_code
148
+ )
149
+
150
+ ret_code = "retCode" if "retCode" in body else "ret_code"
151
+ ret_msg = "retMsg" if "retMsg" in body else "ret_msg"
152
+
153
+ if body[ret_code]:
154
+ raise _failed_request(
155
+ response, endpoint, payload, body[ret_msg], body[ret_code]
156
+ )
157
+
158
+ return body
p2bit/_types.py ADDED
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ HttpMethod = Literal["GET", "POST"]
5
+ Domain = Literal["bybit", "bytick", "byhkbit"]
6
+ Tld = Literal["com", "nl", "tr", "kz"]
7
+ AccountType = Literal["UNIFIED", "FUND"]
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Endpoint:
12
+ path: str
13
+ method: HttpMethod
14
+
15
+
16
+ class Endpoints:
17
+ ACCOUNT_INFO = Endpoint("/v5/p2p/user/personal/info", "POST")
18
+ CURRENT_BALANCE = Endpoint(
19
+ "/v5/asset/transfer/query-account-coins-balance", "GET"
20
+ )
21
+ RELEASE_ORDER = Endpoint("/v5/p2p/order/finish", "POST")
p2bit/async_client.py ADDED
@@ -0,0 +1,104 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from _api import build_balance_params
6
+ from _base import BaseBybitClient
7
+ from _request import prepare_request, process_response
8
+ from _types import AccountType, Domain, Endpoints, HttpMethod, Tld
9
+
10
+
11
+ class AsyncBybitClient(BaseBybitClient):
12
+ def __init__(
13
+ self,
14
+ api_key: str,
15
+ api_secret: str,
16
+ *,
17
+ testnet: bool = False,
18
+ recv_window: int = 5000,
19
+ rsa: bool = False,
20
+ domain: Domain = "bybit",
21
+ tld: Tld = "com",
22
+ timeout: float = 10,
23
+ ) -> None:
24
+ super().__init__(
25
+ api_key,
26
+ api_secret,
27
+ testnet=testnet,
28
+ recv_window=recv_window,
29
+ rsa=rsa,
30
+ domain=domain,
31
+ tld=tld,
32
+ timeout=timeout,
33
+ )
34
+ self._http = httpx.AsyncClient(
35
+ base_url=self._base_url,
36
+ timeout=timeout,
37
+ )
38
+
39
+ async def __aenter__(self) -> "AsyncBybitClient":
40
+ return self
41
+
42
+ async def __aexit__(self, exc_type, exc, tb) -> None:
43
+ await self.close()
44
+
45
+ async def close(self) -> None:
46
+ await self._http.aclose()
47
+
48
+ async def request(
49
+ self,
50
+ endpoint: str,
51
+ method: HttpMethod,
52
+ params: dict[str, Any] | None = None,
53
+ ) -> Any:
54
+ prepared = prepare_request(
55
+ self._api_key,
56
+ self._api_secret,
57
+ self._recv_window,
58
+ endpoint,
59
+ method,
60
+ params,
61
+ use_rsa=self._rsa,
62
+ )
63
+ request = self._http.build_request(
64
+ method=prepared.method,
65
+ url=prepared.path,
66
+ headers=prepared.headers,
67
+ content=prepared.payload if prepared.method == "POST" else None,
68
+ )
69
+ response = await self._http.send(request)
70
+ return process_response(response, prepared.endpoint, prepared.payload)
71
+
72
+ async def get_account_info(self) -> dict[str, Any]:
73
+ response = await self.request(
74
+ Endpoints.ACCOUNT_INFO.path,
75
+ Endpoints.ACCOUNT_INFO.method,
76
+ )
77
+ return response.get("result")
78
+
79
+ async def get_current_balance(
80
+ self,
81
+ member_id: str | None = None,
82
+ account_type: AccountType = "FUND",
83
+ coins: list[str] | None = None,
84
+ with_bonues: bool = False,
85
+ ) -> dict[str, Any]:
86
+ response = await self.request(
87
+ Endpoints.CURRENT_BALANCE.path,
88
+ Endpoints.CURRENT_BALANCE.method,
89
+ params=build_balance_params(
90
+ member_id=member_id,
91
+ account_type=account_type,
92
+ coins=coins,
93
+ with_bonus=with_bonues,
94
+ ),
95
+ )
96
+ return response.get("result")
97
+
98
+ async def release_order(self, order_id: str | int) -> dict[str, Any]:
99
+ response = await self.request(
100
+ Endpoints.RELEASE_ORDER.path,
101
+ Endpoints.RELEASE_ORDER.method,
102
+ params={"orderId": str(order_id)},
103
+ )
104
+ return response.get("result")
p2bit/client.py ADDED
@@ -0,0 +1,96 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ from _api import build_balance_params
6
+ from _base import BaseBybitClient
7
+ from _request import build_full_url, prepare_request, process_response
8
+ from _types import AccountType, Domain, Endpoints, HttpMethod, Tld
9
+
10
+
11
+ class SyncBybitClient(BaseBybitClient):
12
+ def __init__(
13
+ self,
14
+ api_key: str,
15
+ api_secret: str,
16
+ *,
17
+ testnet: bool = False,
18
+ recv_window: int = 5000,
19
+ rsa: bool = False,
20
+ domain: Domain = "bybit",
21
+ tld: Tld = "com",
22
+ timeout: float = 10,
23
+ ) -> None:
24
+ super().__init__(
25
+ api_key,
26
+ api_secret,
27
+ testnet=testnet,
28
+ recv_window=recv_window,
29
+ rsa=rsa,
30
+ domain=domain,
31
+ tld=tld,
32
+ timeout=timeout,
33
+ )
34
+ self._http = requests.Session()
35
+
36
+ def request(
37
+ self,
38
+ endpoint: str,
39
+ method: HttpMethod,
40
+ params: dict[str, Any] | None = None,
41
+ ) -> Any:
42
+ prepared = prepare_request(
43
+ self._api_key,
44
+ self._api_secret,
45
+ self._recv_window,
46
+ endpoint,
47
+ method,
48
+ params,
49
+ use_rsa=self._rsa,
50
+ )
51
+ url = build_full_url(self._base_url, prepared.path)
52
+ request = requests.Request(
53
+ method=prepared.method,
54
+ url=url,
55
+ headers=prepared.headers,
56
+ data=prepared.payload if prepared.method == "POST" else None,
57
+ )
58
+ response = self._http.send(
59
+ self._http.prepare_request(request),
60
+ timeout=self._timeout,
61
+ )
62
+ return process_response(response, prepared.endpoint, prepared.payload)
63
+
64
+ def get_account_info(self) -> dict[str, Any]:
65
+ response = self.request(
66
+ Endpoints.ACCOUNT_INFO.path,
67
+ Endpoints.ACCOUNT_INFO.method,
68
+ )
69
+ return response.get("result")
70
+
71
+ def get_current_balance(
72
+ self,
73
+ member_id: str | None = None,
74
+ account_type: AccountType = "FUND",
75
+ coins: list[str] | None = None,
76
+ with_bonues: bool = False,
77
+ ) -> dict[str, Any]:
78
+ response = self.request(
79
+ Endpoints.CURRENT_BALANCE.path,
80
+ Endpoints.CURRENT_BALANCE.method,
81
+ params=build_balance_params(
82
+ member_id=member_id,
83
+ account_type=account_type,
84
+ coins=coins,
85
+ with_bonus=with_bonues,
86
+ ),
87
+ )
88
+ return response.get("result")
89
+
90
+ def release_order(self, order_id: str | int) -> dict[str, Any]:
91
+ response = self.request(
92
+ Endpoints.RELEASE_ORDER.path,
93
+ Endpoints.RELEASE_ORDER.method,
94
+ params={"orderId": str(order_id)},
95
+ )
96
+ return response.get("result")
p2bit/exceptions.py ADDED
@@ -0,0 +1,11 @@
1
+ class FailedRequestError(Exception):
2
+ def __init__(self, request, message, status_code, time, resp_headers):
3
+ self.request = request
4
+ self.message = message
5
+ self.status_code = status_code
6
+ self.time = time
7
+ self.resp_headers = resp_headers
8
+ super().__init__(
9
+ f"{message.capitalize()} (ErrCode: {status_code}) (ErrTime: {time})"
10
+ f".\nRequest → {request}."
11
+ )
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: p2bit
3
+ Version: 0.1.0
4
+ Summary: Sync/async lib for the Bybit API
5
+ Author-email: herbalsommml <herbalsomml@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: pycryptodome>=3.23.0
9
+ Requires-Dist: requests>=2.34.2
@@ -0,0 +1,13 @@
1
+ p2bit/__init__.py,sha256=VBIKpp1KzRKxmWDFY-5xDiV5gRIb1W3XDj3uF1AQaxo,417
2
+ p2bit/_api.py,sha256=VVc5sDiwFtcp6mBZcWJIyauCgN6qfoSSV8uaUxV0KWo,621
3
+ p2bit/_auth.py,sha256=0kmt-bb85kucS0OkiJoMM8RfL-JBGhSvnFLE_drttiM,577
4
+ p2bit/_base.py,sha256=rwGl9qylpL_254jy4Yb5qI7eBbYpaLAiCu6xnKj-KGg,944
5
+ p2bit/_request.py,sha256=vvaWZ57O6SO7NYVZKwgcaXrT0MLmCKrDhp5P39aINBA,4264
6
+ p2bit/_types.py,sha256=VV46auNr4yRfw2pe1dVN7isVmgMxVEzuqSvzXRN0m6o,564
7
+ p2bit/async_client.py,sha256=o6DAjKMFnx8VFidTeK4uyBYwulph3oocNvDQmx6_QKw,3030
8
+ p2bit/client.py,sha256=tGIo7EBMTXZd-L3VdxUq4lSR3Q_U1KBMekPRAYyW7ZA,2818
9
+ p2bit/exceptions.py,sha256=3lOo0QDmfJRlSo3-HJYm3d4gawJGxkd9uFtGTnhwg9A,436
10
+ p2bit-0.1.0.dist-info/METADATA,sha256=cSx8PW-yfHRo-LZiVzdnQL7ShujVUzJRhhMhpEeMx8M,263
11
+ p2bit-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ p2bit-0.1.0.dist-info/top_level.txt,sha256=GR7803Hre1eOJxzXMD8LEWUQ8XbFZoJ_dCvc0Nas7VE,6
13
+ p2bit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ p2bit