camera-ui-rpc 1.0.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.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020-2024 seydx <dev@seydx.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,547 @@
1
+ Metadata-Version: 2.4
2
+ Name: camera-ui-rpc
3
+ Version: 1.0.0
4
+ Summary: RPC Client for camera.ui
5
+ Author-email: seydx <dev@seydx.com>
6
+ Maintainer-email: seydx <dev@seydx.com>
7
+ Project-URL: Homepage, https://github.com/seydx/camera.ui
8
+ Project-URL: Documentation, https://github.com/seydx/camera.ui
9
+ Project-URL: Repository, https://github.com/seydx/camera.ui
10
+ Project-URL: Bug Tracker, https://github.com/seydx/camera.ui/issues
11
+ Project-URL: Changelog, https://github.com/seydx/camera.ui/blob/master/CHANGELOG.md
12
+ Keywords: camera.ui,python,nats,rpc
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE.md
23
+ Requires-Dist: typing_extensions>=4.12.2
24
+ Requires-Dist: nats-py>=2.11.0
25
+ Requires-Dist: ormsgpack>=1.10.0
26
+ Dynamic: license-file
27
+
28
+ # RPC Library for Python
29
+
30
+ A high-performance, type-safe RPC library built on NATS messaging system for Python applications.
31
+
32
+ ## Features
33
+
34
+ - 🚀 **High Performance**: Achieves 300-1500+ MB/s throughput with sub-millisecond latency
35
+ - 🔒 **Type Safety**: Full type annotations and runtime type checking
36
+ - 🌊 **Streaming**: Async generators with push/pull patterns
37
+ - 🔄 **Auto-reconnection**: Resilient connection management
38
+ - 📦 **Auto-chunking**: Transparent handling of large payloads
39
+ - 🎯 **Service Discovery**: Automatic service registration and discovery
40
+ - ⚖️ **Load Balancing**: Built-in queue-based load distribution
41
+ - 🔀 **Channels**: Bidirectional real-time communication
42
+ - 🎨 **Decorators**: Clean API with Python decorators
43
+ - 🤝 **Cross-language**: Full compatibility with TypeScript implementation
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install camera-ui-rpc
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ### Basic RPC Handlers
54
+
55
+ For simple RPC endpoints without service discovery:
56
+
57
+ ```python
58
+ from camera_ui_rpc import create_rpc_client, RPCClass
59
+ import asyncio
60
+
61
+ # Define your handlers
62
+ @RPCClass
63
+ class MathHandlers:
64
+ async def add(self, a: float, b: float) -> float:
65
+ return a + b
66
+
67
+ async def multiply(self, a: float, b: float) -> float:
68
+ return a * b
69
+
70
+ # Streaming method
71
+ async def fibonacci(self, n: int):
72
+ a, b = 0, 1
73
+ for _ in range(n):
74
+ yield a
75
+ a, b = b, a + b
76
+
77
+ async def main():
78
+ # Server: Register RPC handlers
79
+ server = create_rpc_client({
80
+ 'servers': ['nats://localhost:4222'],
81
+ 'name': 'math-server'
82
+ })
83
+
84
+ await server.connect()
85
+
86
+ # Register handlers under a namespace
87
+ await server.register_handler('math', MathHandlers())
88
+
89
+ print('Math RPC handlers registered')
90
+ await asyncio.Event().wait()
91
+
92
+ asyncio.run(main())
93
+ ```
94
+
95
+ ```python
96
+ # Client: Use RPC proxy
97
+ async def main():
98
+ client = create_rpc_client({
99
+ 'servers': ['nats://localhost:4222'],
100
+ 'name': 'math-client'
101
+ })
102
+
103
+ await client.connect()
104
+
105
+ # Create a typed proxy (no service discovery)
106
+ math = client.create_proxy('math')
107
+
108
+ # Call methods
109
+ result = await math.add(5, 3)
110
+ print(f'5 + 3 = {result}') # 8
111
+
112
+ # Use streaming
113
+ async for num in math.fibonacci(10):
114
+ print(f'Fibonacci: {num}')
115
+
116
+ asyncio.run(main())
117
+ ```
118
+
119
+ ### NATS Micro Services
120
+
121
+ For production services with discovery, monitoring, and load balancing:
122
+
123
+ ```python
124
+ from camera_ui_rpc import ServiceConfig
125
+
126
+ # Server: Register as NATS micro service
127
+ async def main():
128
+ server = create_rpc_client({
129
+ 'servers': ['nats://localhost:4222'],
130
+ 'name': 'math-service'
131
+ })
132
+
133
+ await server.connect()
134
+
135
+ # Register as a proper NATS micro service
136
+ await server.service.register_handler(
137
+ ServiceConfig(
138
+ name='math',
139
+ version='1.0.0',
140
+ description='Math operations service',
141
+ queue_group='math-workers' # For load balancing
142
+ ),
143
+ MathHandlers()
144
+ )
145
+
146
+ print('Math micro service is running')
147
+ await asyncio.Event().wait()
148
+ ```
149
+
150
+ ```python
151
+ # Client: Discover and use service
152
+ async def main():
153
+ client = create_rpc_client({
154
+ 'servers': ['nats://localhost:4222'],
155
+ 'name': 'client'
156
+ })
157
+
158
+ await client.connect()
159
+
160
+ # Discover service by name (with load balancing)
161
+ math = await client.create_service_proxy('math')
162
+
163
+ # Use the service
164
+ result = await math.add(10, 20)
165
+ ```
166
+
167
+ ## Core Concepts
168
+
169
+ ### RPC Client
170
+
171
+ The foundation of all operations:
172
+
173
+ ```python
174
+ from camera_ui_rpc import create_rpc_client
175
+
176
+ client = create_rpc_client({
177
+ 'servers': ['nats://localhost:4222'],
178
+ 'name': 'my-app',
179
+ 'auth': {'user': 'app', 'pass': 'secret'},
180
+ 'timeout': 5000, # 5 second default timeout
181
+ 'reconnect': True,
182
+ 'maxReconnectAttempts': -1 # infinite
183
+ })
184
+
185
+ await client.connect()
186
+ ```
187
+
188
+ ### RPC Handlers vs NATS Services
189
+
190
+ **RPC Handlers** (`register_handler`):
191
+ - Simple namespace-based RPC endpoints
192
+ - Direct addressing via namespace
193
+ - No automatic discovery or load balancing
194
+ - Lightweight for internal communication
195
+
196
+ **NATS Micro Services** (`service.register_handler`):
197
+ - Full NATS micro service features
198
+ - Service discovery via name
199
+ - Automatic load balancing with queue groups
200
+ - Monitoring and stats
201
+ - Ideal for production microservices
202
+
203
+ ### Handler Organization
204
+
205
+ ```python
206
+ from camera_ui_rpc import RPCClass, RPCNested
207
+ from typing import Dict, List, Optional
208
+
209
+ @RPCClass
210
+ class UserHandlers:
211
+ def __init__(self):
212
+ self.users: Dict[str, User] = {}
213
+
214
+ async def get_user(self, user_id: str) -> Optional[User]:
215
+ return self.users.get(user_id)
216
+
217
+ async def create_user(self, data: UserData) -> User:
218
+ user = User(id=generate_id(), **data)
219
+ self.users[user.id] = user
220
+ return user
221
+
222
+ # Nested object for organization
223
+ @RPCNested
224
+ class admin:
225
+ def __init__(self, parent):
226
+ self.parent = parent
227
+
228
+ async def list_users(self) -> List[User]:
229
+ return list(self.parent.users.values())
230
+
231
+ async def delete_user(self, user_id: str) -> bool:
232
+ return self.parent.users.pop(user_id, None) is not None
233
+
234
+ # Register as handlers or service
235
+ await client.register_handler('users', UserHandlers()) # Simple RPC
236
+ # OR
237
+ await client.service.register_handler( # NATS service
238
+ ServiceConfig(
239
+ name='users',
240
+ version='1.0.0',
241
+ queue_group='user-workers'
242
+ ),
243
+ UserHandlers()
244
+ )
245
+ ```
246
+
247
+ ### Channels
248
+
249
+ Real-time bidirectional communication between multiple clients:
250
+
251
+ ```python
252
+ # Server/Client A - Join channel and listen
253
+ client_a = create_rpc_client({'servers': ['nats://localhost:4222'], 'name': 'client-a'})
254
+ await client_a.connect()
255
+
256
+ chat_a = await client_a.channel('room:general')
257
+
258
+ def on_message_a(msg):
259
+ print(f"[Client A received] {msg['user']}: {msg['text']}")
260
+
261
+ chat_a.on('message', on_message_a)
262
+
263
+ # Client B - Join same channel
264
+ client_b = create_rpc_client({'servers': ['nats://localhost:4222'], 'name': 'client-b'})
265
+ await client_b.connect()
266
+
267
+ chat_b = await client_b.channel('room:general')
268
+
269
+ def on_message_b(msg):
270
+ print(f"[Client B received] {msg['user']}: {msg['text']}")
271
+
272
+ chat_b.on('message', on_message_b)
273
+
274
+ # Send messages - all clients in channel receive them
275
+ await chat_a.send({'user': 'Alice', 'text': 'Hello everyone!'})
276
+ # Output on Client B: [Client B received] Alice: Hello everyone!
277
+
278
+ await chat_b.send({'user': 'Bob', 'text': 'Hi Alice!'})
279
+ # Output on Client A: [Client A received] Bob: Hi Alice!
280
+
281
+ # Request/Reply pattern in channels
282
+ # Client B handles requests
283
+ async def handle_info_request(msg):
284
+ if msg.get('type') == 'get-info':
285
+ return {'users': 2, 'topic': 'general chat'}
286
+
287
+ await chat_b.on_request(handle_info_request)
288
+
289
+ # Client A makes request
290
+ info = await chat_a.request({'type': 'get-info'})
291
+ print(f"Channel info: {info}") # {'users': 2, 'topic': 'general chat'}
292
+ ```
293
+
294
+ ### Private Channels
295
+
296
+ Direct one-to-one communication between specific clients:
297
+
298
+ ```python
299
+ # Client A (Alice) - Create private channel to Bob
300
+ alice_client = create_rpc_client({'servers': ['nats://localhost:4222'], 'name': 'alice'})
301
+ await alice_client.connect()
302
+
303
+ # Both clients must use the same channel_id and specify the target client
304
+ alice_to_bob = await alice_client.private_channel('secret-chat', 'bob')
305
+
306
+ def on_alice_message(msg):
307
+ print(f"[Alice received from Bob] {msg}")
308
+
309
+ alice_to_bob.on('message', on_alice_message)
310
+
311
+ # Client B (Bob) - Create private channel to Alice
312
+ bob_client = create_rpc_client({'servers': ['nats://localhost:4222'], 'name': 'bob'})
313
+ await bob_client.connect()
314
+
315
+ # Bob must use the same channel_id ('secret-chat') to connect
316
+ bob_to_alice = await bob_client.private_channel('secret-chat', 'alice')
317
+
318
+ def on_bob_message(msg):
319
+ print(f"[Bob received from Alice] {msg}")
320
+
321
+ bob_to_alice.on('message', on_bob_message)
322
+
323
+ # Exchange private messages
324
+ await alice_to_bob.send({'text': 'Hi Bob, this is private!', 'timestamp': '10:00'})
325
+ # Output: [Bob received from Alice] {'text': 'Hi Bob, this is private!', 'timestamp': '10:00'}
326
+
327
+ await bob_to_alice.send({'text': 'Hi Alice, got your message!', 'timestamp': '10:01'})
328
+ # Output: [Alice received from Bob] {'text': 'Hi Alice, got your message!', 'timestamp': '10:01'}
329
+
330
+ # Private channels are isolated - other clients cannot see these messages
331
+ charlie_client = create_rpc_client({'servers': ['nats://localhost:4222'], 'name': 'charlie'})
332
+ await charlie_client.connect()
333
+ # Charlie cannot see Alice-Bob messages even if trying to listen
334
+ ```
335
+
336
+ ## Advanced Features
337
+
338
+ ### Streaming
339
+
340
+ Two streaming patterns for different use cases:
341
+
342
+ ```python
343
+ # Push-based (server controls flow) - better performance
344
+ async def generate_data(self, count: int):
345
+ # Method name includes "generate" for push-based iteration
346
+ for i in range(count):
347
+ yield {'index': i, 'data': b'x' * (1024 * 1024)} # 1MB
348
+
349
+ # Pull-based (client controls flow) - better backpressure
350
+ async def pull_data(self, count: int):
351
+ # Method name includes "pull" for pull-based iteration
352
+ for i in range(count):
353
+ yield {'index': i, 'data': await self.load_data(i)}
354
+
355
+ # Client usage
356
+ service = client.create_proxy('data')
357
+
358
+ async for item in service.generate_data(100):
359
+ # Process as fast as server sends
360
+ pass
361
+
362
+ async for item in service.pull_data(100):
363
+ # Pull items at client's pace
364
+ await process_item(item)
365
+ ```
366
+
367
+ ### Error Handling
368
+
369
+ ```python
370
+ from camera_ui_rpc import RPCException, ErrorCode
371
+
372
+ service = client.create_proxy('myservice')
373
+
374
+ try:
375
+ await service.some_method()
376
+ except RPCException as e:
377
+ if e.code == ErrorCode.METHOD_NOT_FOUND:
378
+ print('Method does not exist')
379
+ elif e.code == ErrorCode.TIMEOUT:
380
+ print('Request timed out')
381
+ elif e.code == ErrorCode.CONNECTION_CLOSED:
382
+ print('Connection lost')
383
+ ```
384
+
385
+ ### Auto-chunking
386
+
387
+ Large payloads are automatically chunked:
388
+
389
+ ```python
390
+ service = client.create_proxy('data')
391
+
392
+ # Automatically chunks data > server limit
393
+ large_data = b'x' * (100 * 1024 * 1024) # 100MB
394
+ await service.process_large_data(large_data) # Works transparently!
395
+ ```
396
+
397
+ ### Isolated Connections
398
+
399
+ Isolated connections provide separate NATS connections for specific operations, preventing blocking of the main connection. They're available for:
400
+
401
+ - **Proxies**: `create_proxy()` and `create_service_proxy()`
402
+ - **Handlers**: `register_handler()`
403
+ - **Channels**: `channel()` and `private_channel()`
404
+
405
+ ```python
406
+ # 1. Isolated proxy (returns object with proxy and close method)
407
+ isolated = client.create_proxy('service', isolated_connection=True)
408
+ result = await isolated.proxy.heavy_computation(params)
409
+ await isolated.close() # Close when done
410
+
411
+ # 2. Isolated service proxy
412
+ service_isolated = await client.create_service_proxy('service',
413
+ isolated_connection=True
414
+ )
415
+ await service_isolated.proxy.process_large_dataset()
416
+ await service_isolated.close()
417
+
418
+ # 3. Isolated handler (for CPU-intensive services)
419
+ cleanup = await client.register_handler('heavy-service', handlers,
420
+ isolated_connection=True
421
+ )
422
+ # Later: await cleanup() to unregister and close
423
+
424
+ # 4. Isolated channel
425
+ channel = await client.channel('data-stream', isolated_connection=True)
426
+ ```
427
+
428
+ **Important**:
429
+ - Isolated connections must be closed explicitly when no longer needed
430
+ - Alternatively, closing the main client (`client.disconnect()`) will automatically close all isolated connections and channels
431
+ - Use isolated connections for: CPU-intensive operations, high-throughput streaming, or operations that might block
432
+
433
+ ### Property Access
434
+
435
+ Expose class properties for remote access using descriptors:
436
+
437
+ ```python
438
+ from camera_ui_rpc import RPCClass, RPCProperty
439
+
440
+ @RPCClass
441
+ class ConfigService:
442
+ # Use RPCProperty descriptor
443
+ name = RPCProperty()
444
+ version = RPCProperty()
445
+ max_connections = RPCProperty()
446
+
447
+ def __init__(self):
448
+ self.name = 'Config Service'
449
+ self.version = '1.0.0'
450
+ self.max_connections = 100
451
+
452
+ # Client usage
453
+ config = client.create_proxy('config')
454
+
455
+ # Read properties
456
+ name = await config.name
457
+ version = await config.version
458
+
459
+ # Update properties via setter methods
460
+ await config.setName('Updated Service')
461
+ await config.setMaxConnections(200)
462
+ ```
463
+
464
+ ## Configuration
465
+
466
+ ### Client Options
467
+
468
+ ```python
469
+ from camera_ui_rpc.types import RPCClientOptions
470
+
471
+ options: RPCClientOptions = {
472
+ 'servers': ['nats://localhost:4222'], # NATS servers
473
+ 'name': 'my-client', # Client identifier
474
+ 'auth': { # Authentication
475
+ 'user': 'username',
476
+ 'pass': 'password',
477
+ 'token': 'auth_token',
478
+ 'jwt': 'jwt_token'
479
+ },
480
+ 'tls': { # TLS configuration
481
+ 'cert': 'path/to/cert.pem',
482
+ 'key': 'path/to/key.pem',
483
+ 'ca': 'path/to/ca.pem'
484
+ },
485
+ 'timeout': 5000, # Default timeout (ms)
486
+ 'reconnect': True, # Auto-reconnect
487
+ 'max_reconnect_attempts': -1, # -1 for infinite
488
+ 'reconnect_time_wait': 2000, # Reconnect delay (ms)
489
+ 'max_payload_size': 4194304 # Override server limit
490
+ }
491
+ ```
492
+
493
+ ### Service Options
494
+
495
+ ```python
496
+ from camera_ui_rpc import ServiceConfig
497
+
498
+ service_config = ServiceConfig(
499
+ name='my-service', # Service name
500
+ version='1.0.0', # Semantic version
501
+ description='My service', # Service description
502
+ queue_group='my-service-queue', # Load balancing group
503
+ metadata={ # Custom metadata
504
+ 'region': 'us-east',
505
+ 'environment': 'production'
506
+ }
507
+ )
508
+
509
+ # Register service
510
+ await server.service.register_handler(service_config, MyHandlers())
511
+ ```
512
+
513
+ ## Cross-Language Compatibility
514
+
515
+ This library is fully compatible with the TypeScript/Node.js implementation:
516
+
517
+ - Identical wire protocol and message format
518
+ - Same feature set and API patterns
519
+ - Seamless service interoperability
520
+ - Shared channel communication
521
+
522
+ ### Example: Python calling TypeScript service
523
+
524
+ ```python
525
+ # TypeScript service running
526
+ math = await client.create_service_proxy('math-service')
527
+ result = await math.calculate(10, 20) # Works seamlessly!
528
+ ```
529
+
530
+ ## Examples
531
+
532
+ Check the `examples/` directory for complete examples:
533
+
534
+ - `service.py` - Basic service implementation
535
+ - `channel_communication.py` - Channel messaging
536
+ - `streaming.py` - Streaming patterns
537
+ - `multi_service.py` - Multiple services interaction
538
+ - `large_data_transfer.py` - Handling large payloads
539
+ - And 17 more examples...
540
+
541
+ ## License
542
+
543
+ MIT
544
+
545
+ ---
546
+
547
+ *Part of the camera.ui ecosystem - A comprehensive camera management solution.*