ubdcc 0.3.1__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.
ubdcc/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ¯\_(ツ)_/¯
4
+ #
5
+ # File: packages/ubdcc/ubdcc/__init__.py
6
+ #
7
+ # Project website: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
8
+ # Github: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
9
+ # Documentation: https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster
10
+ # PyPI: https://pypi.org/project/ubdcc
11
+ #
12
+ # License: MIT
13
+ # https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/LICENSE
14
+ #
15
+ # Author: Oliver Zehentleitner
16
+ #
17
+ # Copyright (c) 2024-2026, Oliver Zehentleitner (https://about.me/oliver-zehentleitner)
18
+ # All rights reserved.
19
+
20
+ __version__ = "0.2.0"
ubdcc/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from ubdcc.cli import main
2
+
3
+ main()
ubdcc/cli.py ADDED
@@ -0,0 +1,534 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ¯\_(ツ)_/¯
4
+ #
5
+ # File: packages/ubdcc/ubdcc/cli.py
6
+ #
7
+ # Project website: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
8
+ # Github: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
9
+ # Documentation: https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster
10
+ # PyPI: https://pypi.org/project/ubdcc
11
+ #
12
+ # License: MIT
13
+ # https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/LICENSE
14
+ #
15
+ # Author: Oliver Zehentleitner
16
+ #
17
+ # Copyright (c) 2024-2026, Oliver Zehentleitner (https://about.me/oliver-zehentleitner)
18
+ # All rights reserved.
19
+
20
+ import argparse
21
+ import os
22
+ import signal
23
+ import socket
24
+ import subprocess
25
+ import sys
26
+ import time
27
+
28
+ import requests
29
+
30
+ from ubdcc import __version__
31
+
32
+ STATE_FILE = ".ubdcc"
33
+ DEFAULT_MGMT_PORT = 42080
34
+
35
+
36
+ def save_port(port):
37
+ with open(STATE_FILE, "w") as f:
38
+ f.write(str(port))
39
+
40
+
41
+ def load_port():
42
+ try:
43
+ with open(STATE_FILE, "r") as f:
44
+ return int(f.read().strip())
45
+ except (FileNotFoundError, ValueError):
46
+ return None
47
+
48
+
49
+ def get_mgmt_port(args):
50
+ """Resolve mgmt port: --port flag > state file > default."""
51
+ if args.port is not None:
52
+ return args.port
53
+ saved = load_port()
54
+ if saved is not None:
55
+ return saved
56
+ return DEFAULT_MGMT_PORT
57
+
58
+
59
+ def is_port_free(port, host='127.0.0.1'):
60
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
61
+ try:
62
+ s.bind((host, port))
63
+ return True
64
+ except OSError:
65
+ return False
66
+
67
+
68
+ def find_free_port(start=42080):
69
+ port = start
70
+ while not is_port_free(port):
71
+ port += 1
72
+ return port
73
+
74
+
75
+ def wait_for_cluster(mgmt_port, expected_pods, timeout=120):
76
+ """Poll get_cluster_info until all expected pods are registered."""
77
+ url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
78
+ start_time = time.time()
79
+ while time.time() - start_time < timeout:
80
+ try:
81
+ response = requests.get(url, timeout=5)
82
+ data = response.json()
83
+ if data.get('result') == 'OK' and data.get('db', {}).get('pods'):
84
+ registered = len(data['db']['pods'])
85
+ if registered >= expected_pods:
86
+ return data
87
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
88
+ pass
89
+ time.sleep(1)
90
+ return None
91
+
92
+
93
+ def cmd_start(args):
94
+ mgmt_port = args.port if args.port else find_free_port(DEFAULT_MGMT_PORT)
95
+ dcn_count = args.dcn
96
+ logdir = args.logdir if args.logdir else os.getcwd()
97
+ os.makedirs(logdir, exist_ok=True)
98
+ save_port(mgmt_port)
99
+
100
+ print(f"UBDCC Cluster Manager v{__version__}")
101
+ print(f"Starting cluster with mgmt port {mgmt_port}, {dcn_count} DCN(s)...")
102
+ print(f"Log directory: {logdir}")
103
+
104
+ processes = []
105
+ cwd = os.getcwd()
106
+ dcn_counter = [0] # mutable for closure access
107
+
108
+ def spawn_mgmt():
109
+ log = open(os.path.join(logdir, "ubdcc-mgmt.log"), "a")
110
+ proc = subprocess.Popen(
111
+ [sys.executable, "-c",
112
+ f"import os; from ubdcc_mgmt.Mgmt import Mgmt; Mgmt(cwd='{cwd}', mgmt_port={mgmt_port})"],
113
+ stdout=log, stderr=subprocess.STDOUT
114
+ )
115
+ # Remove old mgmt from processes list
116
+ for i, (name, p, l) in enumerate(processes):
117
+ if name == "mgmt":
118
+ processes.pop(i)
119
+ break
120
+ processes.append(("mgmt", proc, log))
121
+ return proc.pid
122
+
123
+ def spawn_restapi():
124
+ log = open(os.path.join(logdir, "ubdcc-restapi.log"), "a")
125
+ proc = subprocess.Popen(
126
+ [sys.executable, "-c",
127
+ f"import os; from ubdcc_restapi.RestApi import RestApi; RestApi(cwd='{cwd}', mgmt_port={mgmt_port})"],
128
+ stdout=log, stderr=subprocess.STDOUT
129
+ )
130
+ # Remove old restapi from processes list
131
+ for i, (name, p, l) in enumerate(processes):
132
+ if name == "restapi":
133
+ processes.pop(i)
134
+ break
135
+ processes.append(("restapi", proc, log))
136
+ return proc.pid
137
+
138
+ def spawn_dcn():
139
+ dcn_counter[0] += 1
140
+ nr = dcn_counter[0]
141
+ log = open(os.path.join(logdir, f"ubdcc-dcn-{nr}.log"), "a")
142
+ proc = subprocess.Popen(
143
+ [sys.executable, "-c",
144
+ f"import os; from ubdcc_dcn.DepthCacheNode import DepthCacheNode; "
145
+ f"DepthCacheNode(cwd='{cwd}', mgmt_port={mgmt_port})"],
146
+ stdout=log, stderr=subprocess.STDOUT
147
+ )
148
+ processes.append((f"dcn-{nr}", proc, log))
149
+ return nr, proc.pid
150
+
151
+ # Start mgmt
152
+ pid = spawn_mgmt()
153
+ print(f" mgmt started (PID {pid})")
154
+
155
+ # Start restapi
156
+ pid = spawn_restapi()
157
+ print(f" restapi started (PID {pid})")
158
+
159
+ # Start DCNs
160
+ for i in range(dcn_count):
161
+ nr, pid = spawn_dcn()
162
+ print(f" dcn-{nr} started (PID {pid})")
163
+
164
+ expected_pods = 1 + dcn_count # restapi + DCNs (mgmt doesn't register itself)
165
+ print(f"\nWaiting for {expected_pods} pods to register with mgmt...")
166
+
167
+ cluster_info = wait_for_cluster(mgmt_port, expected_pods)
168
+ if cluster_info:
169
+ print(f"Cluster is ready!\n")
170
+ print_status_table(cluster_info, mgmt_port=mgmt_port)
171
+ else:
172
+ print("Warning: Timeout waiting for all pods to register. Check the logs.")
173
+
174
+ print(f"\nType 'help' for available commands, Ctrl+C or 'stop' to shut down.\n")
175
+
176
+ # Interactive console
177
+ def do_shutdown():
178
+ print("\nShutting down cluster...")
179
+ shutdown_all(mgmt_port)
180
+ for name, proc, log in processes:
181
+ proc.terminate()
182
+ log.close()
183
+ try:
184
+ os.remove(STATE_FILE)
185
+ except FileNotFoundError:
186
+ pass
187
+ sys.exit(0)
188
+
189
+ signal.signal(signal.SIGINT, lambda sig, frame: do_shutdown())
190
+ signal.signal(signal.SIGTERM, lambda sig, frame: do_shutdown())
191
+
192
+ while True:
193
+ try:
194
+ raw = input("ubdcc> ").strip()
195
+ except (KeyboardInterrupt, EOFError):
196
+ do_shutdown()
197
+
198
+ if not raw:
199
+ continue
200
+ parts = raw.split(None, 1)
201
+ cmd = parts[0].lower()
202
+ arg = parts[1] if len(parts) > 1 else None
203
+
204
+ if cmd in ('status', '/status'):
205
+ try:
206
+ response = requests.get(f"http://127.0.0.1:{mgmt_port}/get_cluster_info", timeout=5)
207
+ data = response.json()
208
+ if data.get('result') == 'OK':
209
+ print()
210
+ print_status_table(data, mgmt_port=mgmt_port)
211
+ print()
212
+ except requests.exceptions.ConnectionError:
213
+ print("Cannot connect to mgmt.")
214
+ elif cmd in ('stop', '/stop', 'quit', 'exit'):
215
+ do_shutdown()
216
+ elif cmd in ('add-dcn', '/add-dcn'):
217
+ count = int(arg) if arg else 1
218
+ for _ in range(count):
219
+ nr, pid = spawn_dcn()
220
+ print(f" dcn-{nr} started (PID {pid})")
221
+ print(f"Waiting for registration...")
222
+ time.sleep(3)
223
+ elif cmd in ('remove-dcn', '/remove-dcn'):
224
+ if not arg:
225
+ print("Usage: remove-dcn <count|pod-name>")
226
+ else:
227
+ try:
228
+ count = int(arg)
229
+ remove_dcn_by_count(mgmt_port, count, processes)
230
+ except ValueError:
231
+ remove_dcn(mgmt_port, arg, processes)
232
+ elif cmd in ('restart', '/restart'):
233
+ if not arg:
234
+ print("Usage: restart <pod-name|mgmt|restapi>")
235
+ elif arg in ('ubdcc-mgmt', 'mgmt'):
236
+ try:
237
+ requests.get(f"http://127.0.0.1:{mgmt_port}/shutdown", timeout=5)
238
+ except requests.exceptions.ConnectionError:
239
+ pass
240
+ for name, proc, log in processes:
241
+ if name == "mgmt":
242
+ proc.wait(timeout=10)
243
+ break
244
+ pid = spawn_mgmt()
245
+ print(f" mgmt restarted (PID {pid})")
246
+ print(" Waiting for mgmt to come up...")
247
+ for _ in range(30):
248
+ try:
249
+ requests.get(f"http://127.0.0.1:{mgmt_port}/test", timeout=2)
250
+ print(" mgmt is ready!")
251
+ break
252
+ except requests.exceptions.ConnectionError:
253
+ time.sleep(1)
254
+ elif arg in ('ubdcc-restapi', 'restapi'):
255
+ try:
256
+ data = requests.get(f"http://127.0.0.1:{mgmt_port}/get_cluster_info", timeout=5).json()
257
+ for uid, pod in data.get('db', {}).get('pods', {}).items():
258
+ if pod.get('ROLE') == 'ubdcc-restapi':
259
+ requests.get(f"http://127.0.0.1:{pod['API_PORT_REST']}/shutdown", timeout=3)
260
+ break
261
+ except requests.exceptions.ConnectionError:
262
+ pass
263
+ for name, proc, log in processes:
264
+ if name == "restapi":
265
+ proc.wait(timeout=10)
266
+ break
267
+ pid = spawn_restapi()
268
+ print(f" restapi restarted (PID {pid})")
269
+ else:
270
+ restart_pod(mgmt_port, arg)
271
+ nr, pid = spawn_dcn()
272
+ print(f" dcn-{nr} respawned (PID {pid})")
273
+ elif cmd in ('help', '/help', '?'):
274
+ print()
275
+ print("Available commands:")
276
+ print(" status Show cluster status")
277
+ print(" add-dcn [count] Spawn new DCN process(es)")
278
+ print(" remove-dcn <count|name> Stop and remove DCN(s)")
279
+ print(" restart <name> Restart a specific pod")
280
+ print(" stop Shut down the cluster")
281
+ print(" help Show this help")
282
+ print()
283
+ else:
284
+ print(f"Unknown command: {cmd}. Type 'help' for available commands.")
285
+
286
+
287
+ def cmd_status(args):
288
+ mgmt_port = get_mgmt_port(args)
289
+ url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
290
+ try:
291
+ response = requests.get(url, timeout=5)
292
+ data = response.json()
293
+ if data.get('result') == 'OK':
294
+ print_status_table(data, mgmt_port=mgmt_port)
295
+ else:
296
+ print(f"Error: {data.get('message', 'Unknown error')}")
297
+ except requests.exceptions.ConnectionError:
298
+ print(f"Cannot connect to mgmt on port {mgmt_port}. Is the cluster running?")
299
+
300
+
301
+ def cmd_stop(args):
302
+ mgmt_port = get_mgmt_port(args)
303
+ shutdown_all(mgmt_port)
304
+
305
+
306
+ def cmd_restart(args):
307
+ mgmt_port = get_mgmt_port(args)
308
+ restart_pod(mgmt_port, args.name)
309
+
310
+
311
+ def remove_dcn_by_count(mgmt_port, count, processes):
312
+ """Stop N DCN pods."""
313
+ url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
314
+ try:
315
+ response = requests.get(url, timeout=5)
316
+ data = response.json()
317
+ except requests.exceptions.ConnectionError:
318
+ print(f"Cannot connect to mgmt on port {mgmt_port}.")
319
+ return
320
+
321
+ pods = data.get('db', {}).get('pods', {})
322
+ dcns = [(uid, pod) for uid, pod in pods.items() if pod.get('ROLE') == 'ubdcc-dcn']
323
+ if count > len(dcns):
324
+ print(f"Only {len(dcns)} DCN(s) running, cannot remove {count}.")
325
+ return
326
+
327
+ removed = 0
328
+ for uid, pod in dcns:
329
+ if removed >= count:
330
+ break
331
+ port = pod['API_PORT_REST']
332
+ name = pod['NAME']
333
+ try:
334
+ requests.get(f"http://127.0.0.1:{port}/shutdown", timeout=5)
335
+ print(f" Removed: {name} (port {port})")
336
+ removed += 1
337
+ except requests.exceptions.ConnectionError:
338
+ print(f" Warning: Could not reach {name} on port {port}")
339
+
340
+
341
+ def remove_dcn(mgmt_port, target, processes):
342
+ """Stop a DCN pod and remove it from the process list."""
343
+ url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
344
+ try:
345
+ response = requests.get(url, timeout=5)
346
+ data = response.json()
347
+ except requests.exceptions.ConnectionError:
348
+ print(f"Cannot connect to mgmt on port {mgmt_port}.")
349
+ return
350
+
351
+ pods = data.get('db', {}).get('pods', {})
352
+ for uid, pod in pods.items():
353
+ if pod['NAME'] == target or uid == target:
354
+ if pod.get('ROLE') != 'ubdcc-dcn':
355
+ print(f"'{target}' is not a DCN. Use 'remove-dcn' only for DCN pods.")
356
+ return
357
+ port = pod['API_PORT_REST']
358
+ name = pod['NAME']
359
+ try:
360
+ requests.get(f"http://127.0.0.1:{port}/shutdown", timeout=5)
361
+ print(f" Removed: {name} (port {port})")
362
+ except requests.exceptions.ConnectionError:
363
+ print(f" Warning: Could not reach {name} on port {port}")
364
+ # Remove from local process list
365
+ for i, (pname, proc, log) in enumerate(processes):
366
+ if proc.poll() is not None or pname.startswith('dcn'):
367
+ # Can't match by name since local names differ from pod names
368
+ # Just clean up dead processes
369
+ pass
370
+ return
371
+
372
+ print(f"Pod '{target}' not found. Use 'status' to see available pods.")
373
+
374
+
375
+ def restart_pod(mgmt_port, target):
376
+ """Send shutdown to a specific pod by name or UID."""
377
+ # Handle mgmt separately — it doesn't register itself in the pod list
378
+ if target in ('ubdcc-mgmt', 'mgmt'):
379
+ try:
380
+ requests.get(f"http://127.0.0.1:{mgmt_port}/shutdown", timeout=5)
381
+ print(f"Shutdown signal sent to mgmt on port {mgmt_port}.")
382
+ print(f"Note: Restart the process manually or re-run 'ubdcc start'.")
383
+ except requests.exceptions.ConnectionError:
384
+ print(f"Cannot connect to mgmt on port {mgmt_port}.")
385
+ return
386
+
387
+ url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
388
+ try:
389
+ response = requests.get(url, timeout=5)
390
+ data = response.json()
391
+ except requests.exceptions.ConnectionError:
392
+ print(f"Cannot connect to mgmt on port {mgmt_port}. Is the cluster running?")
393
+ return
394
+
395
+ pods = data.get('db', {}).get('pods', {})
396
+ for uid, pod in pods.items():
397
+ if pod['NAME'] == target or uid == target:
398
+ port = pod['API_PORT_REST']
399
+ name = pod['NAME']
400
+ try:
401
+ requests.get(f"http://127.0.0.1:{port}/shutdown", timeout=5)
402
+ print(f"Shutdown signal sent to '{name}' on port {port}.")
403
+ print(f"Note: Restart the process manually or re-run 'ubdcc start'.")
404
+ except requests.exceptions.ConnectionError:
405
+ print(f"Cannot connect to '{name}' on port {port}.")
406
+ return
407
+
408
+ print(f"Pod '{target}' not found. Use 'status' to see available pods.")
409
+
410
+
411
+ def shutdown_all(mgmt_port):
412
+ """Shutdown all pods via their /shutdown endpoints."""
413
+ url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
414
+ try:
415
+ response = requests.get(url, timeout=5)
416
+ data = response.json()
417
+ except requests.exceptions.ConnectionError:
418
+ print(f"Cannot connect to mgmt on port {mgmt_port}.")
419
+ return
420
+
421
+ pods = data.get('db', {}).get('pods', {})
422
+ for uid, pod in pods.items():
423
+ port = pod['API_PORT_REST']
424
+ name = pod['NAME']
425
+ try:
426
+ requests.get(f"http://127.0.0.1:{port}/shutdown", timeout=3)
427
+ print(f" Shutdown: {name} ({pod['ROLE']}) on port {port}")
428
+ except requests.exceptions.ConnectionError:
429
+ print(f" Warning: Could not reach {name} on port {port}")
430
+
431
+ # Shutdown mgmt last
432
+ try:
433
+ requests.get(f"http://127.0.0.1:{mgmt_port}/shutdown", timeout=3)
434
+ print(f" Shutdown: mgmt on port {mgmt_port}")
435
+ except requests.exceptions.ConnectionError:
436
+ print(f" Warning: Could not reach mgmt on port {mgmt_port}")
437
+
438
+ print("Cluster shutdown complete.")
439
+
440
+
441
+ def print_status_table(data, mgmt_port=42080):
442
+ """Print a formatted status table from cluster info."""
443
+ pods = data.get('db', {}).get('pods', {})
444
+
445
+ print(f"{'ROLE':<16} {'NAME':<20} {'PORT':<8} {'STATUS':<10} {'VERSION'}")
446
+ print("-" * 70)
447
+
448
+ # Get mgmt info via /test endpoint
449
+ try:
450
+ mgmt_response = requests.get(f"http://127.0.0.1:{mgmt_port}/test", timeout=3)
451
+ mgmt_data = mgmt_response.json()
452
+ mgmt_name = mgmt_data.get('app', {}).get('name', '?')
453
+ mgmt_version = mgmt_data.get('app', {}).get('version', '?')
454
+ print(f"{'ubdcc-mgmt':<16} {mgmt_name:<20} {mgmt_port:<8} {'running':<10} {mgmt_version}")
455
+ except requests.exceptions.ConnectionError:
456
+ print(f"{'ubdcc-mgmt':<16} {'?':<20} {mgmt_port:<8} {'down':<10} {'?'}")
457
+
458
+ # Sort: restapi first, then dcn
459
+ role_order = {'ubdcc-restapi': 0, 'ubdcc-dcn': 1}
460
+ sorted_pods = sorted(pods.values(), key=lambda p: (role_order.get(p.get('ROLE', ''), 9), p.get('NAME', '')))
461
+
462
+ restapi_port = None
463
+ for pod in sorted_pods:
464
+ role = pod.get('ROLE', '?')
465
+ name = pod.get('NAME', '?')
466
+ port = pod.get('API_PORT_REST', '?')
467
+ status = pod.get('STATUS', '?')
468
+ version = pod.get('VERSION', '?')
469
+ print(f"{role:<16} {name:<20} {port:<8} {status:<10} {version}")
470
+ if role == 'ubdcc-restapi' and restapi_port is None:
471
+ restapi_port = port
472
+
473
+ depthcaches = data.get('db', {}).get('depthcaches', {})
474
+ dc_count = sum(len(markets) for markets in depthcaches.values())
475
+ print(f"\nDepthCaches: {dc_count}")
476
+ print(f"Version: {data.get('version', '?')}")
477
+ if restapi_port:
478
+ print(f"\nREST API: http://127.0.0.1:{restapi_port}/")
479
+ print(f"Cluster info: http://127.0.0.1:{restapi_port}/get_cluster_info")
480
+
481
+
482
+ def main():
483
+ parser = argparse.ArgumentParser(
484
+ prog='ubdcc',
485
+ description='UNICORN Binance DepthCache Cluster — Cluster Manager\n'
486
+ 'https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster',
487
+ epilog='Interactive shell commands (available inside "ubdcc start"):\n'
488
+ ' add-dcn [count] Spawn new DCN process(es)\n'
489
+ ' remove-dcn <count|name> Stop and remove DCN(s)\n'
490
+ ' status Show cluster status\n'
491
+ ' restart <name> Restart a specific pod\n'
492
+ ' stop Shut down the cluster\n'
493
+ ' help Show available shell commands',
494
+ formatter_class=argparse.RawDescriptionHelpFormatter
495
+ )
496
+ parser.add_argument('-v', '--version', action='version', version=f'ubdcc {__version__}')
497
+
498
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
499
+
500
+ # start
501
+ start_parser = subparsers.add_parser('start', help='Start the cluster')
502
+ start_parser.add_argument('--dcn', type=int, default=1, help='Number of DCN processes (default: 1)')
503
+ start_parser.add_argument('--port', type=int, default=None, help='Mgmt port (default: 42080 or next free)')
504
+ start_parser.add_argument('--logdir', type=str, default=None, help='Log directory (default: current directory)')
505
+
506
+ # status
507
+ status_parser = subparsers.add_parser('status', help='Show cluster status')
508
+ status_parser.add_argument('--port', type=int, default=None, help='Mgmt port (default: 42080)')
509
+
510
+ # stop
511
+ stop_parser = subparsers.add_parser('stop', help='Stop the cluster')
512
+ stop_parser.add_argument('--port', type=int, default=None, help='Mgmt port (default: 42080)')
513
+
514
+ # restart
515
+ restart_parser = subparsers.add_parser('restart', help='Restart a specific pod')
516
+ restart_parser.add_argument('name', help='Pod name or UID to restart')
517
+ restart_parser.add_argument('--port', type=int, default=None, help='Mgmt port (default: 42080)')
518
+
519
+ args = parser.parse_args()
520
+
521
+ if args.command == 'start':
522
+ cmd_start(args)
523
+ elif args.command == 'status':
524
+ cmd_status(args)
525
+ elif args.command == 'stop':
526
+ cmd_stop(args)
527
+ elif args.command == 'restart':
528
+ cmd_restart(args)
529
+ else:
530
+ parser.print_help()
531
+
532
+
533
+ if __name__ == "__main__":
534
+ main()
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: ubdcc
3
+ Version: 0.3.1
4
+ Summary: UNICORN Binance DepthCache Cluster — cluster manager and meta-package
5
+ Home-page: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
6
+ Author: Oliver Zehentleitner
7
+ Author-email:
8
+ License: MIT
9
+ Project-URL: Howto, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster#howto
10
+ Project-URL: Documentation, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
11
+ Project-URL: Wiki, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/wiki
12
+ Project-URL: Author, https://www.linkedin.com/in/oliver-zehentleitner
13
+ Project-URL: Changes, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/packages/ubdcc/CHANGELOG.md
14
+ Project-URL: License, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/LICENSE
15
+ Project-URL: Issue Tracker, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues
16
+ Project-URL: Telegram, https://t.me/unicorndevs
17
+ Keywords: binance,depth cache,cluster,order book
18
+ Classifier: Development Status :: 4 - Beta
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: License :: OSI Approved :: MIT License
26
+ Classifier: Intended Audience :: Developers
27
+ Classifier: Intended Audience :: Financial and Insurance Industry
28
+ Classifier: Intended Audience :: Information Technology
29
+ Classifier: Intended Audience :: Science/Research
30
+ Classifier: Operating System :: OS Independent
31
+ Classifier: Topic :: Office/Business :: Financial :: Investment
32
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
+ Classifier: Framework :: AsyncIO
34
+ Requires-Python: >=3.9.0
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: ubdcc-mgmt
38
+ Requires-Dist: ubdcc-restapi
39
+ Requires-Dist: ubdcc-dcn
40
+ Dynamic: author
41
+ Dynamic: classifier
42
+ Dynamic: description
43
+ Dynamic: description-content-type
44
+ Dynamic: home-page
45
+ Dynamic: keywords
46
+ Dynamic: license
47
+ Dynamic: license-file
48
+ Dynamic: project-url
49
+ Dynamic: requires-dist
50
+ Dynamic: requires-python
51
+ Dynamic: summary
52
+
53
+ [![GitHub Release](https://img.shields.io/github/release/oliver-zehentleitner/unicorn-binance-depth-cache-cluster.svg?label=github)](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/releases)
54
+ [![GitHub Downloads](https://img.shields.io/github/downloads/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/total?color=blue)](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/releases)
55
+ [![PyPi Release](https://img.shields.io/pypi/v/ubdcc?color=blue)](https://pypi.org/project/ubdcc/)
56
+ [![PyPi Downloads](https://pepy.tech/badge/ubdcc)](https://pepy.tech/project/ubdcc)
57
+ [![License](https://img.shields.io/badge/license-MIT-blue)](https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster/license.html)
58
+ [![Supported Python Version](https://img.shields.io/pypi/pyversions/ubdcc.svg)](https://www.python.org/downloads/)
59
+ [![PyPI - Status](https://img.shields.io/pypi/status/ubdcc.svg)](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues)
60
+ [![Build and Publish PyPi](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/actions/workflows/build_wheels_ubdcc.yml/badge.svg)](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/actions/workflows/build_wheels_ubdcc.yml)
61
+ [![Read the Docs](https://img.shields.io/badge/read-%20docs-yellow)](https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster)
62
+ [![Github](https://img.shields.io/badge/source-github-cbc2c8)](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/tree/master/packages/ubdcc)
63
+ [![Telegram](https://img.shields.io/badge/community-telegram-41ab8c)](https://t.me/unicorndevs)
64
+
65
+ # UNICORN Binance DepthCache Cluster
66
+
67
+ The `ubdcc` package is the all-in-one installer and cluster manager for the
68
+ [UNICORN Binance DepthCache Cluster](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster).
69
+
70
+ ## What you get
71
+
72
+ - **All components installed**: `ubdcc-mgmt`, `ubdcc-restapi`, `ubdcc-dcn` and their dependencies
73
+ - **`ubdcc` command**: Cluster manager to start, stop and monitor your UBDCC instance (work in progress)
74
+
75
+ ## Quick Start
76
+
77
+ ```bash
78
+ pip install ubdcc
79
+ ```
80
+
81
+ This installs everything you need. Start the cluster:
82
+
83
+ ```bash
84
+ ubdcc start --dcn 4
85
+ ```
86
+
87
+ This starts 1 mgmt + 1 restapi + 4 DCN processes and drops you into an interactive console where you can monitor
88
+ and manage the cluster (`status`, `stop`, `restart <name>`, `help`).
89
+
90
+ The REST API is available at `http://127.0.0.1:42081/`.
91
+
92
+ ### Development (without pip install)
93
+
94
+ When working on the source code, run directly from the package directory:
95
+
96
+ ```bash
97
+ cd packages/ubdcc
98
+ python -m ubdcc start --dcn 4
99
+ python -m ubdcc status
100
+ python -m ubdcc stop
101
+ ```
102
+
103
+ For full documentation, architecture overview, REST API reference and Kubernetes setup, see the
104
+ [main project README](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster).
105
+
106
+ ## How to report Bugs or suggest Improvements?
107
+ [List of planned features](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) - click ![thumbs-up](https://raw.githubusercontent.com/oliver-zehentleitner/unicorn-binance-suite/master/images/misc/thumbup.png) if you need one of them or suggest a new feature!
108
+
109
+ Before you report a bug, [try the latest release](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster#installation-and-upgrade). If the issue still exists, provide the error trace, OS
110
+ and Python version and explain how to reproduce the error. A demo script is appreciated.
111
+
112
+ If you don't find an issue related to your topic, please open a new [issue](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues)!
113
+
114
+ [Report a security bug!](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/security/policy)
115
+
116
+ ## Contributing
117
+ [UNICORN Binance DepthCache Cluster](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster) is an open
118
+ source project which welcomes contributions which can be anything from simple documentation fixes and reporting dead links to new features. To
119
+ contribute follow
120
+ [this guide](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/CONTRIBUTING.md).
121
+
122
+ ### Contributors
123
+ [![Contributors](https://contributors-img.web.app/image?repo=oliver-zehentleitner/unicorn-binance-depth-cache-cluster)](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/graphs/contributors)
124
+
125
+ We ![love](https://raw.githubusercontent.com/oliver-zehentleitner/unicorn-binance-suite/master/images/misc/heart.png) open source!
126
+
127
+ ## Disclaimer
128
+ This project is for informational purposes only. You should not construe this information or any other material as
129
+ legal, tax, investment, financial or other advice. Nothing contained herein constitutes a solicitation, recommendation,
130
+ endorsement or offer by us or any third party provider to buy or sell any securities or other financial instruments in
131
+ this or any other jurisdiction in which such solicitation or offer would be unlawful under the securities laws of such
132
+ jurisdiction.
133
+
134
+ ### If you intend to use real money, use it at your own risk!
135
+
136
+ Under no circumstances will we be responsible or liable for any claims, damages, losses, expenses, costs or liabilities
137
+ of any kind, including but not limited to direct or indirect damages for loss of profits.
@@ -0,0 +1,9 @@
1
+ ubdcc/__init__.py,sha256=Jj7rFspXn8HVd2jNgKSHeNtIdjDBP1bf2CMIYak1OLM,704
2
+ ubdcc/__main__.py,sha256=0zN_oeN02xzMGvpVDK6l-9IDKKxcSknpKs8wM0ZVgnk,35
3
+ ubdcc/cli.py,sha256=pwWQIUEcJ0Y-XFIRcYVYzmbZr6wyHtQ0R2QTYxkj6jA,20124
4
+ ubdcc-0.3.1.dist-info/licenses/LICENSE,sha256=ucnbASA25rhSF9cHkE9Eb-q58uXbYLtdaZyyVkjSn9M,1123
5
+ ubdcc-0.3.1.dist-info/METADATA,sha256=Pr0pLk56bPh186F7s8voKjw8Usx9YDte0ZO68uu6ZjE,7868
6
+ ubdcc-0.3.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ ubdcc-0.3.1.dist-info/entry_points.txt,sha256=dtVonT-ML3jK0oxr2hD9hifJBzcaA2EGs5FsIc8f_Vs,41
8
+ ubdcc-0.3.1.dist-info/top_level.txt,sha256=xUIKyETAc7TVepxCLkHXfiD8-e65oNVs52VGSRPO_jU,6
9
+ ubdcc-0.3.1.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,2 @@
1
+ [console_scripts]
2
+ ubdcc = ubdcc.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026, Oliver Zehentleitner (https://about.me/oliver-zehentleitner)
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.
@@ -0,0 +1 @@
1
+ ubdcc