-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
Copy pathcode-generator.py
476 lines (405 loc) · 19.3 KB
/
code-generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
import re
import requests
from bs4 import BeautifulSoup
import os
from binance.client import Client
from binance.exceptions import BinanceAPIException
URLS = [
'https://developers.binance.com/docs/binance-spot-api-docs',
'https://developers.binance.com/docs/derivatives/change-log',
'https://developers.binance.com/docs/margin_trading/change-log',
'https://developers.binance.com/docs/algo/change-log',
'https://developers.binance.com/docs/wallet/change-log',
'https://developers.binance.com/docs/copy_trading/change-log',
'https://developers.binance.com/docs/convert/change-log',
'https://developers.binance.com/docs/sub_account/change-log',
'https://developers.binance.com/docs/binance_link/change-log',
'https://developers.binance.com/docs/auto_invest/change-log',
'https://developers.binance.com/docs/staking/change-log',
'https://developers.binance.com/docs/dual_investment/change-log',
'https://developers.binance.com/docs/mining/change-log',
'https://developers.binance.com/docs/crypto_loan/change-log',
'https://developers.binance.com/docs/vip_loan/change-log',
'https://developers.binance.com/docs/c2c/change-log',
'https://developers.binance.com/docs/fiat/change-log',
'https://developers.binance.com/docs/nft/change-log',
'https://developers.binance.com/docs/gift_card/change-log',
'https://developers.binance.com/docs/rebate/change-log',
'https://developers.binance.com/docs/simple_earn/change-log',
'https://developers.binance.com/docs/pay/change-log'
]
# Map endpoint prefixes to client.py's request methods
PREFIX_MAP = {
'/api': '_request_api',
'/sapi': '_request_margin_api',
'/papi': '_request_papi_api',
'/fapi': '_request_futures_api',
'/dapi': '_request_futures_coin_api',
'/eapi': '_request_options_api',
'/wapi': '_request_website'
}
DEPRECATED_PREFIXES = [
'/wapi',
]
DEPRECATED_ENDPOINTS = [
('GET', '/fapi/v1/ticker/price'),
('GET', '/fapi/v1/pmExchangeInfo'),
('POST', '/api/v3/order/oco'),
('POST', '/sapi/v1/eth-staking/eth/stake'),
('GET', '/sapi/v1/eth-staking/account'),
('GET', '/sapi/v1/portfolio/interest-rate'),
('GET', '/api/v1/order'),
('GET', '/api/v1/openOrders'),
('POST', '/api/v1/order'),
('DELETE', '/api/v1/order'),
('GET', '/api/v1/allOrders'),
('GET', '/api/v1/account'),
('GET', '/api/v1/myTrades'),
('POST', '/sapi/v1/loan/flexible/borrow'),
('GET', '/sapi/v1/loan/flexible/ongoing/orders'),
('GET', '/sapi/v1/loan/flexible/borrow/history'),
('POST', '/sapi/v1/loan/flexible/repay'),
('GET', '/sapi/v1/loan/flexible/repay/history'),
('POST', '/sapi/v1/loan/flexible/adjust/ltv'),
('GET', '/sapi/v1/loan/flexible/ltv/adjustment/history'),
('GET', '/sapi/v1/loan/flexible/loanable/data'),
('GET', '/sapi/v1/loan/flexible/collateral/data')
]
# Some request methods do not require a version argument
NO_VERSION_FUNCTIONS = [
'_request_options_api',
'_request_futures_data_api'
]
def fetch_endpoints():
"""Fetch endpoints from the provided Binance doc URLs, filtering duplicates."""
endpoints = set()
deprecated_endpoints = set()
for url in URLS:
print(f'Fetching {url}')
page = requests.get(url)
soup = BeautifulSoup(page.content, 'html.parser')
all_code_blocks = soup.find_all('code')
for code in all_code_blocks:
code_text = code.get_text().strip()
parts = code_text.split(' ')
if len(parts) >=2:
parts[0] = parts[0].strip()
parts[1] = parts[1].strip().split('?')[0]
# Basic check for lines that look like: GET /path or POST /path etc.
if len(parts) >= 2 and parts[0] in ['GET', 'POST', 'PUT', 'DELETE'] and parts[1] is not None and parts[1] != '':
method = parts[0]
endpoint = parts[1]
# Ensure endpoint starts with /
if not endpoint.startswith('/'):
endpoint = '/' + endpoint
# Check both deprecated prefixes and specific endpoints
if any(endpoint.startswith(prefix) for prefix in DEPRECATED_PREFIXES) or (method, endpoint) in DEPRECATED_ENDPOINTS:
deprecated_endpoints.add((method, endpoint))
continue # Skip adding to main endpoints set
# Use a tuple of (method, endpoint) for uniqueness
endpoints.add((method, endpoint))
print(f'Found {len(deprecated_endpoints)} deprecated endpoints in the docs.')
print(f'Found {len(endpoints)} unique endpoints in the docs.')
# Filter endpoints that don't start with any known prefix
valid_endpoints = {
(method, endpoint) for method, endpoint in endpoints
if any(endpoint.startswith(prefix) for prefix in PREFIX_MAP.keys())
}
filtered_count = len(endpoints) - len(valid_endpoints)
print(f'Filtered out {filtered_count} endpoints that don\'t match known prefixes')
print(f'Remaining endpoints: {len(valid_endpoints)}')
return valid_endpoints
def get_request_function_and_path(endpoint: str) -> tuple[str | None, str | None, int | None]:
"""
Given an endpoint (e.g. '/sapi/v1/userInfo'), determine which _request_*_api
function is appropriate in the client, remove the recognized prefix plus any
version segments (e.g. /v1/), parse out any version (v1, v2, etc.),
and return (request_function, stripped_path, version).
Example:
endpoint = '/sapi/v1/exchangeInfo'
-> returns ('_request_margin_api', 'exchangeInfo', 1)
If no recognized prefix is found, return (None, None, None).
If no version is found, version will be None.
"""
# Sort prefixes by length descending to match the longest prefix first
sorted_prefixes = sorted(PREFIX_MAP.keys(), key=len, reverse=True)
request_func = None
stripped = None
# Identify which prefix is present, if any
matched_prefix = None
for prefix in sorted_prefixes:
if endpoint.startswith(prefix):
request_func = PREFIX_MAP[prefix]
matched_prefix = prefix
break
# If no recognized prefix, return null
if not request_func or matched_prefix is None:
return None, None, None
stripped = endpoint[len(matched_prefix):]
# Attempt to parse out the version, e.g. '/v1/', '/v2/'
version_match = re.search(r'/v(\d+)/', stripped)
version = None
if version_match:
# Convert the matched text into an integer
version = int(version_match.group(1))
# Remove version segments like /v1/, /v2/
if stripped:
stripped = re.sub(r'/v\d+/', '/', stripped)
# Strip leading/trailing slashes
stripped = stripped.strip('/')
return request_func, stripped, version
def check_method_in_file(method, endpoint, file_name):
"""
Return True if a function for this endpoint likely exists in client.py.
"""
if not os.path.isfile(file_name):
print(f'{file_name} does not exist')
return False
func_name, stripped_path, version = get_request_function_and_path(endpoint)
# If no known request function is found, we consider it not found.
if not func_name or not stripped_path:
print(f'No known request function for endpoint: {endpoint}')
return False
with open(file_name, 'r', encoding='utf-8') as f:
content = f.read()
# Remove any leftover version tokens from the path
stripped_path = re.sub(r'^v\d+/', '', stripped_path)
stripped_path = re.sub(r'/v\d+/', '/', stripped_path)
patterns = []
if func_name == "_request_api":
if version == 3:
# v3 endpoints use PRIVATE_API_VERSION or "v3"
patterns.extend([
# Direct request with PRIVATE_API_VERSION
rf'{re.escape(func_name)}\s*\(\s*"{re.escape(method.lower())}"\s*,\s*"{re.escape(stripped_path)}"[\s\S]*?version\s*=\s*self\.PRIVATE_API_VERSION[\s\S]*?\)',
# Direct request with "v3"
rf'{re.escape(func_name)}\s*\(\s*"{re.escape(method.lower())}"\s*,\s*"{re.escape(stripped_path)}"[\s\S]*?version\s*=\s*["\']v3["\'][\s\S]*?\)',
# Helper method with PRIVATE_API_VERSION
rf'_{method.lower()}\s*\(\s*"{re.escape(stripped_path)}"(?:\s*,\s*(?:True|False))?[\s\S]*?version\s*=\s*self\.PRIVATE_API_VERSION[\s\S]*?\)',
# Helper method with "v3"
rf'_{method.lower()}\s*\(\s*"{re.escape(stripped_path)}"(?:\s*,\s*(?:True|False))?[\s\S]*?version\s*=\s*["\']v3["\'][\s\S]*?\)'
])
elif version == 1:
# v1 endpoints can use either no version arg, PUBLIC_API_VERSION, or "v1"
patterns.extend([
# Direct request with no version
rf'{re.escape(func_name)}\s*\(\s*"{re.escape(method.lower())}"\s*,\s*"{re.escape(stripped_path)}"[\s\S]*?\)',
# Direct request with PUBLIC_API_VERSION
rf'{re.escape(func_name)}\s*\(\s*"{re.escape(method.lower())}"\s*,\s*"{re.escape(stripped_path)}"[\s\S]*?version\s*=\s*self\.PUBLIC_API_VERSION[\s\S]*?\)',
# Direct request with "v1"
rf'{re.escape(func_name)}\s*\(\s*"{re.escape(method.lower())}"\s*,\s*"{re.escape(stripped_path)}"[\s\S]*?version\s*=\s*["\']v1["\'][\s\S]*?\)',
# Helper method with no version
rf'_{method.lower()}\s*\(\s*"{re.escape(stripped_path)}"(?:\s*,\s*(?:True|False))?[\s\S]*?\)',
# Helper method with PUBLIC_API_VERSION
rf'_{method.lower()}\s*\(\s*"{re.escape(stripped_path)}"(?:\s*,\s*(?:True|False))?[\s\S]*?version\s*=\s*self\.PUBLIC_API_VERSION[\s\S]*?\)',
# Helper method with "v1"
rf'_{method.lower()}\s*\(\s*"{re.escape(stripped_path)}"(?:\s*,\s*(?:True|False))?[\s\S]*?version\s*=\s*["\']v1["\'][\s\S]*?\)'
])
else:
# Non-API requests (margin, futures, etc.)
patterns.append(
rf'{re.escape(func_name)}\s*\(\s*"{re.escape(method.lower())}"\s*,\s*"{re.escape(stripped_path)}"'
)
# Check all patterns
for pattern in patterns:
if re.search(pattern, content, re.DOTALL):
return True
return False
def convert_to_function_name(method: str, endpoint: str) -> str:
"""
Convert an endpoint path to a consistent function name format with appropriate prefix and version.
Examples:
GET, /api/v3/ticker/tradingDay -> v3_get_ticker_trading_day
GET, /sapi/v1/margin/order -> margin_v1_get_order
GET, /fapi/v1/ticker/price -> futures_v1_get_ticker_price
GET, /dapi/v1/ticker/price -> futures_coin_v1_get_ticker_price
GET, /vapi/v1/ticker -> options_v1_get_ticker
"""
# Get the request function and path info
request_function, cleaned_endpoint, version = get_request_function_and_path(endpoint)
# Map request functions to their prefix in the function name
PREFIX_NAME_MAP = {
'_request_margin_api': 'margin',
'_request_papi_api': 'papi',
'_request_futures_api': 'futures',
'_request_futures_coin_api': 'futures_coin',
'_request_options_api': 'options'
}
# Remove known prefixes and version segments first
cleaned_endpoint = endpoint
sorted_prefixes = sorted(PREFIX_MAP.keys(), key=len, reverse=True)
for prefix in sorted_prefixes:
if cleaned_endpoint.startswith(prefix):
cleaned_endpoint = cleaned_endpoint[len(prefix):]
break
# Remove version segments and leading/trailing slashes
cleaned_endpoint = re.sub(r'/v\d+/', '/', cleaned_endpoint)
cleaned_endpoint = cleaned_endpoint.strip('/')
# Split on slashes and process each part
parts = cleaned_endpoint.split('/')
processed_parts = []
for part in parts:
# Replace hyphens with underscores
part = part.replace('-', '_')
part = part.replace('.', '_')
# Insert underscore before capital letters in camelCase
part = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', part)
# Convert to lowercase
part = part.lower()
# Remove any duplicate underscores
part = re.sub(r'_+', '_', part)
processed_parts.append(part)
# Join all parts with underscores
base_name = '_'.join(processed_parts)
base_name = re.sub(r'_+', '_', base_name) # Remove any duplicate underscores
# Add version to the name (default to v1 if no version found)
version_str = f"v{version if version else ''}"
# Prepend the appropriate prefix if this is a special endpoint
if request_function in PREFIX_NAME_MAP:
prefix = PREFIX_NAME_MAP[request_function]
return f"{prefix}_{version_str}_{method.lower()}_{base_name}"
# Default case (for _request_api)
return f"{version_str}_{method.lower()}_{base_name}"
def check_function(method, request_function, cleaned_endpoint, version_arg, params={}):
"""
Check if the function is signed based on the endpoint. If not found or deprecated, return None.
For GET requests, call the endpoint and check if it returns an error indicating a signature is required.
"""
if method != "GET":
return True
else:
try:
client = Client("", "")
client_function = getattr(client, request_function)
version = {"version": version_arg} if version_arg else {}
client_function(
method.lower(),
cleaned_endpoint,
signed=False,
**version,
**params
)
return False
except BinanceAPIException as e:
if e.status_code == 400 and "symbol" in e.message:
return check_function(method, request_function, cleaned_endpoint, version_arg, {'data': {'symbol': 'BTCUSDT'}})
if 'signature' in e.message or 'API-key' in e.message:
return True
if 'The endpoint has been out of maintenance' in e.message or \
'This endpoint has been deprecated, please remove as soon as possible.' in e.message or \
e.status_code == 404:
return None
else:
print(f"Error calling endpoint {request_function} - {cleaned_endpoint} - {version_arg} - {params}")
return None
except Exception as e:
print(f"Error calling endpoint {request_function} - {cleaned_endpoint} - {version_arg} - {params}")
return None
def generate_function_code(method, endpoint, type="sync", file_name="./binance/client.py"):
"""
Determines which _request_*_api function, path, and version to call based on the endpoint,
generates a placeholder function to handle the specified method/endpoint.
If the chosen request function is in NO_VERSION_FUNCTIONS, the code does not pass a 'version' argument.
If no recognized prefix is found, returns an empty string.
If GET function will test if the endpoint is public or not
"""
request_function, cleaned_endpoint, version = get_request_function_and_path(endpoint)
# If no recognized prefix, skip generating
if not request_function:
return ""
func_name = convert_to_function_name(method, endpoint)
# Build version argument if needed
version_string = ""
if request_function in NO_VERSION_FUNCTIONS or version is None:
# No version arg is needed
version_arg = None
elif request_function == "_request_api":
version_arg = f"v{version}"
else:
# If a version was found, pass version= the integer, else default to 1
version_arg = version if version else 1
if version_arg is not None:
if isinstance(version_arg, str):
version_string = f', version="{version_arg}"'
else:
version_string = f", version={version_arg}"
is_signed = check_function(method, request_function, cleaned_endpoint, version_arg)
if is_signed is None:
return
code_snippet = ""
if type == "sync":
code_snippet = f"""
def {func_name}(self, **params):
\"\"\"
Placeholder function for {method.upper()} {endpoint}.
Note: This function was auto-generated. Any issue please open an issue on GitHub.
:param params: parameters required by the endpoint
:type params: dict
:returns: API response
\"\"\"
return self.{request_function}("{method.lower()}", "{cleaned_endpoint}", signed={is_signed}, data=params{version_string})
"""
elif type == "async":
code_snippet = f"""
async def {func_name}(self, **params):
return await self.{request_function}("{method.lower()}", "{cleaned_endpoint}", signed={is_signed}, data=params{version_string})
"""
with open('./binance/client.py', 'r', encoding='utf-8') as f:
content = f.read()
if f"def {func_name}("in content:
code_snippet += f"""
{func_name}.__doc__ = Client.{func_name}.__doc__
"""
with open(file_name, 'a', encoding='utf-8') as f:
f.write(code_snippet)
def write_function_to_endpoints_md(method, endpoint):
"""
Append a brief reference entry to Endpoints.md, showing the usage example.
First checks if the entry already exists to avoid duplicates.
"""
function_name = convert_to_function_name(method, endpoint)
# Check if the entry already exists
with open('Endpoints.md', 'r', encoding='utf-8') as f:
content = f.read()
# Look for the exact method and endpoint
if f"**{method.upper()} {endpoint}" in content:
return False
# Create the entry we want to add
md_entry = (
f"\t- **{method} {endpoint}**\n"
f" ```python\n"
f" client.{function_name}(**params)\n"
f" ```\n\n"
)
# If we get here, the entry doesn't exist, so append it
with open('Endpoints.md', 'a', encoding='utf-8') as f:
f.write(md_entry)
return True
def main():
endpoints = fetch_endpoints()
# Write to Endpoints.md
endpoints_md_created = 0
for method, endpoint in endpoints:
success = write_function_to_endpoints_md(method, endpoint)
if success:
endpoints_md_created += 1
print(f"Added {endpoints_md_created} endpoints to Endpoints.md")
# Filter out endpoints already in client.py
new_endpoints = []
for method, endpoint in endpoints:
if not check_method_in_file(method, endpoint, './binance/client.py'):
new_endpoints.append((method, endpoint))
print(f"{len(new_endpoints)} endpoints were added out of {len(endpoints)} scrapped in client.py")
# Generate placeholder code for these endpoints
for method, endpoint in new_endpoints:
generate_function_code(method, endpoint, type="sync", file_name="./binance/client.py")
# Generate async functions
new_endpoints_async = []
for method, endpoint in endpoints:
if not check_method_in_file(method, endpoint, './binance/async_client.py'):
new_endpoints_async.append((method, endpoint))
for method, endpoint in new_endpoints_async:
generate_function_code(method, endpoint, type="async", file_name="./binance/async_client.py")
print(f"{len(new_endpoints_async)} endpoints were added out of {len(endpoints)} scrapped in async_client.py")
if __name__ == "__main__":
main()