callserver 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.
@@ -0,0 +1,511 @@
1
+ """
2
+ CallServer for shared-memory RPC with inter-process function execution.
3
+
4
+ This module provides the CallServer class for making RPC calls between processes
5
+ on the same machine, using shared memory for efficient numpy array transfer.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import threading
12
+ from multiprocessing import Pipe, Process
13
+ from multiprocessing.connection import Client, Connection, Listener
14
+ from typing import Any, Callable, Optional, Tuple, Union
15
+
16
+ import numpy as np
17
+
18
+ from .allocationmanager import AllocationSetManager
19
+ from .allocationset import AllocationSet
20
+
21
+ DEFAULT_AUTHKEY = b"callserver"
22
+
23
+
24
+ class CallServer:
25
+ """
26
+ Client interface for shared-memory RPC with inter-process function execution.
27
+
28
+ CallServer enables remote or local function execution with shared memory argument
29
+ passing. It manages allocation sets for efficient inter-process communication,
30
+ supports asynchronous and synchronous calls, and can operate as a client
31
+ (connecting to an existing process) or spawn a child server process.
32
+
33
+ Key Features:
34
+ - Automatic promotion of large numpy arrays to shared memory
35
+ - Support for both synchronous and asynchronous calls
36
+ - Server-side state management via context objects
37
+ - Connection to child processes or existing servers via TCP
38
+
39
+ Limitations:
40
+ - Only suitable for inter-process communication on the same machine
41
+ (shared memory cannot be used across machines)
42
+ - Only one async call can be in progress at a time
43
+
44
+ Attributes:
45
+ name: The unique name for this server instance.
46
+ port: TCP port for remote connections (None for child process mode).
47
+ context: Server-side state object or initializer.
48
+
49
+ Example:
50
+ >>> import numpy as np
51
+ >>> from callserver import CallServer
52
+ >>> server = CallServer('adder')
53
+ >>> server.start()
54
+ >>> result = server.call(np.add, np.ones((1024, 1024)), np.ones((1024, 1024)))
55
+ >>> server.stop()
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ name: Union[str, Tuple[str, Union[int, Tuple[str, int]]]],
61
+ context: Optional[Any] = None,
62
+ *args: Any,
63
+ authkey: bytes = DEFAULT_AUTHKEY,
64
+ **kwargs: Any,
65
+ ) -> None:
66
+ """
67
+ Initialize a CallServer client.
68
+
69
+ Args:
70
+ name: Either a string name (spawns a child process), or a tuple of
71
+ (name, endpoint) where endpoint is a port number or (address, port)
72
+ tuple for connecting to an existing server.
73
+ context: Server-side context object, or a callable that will be
74
+ invoked on the server to create the context.
75
+ *args: Arguments passed to the context initializer.
76
+ authkey: Authentication key used when connecting to a standalone
77
+ server. Must match the key the server was started with.
78
+ **kwargs: Keyword arguments passed to the context initializer.
79
+ """
80
+ self.name = name
81
+ self.port: Optional[int] = None
82
+ self.address: str = "127.0.0.1"
83
+ if isinstance(name, tuple):
84
+ self.name, endpoint = name
85
+ self.address, self.port = (
86
+ endpoint if isinstance(endpoint, tuple) else ("127.0.0.1", endpoint)
87
+ )
88
+ self.context = context
89
+ self.context_args = args
90
+ self.context_kwargs = kwargs
91
+ self._alloc_manager = AllocationSetManager(self.name)
92
+ self._call_in_progress = False
93
+ self._started = False
94
+ if self.port:
95
+ self._pipe: Connection = Client(
96
+ (self.address, self.port), authkey=authkey
97
+ )
98
+ self._server_process: Optional[Process] = None
99
+ else:
100
+ self._pipe, self._server_pipe = Pipe()
101
+ self._server_process = Process(
102
+ name=self.name, target=run, args=(self._server_pipe,), daemon=True
103
+ )
104
+ self._last_result: Any = None
105
+ self._last_result_raw: Any = None
106
+ self._call_unpack_fn: Optional[Callable] = None
107
+ self._call_unpack_argset: Optional[AllocationSet] = None
108
+
109
+ def __repr__(self) -> str:
110
+ """Return a string representation of the CallServer."""
111
+ mode = "remote" if self.port else "child"
112
+ status = "started" if self._started else "stopped"
113
+ return f"CallServer(name={self.name!r}, mode={mode}, status={status})"
114
+
115
+ def __enter__(self) -> "CallServer":
116
+ """Enter context manager, starting the server."""
117
+ return self.start()
118
+
119
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
120
+ """Exit context manager, stopping the server."""
121
+ self.stop()
122
+
123
+ def start(self) -> "CallServer":
124
+ """
125
+ Start the server process or connect to a remote server.
126
+
127
+ For child process mode, spawns the server process. For remote mode,
128
+ sends the initial configuration to the server.
129
+
130
+ Returns:
131
+ self, for method chaining.
132
+ """
133
+ if self._server_process:
134
+ self._server_process.start()
135
+ self._pipe.send(
136
+ (self.name, self.context, self.context_args, self.context_kwargs)
137
+ )
138
+ self._started = True
139
+ return self
140
+
141
+ def add_allocation_set(self, fn: Union[Callable, str]) -> AllocationSet:
142
+ """
143
+ Add a new AllocationSet for the given function.
144
+
145
+ Use this to pre-allocate additional buffer sets for double-buffering
146
+ in async call patterns.
147
+
148
+ Args:
149
+ fn: A callable or string identifier for the function.
150
+
151
+ Returns:
152
+ The newly created AllocationSet.
153
+ """
154
+ return self._alloc_manager.add_allocation_set(fn)
155
+
156
+ @staticmethod
157
+ def alloc(
158
+ callserver: Union["CallServer", str],
159
+ fn: Union[Callable, str],
160
+ shape: Tuple[int, ...],
161
+ dtype: type = float,
162
+ alloc_set_id: int = 0,
163
+ ) -> np.ndarray:
164
+ """
165
+ Pre-allocate a shared memory buffer for a function.
166
+
167
+ This static method can be used from both client and server code to
168
+ pre-allocate buffers that will be used for argument passing.
169
+
170
+ Args:
171
+ callserver: A CallServer instance, or the server name string.
172
+ fn: A callable or string identifier for the function.
173
+ shape: Shape of the buffer to allocate.
174
+ dtype: Data type of the buffer (default: float).
175
+ alloc_set_id: Which allocation set to use (default: 0).
176
+
177
+ Returns:
178
+ A numpy array backed by shared memory.
179
+ """
180
+ alloc_mgr = (
181
+ callserver._alloc_manager
182
+ if isinstance(callserver, CallServer)
183
+ else AllocationSetManager(callserver)
184
+ )
185
+ argset = alloc_mgr.get_allocation_set(fn, alloc_set_id)
186
+ descriptor = argset.alloc("buffer", shape, dtype)
187
+ return argset.from_sharedmemory(descriptor)
188
+
189
+ def get_allocation_set(self, fn: Union[Callable, str]) -> AllocationSet:
190
+ """
191
+ Get the current AllocationSet for the given function.
192
+
193
+ Args:
194
+ fn: A callable or string identifier for the function.
195
+
196
+ Returns:
197
+ The current AllocationSet for the function.
198
+ """
199
+ return self._alloc_manager.get_allocation_set(fn)
200
+
201
+ def _call_async(
202
+ self,
203
+ fn: Callable,
204
+ argset: AllocationSet,
205
+ unpack_fn: Callable,
206
+ *args: Any,
207
+ **kwargs: Any,
208
+ ) -> None:
209
+ """
210
+ Internal method to initiate an async call.
211
+
212
+ Args:
213
+ fn: The function to call on the server.
214
+ argset: The AllocationSet containing the arguments.
215
+ unpack_fn: Function to convert the result from shared memory.
216
+ *args: Positional arguments (already converted to descriptors).
217
+ **kwargs: Keyword arguments (already converted to descriptors).
218
+ """
219
+ self._call_in_progress = True
220
+ self._call_unpack_fn = unpack_fn
221
+ self._call_unpack_argset = argset
222
+ self._pipe.send((fn, argset.ix, args, kwargs))
223
+
224
+ def call_async(self, fn: Callable, *args: Any, **kwargs: Any) -> None:
225
+ """
226
+ Initiate an asynchronous function call.
227
+
228
+ The call executes on the server in parallel. Use get_result() to
229
+ retrieve the result when ready.
230
+
231
+ Args:
232
+ fn: The function to call on the server.
233
+ *args: Positional arguments to pass to the function.
234
+ **kwargs: Keyword arguments to pass to the function.
235
+
236
+ Raises:
237
+ RuntimeError: If an async call is already in progress.
238
+ """
239
+ if self._call_in_progress:
240
+ raise RuntimeError(
241
+ "An async call is already in progress. Call get_result to "
242
+ "complete the current call before starting a new one."
243
+ )
244
+ arg_set = self.get_allocation_set(fn)
245
+ args, kwargs = arg_set.args_to_sharedmemory(*args, **kwargs)
246
+ self._call_async(fn, arg_set, AllocationSet.retval_from_sharedmemory, *args, **kwargs)
247
+
248
+ def call(self, fn: Callable, *args: Any, **kwargs: Any) -> Any:
249
+ """
250
+ Make a synchronous function call.
251
+
252
+ Equivalent to call_async() followed by get_result().
253
+
254
+ Args:
255
+ fn: The function to call on the server.
256
+ *args: Positional arguments to pass to the function.
257
+ **kwargs: Keyword arguments to pass to the function.
258
+
259
+ Returns:
260
+ The result of the function call.
261
+ """
262
+ self.call_async(fn, *args, **kwargs)
263
+ return self.get_result()
264
+
265
+ def call_in_progress(self) -> bool:
266
+ """
267
+ Check if an async call is currently in progress.
268
+
269
+ Returns:
270
+ True if a call is in progress, False otherwise.
271
+ """
272
+ return self._call_in_progress
273
+
274
+ def has_result(self) -> bool:
275
+ """
276
+ Check if a result is available without blocking.
277
+
278
+ Returns:
279
+ True if a result is ready to be retrieved, False otherwise.
280
+ """
281
+ return self._pipe.poll()
282
+
283
+ def wait_for_result(self, timeout: Optional[float] = None) -> bool:
284
+ """
285
+ Wait for a result to become available.
286
+
287
+ If no async call is in progress, returns True immediately (a previous
288
+ result may be available via get_result()).
289
+
290
+ Args:
291
+ timeout: Maximum time to wait in seconds. None means wait forever.
292
+
293
+ Returns:
294
+ True if a result is available (or no call is in progress),
295
+ False if timeout expired while waiting.
296
+ """
297
+ if not self._call_in_progress:
298
+ return True
299
+ return self._pipe.poll(timeout)
300
+
301
+ def get_result(self, timeout: Optional[float] = None) -> Any:
302
+ """
303
+ Retrieve the result of the current or last async call.
304
+
305
+ If no call is in progress, returns the result of the last call.
306
+ Otherwise, blocks until the result is available or timeout expires.
307
+
308
+ Args:
309
+ timeout: Maximum time to wait in seconds. None means wait forever.
310
+
311
+ Returns:
312
+ The result of the function call, with numpy arrays converted
313
+ from shared memory.
314
+
315
+ Raises:
316
+ TimeoutError: If timeout expires before result is available.
317
+ """
318
+ if not self._call_in_progress:
319
+ return self._last_result
320
+ if not self._pipe.poll(timeout):
321
+ raise TimeoutError(
322
+ f"Timed out waiting for result after {timeout} seconds"
323
+ )
324
+ self._last_result_raw = self._pipe.recv()
325
+ if isinstance(self._last_result_raw, Exception):
326
+ self._call_in_progress = False
327
+ raise self._last_result_raw from self._last_result_raw
328
+ self._last_result = self._call_unpack_fn(
329
+ self._call_unpack_argset, self._last_result_raw
330
+ )
331
+ self._call_in_progress = False
332
+ return self._last_result
333
+
334
+ def stop(self) -> None:
335
+ """
336
+ Stop the server and clean up resources.
337
+
338
+ Closes the connection and waits for the server process to terminate
339
+ (if in child process mode).
340
+ """
341
+ if not self._started:
342
+ return
343
+ self._started = False
344
+ self._pipe.close()
345
+ if self._server_process is not None:
346
+ self._server_process.join()
347
+ self._server_process = None
348
+ self._alloc_manager.close()
349
+
350
+ def __del__(self) -> None:
351
+ """Clean up resources when the client is garbage collected."""
352
+ try:
353
+ self.stop()
354
+ except Exception:
355
+ pass # Ignore errors during cleanup
356
+
357
+ def run(
358
+ endpoint_or_connection: Union[Connection, Tuple[str, int, bytes]],
359
+ ) -> None:
360
+ """
361
+ Server loop for handling RPC calls.
362
+
363
+ This function runs the main server loop, processing function calls from
364
+ clients and returning results. It can operate in two modes:
365
+
366
+ 1. Child process mode: Receives a connection from a parent process
367
+ 2. Standalone mode: Listens for TCP connections on a specified endpoint.
368
+ In this mode the function never returns; after a client disconnects,
369
+ it waits for the next client.
370
+
371
+ Args:
372
+ endpoint_or_connection: Either a Connection object (for child process
373
+ mode), or a tuple of (address, port, authkey) for standalone mode.
374
+
375
+ Example:
376
+ >>> from callserver import run
377
+ >>> run(("127.0.0.1", 5000, b"callserver")) # blocks forever
378
+ """
379
+ keep_alive = isinstance(endpoint_or_connection, tuple)
380
+ pipe: Optional[Connection] = None if keep_alive else endpoint_or_connection
381
+ config: Optional[Tuple] = None
382
+
383
+ while keep_alive or pipe:
384
+ if keep_alive:
385
+ pipe = _wait_for_client_connection(*endpoint_or_connection)
386
+ try:
387
+ name, context_init, context_args, context_kwargs = pipe.recv()
388
+ except (EOFError, BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
389
+ if not keep_alive:
390
+ return
391
+ continue
392
+
393
+ alloc_manager = AllocationSetManager(name)
394
+ if not config or config != (name, context_init, context_args, context_kwargs):
395
+ config = (name, context_init, context_args, context_kwargs)
396
+ try:
397
+ context = (
398
+ context_init(*context_args, **context_kwargs)
399
+ if callable(context_init)
400
+ else context_init
401
+ )
402
+ except Exception as e:
403
+ try:
404
+ pipe.send(e)
405
+ except (EOFError, BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
406
+ pass
407
+ if not keep_alive:
408
+ return
409
+ continue
410
+
411
+ while pipe:
412
+ try:
413
+ cmd = pipe.recv()
414
+ except (EOFError, BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
415
+ if not keep_alive:
416
+ return
417
+ break
418
+
419
+ fn, arg_set_idx, args, kwargs = cmd
420
+ argset = alloc_manager.get_allocation_set(fn, arg_set_idx)
421
+ args, kwargs = argset.args_from_sharedmemory(*args, **kwargs)
422
+ result = (
423
+ fn(context, *args, **kwargs) if context is not None else fn(*args, **kwargs)
424
+ )
425
+ result = argset.retval_to_sharedmemory(result)
426
+ try:
427
+ pipe.send(result)
428
+ except (EOFError, BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
429
+ if not keep_alive:
430
+ return
431
+ break
432
+
433
+
434
+ def _wait_for_client_connection(
435
+ address: str,
436
+ port: int,
437
+ authkey: bytes,
438
+ ) -> Connection:
439
+ """
440
+ Wait for a client connection on the specified endpoint.
441
+
442
+ Starts a listener thread and waits for a client to connect.
443
+
444
+ Args:
445
+ address: The address to listen on.
446
+ port: The port to listen on.
447
+ authkey: Authentication key for the connection.
448
+
449
+ Returns:
450
+ The established Connection object.
451
+ """
452
+ listener_thread = threading.Thread(
453
+ target=_wait_for_client_connection_thread,
454
+ args=(address, port, authkey),
455
+ daemon=True,
456
+ )
457
+ listener_thread.start()
458
+ while listener_thread.is_alive():
459
+ listener_thread.join(timeout=1)
460
+ return listener_thread.connection
461
+
462
+
463
+ def _wait_for_client_connection_thread(
464
+ address: str,
465
+ port: int,
466
+ authkey: bytes,
467
+ ) -> None:
468
+ """
469
+ Thread target for listening for client connections.
470
+
471
+ Creates a listener and waits for a client to connect. Stores the
472
+ connection on the thread object for retrieval by the parent.
473
+
474
+ Args:
475
+ address: The address to listen on.
476
+ port: The port to listen on.
477
+ authkey: Authentication key for the connection.
478
+ """
479
+ with Listener((address, port), authkey=authkey) as listener:
480
+ print(f"Listening for connections on {address}:{port}")
481
+ threading.current_thread().connection = listener.accept()
482
+ print("Connection accepted from", listener.last_accepted)
483
+
484
+
485
+ def run_console() -> None:
486
+ """
487
+ Command-line entry point for running a standalone RPC server.
488
+
489
+ Parses command-line arguments for address, port, and authentication key,
490
+ then starts the server loop. The server can be stopped with Ctrl+C.
491
+
492
+ Command-line arguments:
493
+ --port: Port to listen on (required)
494
+ --address: Address to listen on (default: 127.0.0.1)
495
+ --authkey: Authentication key (default: callserver)
496
+ """
497
+ parser = argparse.ArgumentParser(description="Start a standalone CallServer.")
498
+ parser.add_argument("--port", type=int, required=True)
499
+ parser.add_argument("--address", type=str, default="127.0.0.1")
500
+ parser.add_argument(
501
+ "--authkey", type=str.encode, default=DEFAULT_AUTHKEY.decode()
502
+ )
503
+ args = parser.parse_args()
504
+ try:
505
+ run((args.address, args.port, args.authkey))
506
+ except KeyboardInterrupt:
507
+ pass
508
+
509
+
510
+ if __name__ == "__main__":
511
+ run_console()
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: callserver
3
+ Version: 0.1.0
4
+ Summary: Shared-memory RPC for fast inter-process function calls with numpy arrays.
5
+ Author: dexman-ai
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest; extra == "test"
15
+ Dynamic: license-file
16
+
17
+ # callserver
18
+
19
+ Fast inter-process function calls for Python, with numpy arrays passed through shared memory.
20
+
21
+ ## Overview
22
+
23
+ `callserver` runs Python functions in another process on the same machine. Large numpy arrays in arguments and return values are moved to shared memory automatically, so they are not pickled or pushed through a pipe. Shared memory is allocated on the first call and reused on every call after that.
24
+
25
+ It is built for low-latency, high-throughput workloads that need a separate process, such as:
26
+
27
+ - Real-time robot control
28
+ - Vision processing pipelines
29
+ - Compute and data-intensive workloads that benefit from zero-copy data sharing
30
+
31
+ **Features**
32
+
33
+ - Automatic shared memory transfer of large numpy arrays, in both directions
34
+ - Zero-copy transfer with pre-allocated shared memory buffers
35
+ - Synchronous and asynchronous calls, with double-buffering to overlap work
36
+ - Persistent server-side state
37
+ - Runs the server as a child process, or connects to a standalone server over TCP
38
+
39
+ `callserver` works on a single machine only. It is not meant for distributed execution.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install callserver
45
+ ```
46
+
47
+ Requires Python 3.8+ and numpy.
48
+
49
+ ## Usage
50
+ ### Simple Example
51
+ Arrays are transferred via shared memory both ways. No serialization!
52
+ Shared memory is only allocated the first time the RPC is made. Subsequent calls reuse the same buffers.
53
+ ```python
54
+ import numpy as np
55
+ from callserver import CallServer
56
+
57
+ if __name__ == "__main__":
58
+ # Spawns a child process. np.add runs there; the arrays travel through shared memory.
59
+ with CallServer("adder") as server:
60
+ result = np.zeros((1024, 1024))
61
+ ones = np.ones((1024, 1024))
62
+ for _ in range(100):
63
+ result = server.call(np.add, result, ones)
64
+ print(result[0, 0]) # 100.0
65
+ ```
66
+
67
+ ### Server-Side State
68
+ A context passed to the server (instance or callable) holds state for subsequent calls:
69
+
70
+ ```python
71
+ import numpy as np
72
+ from callserver import CallServer
73
+
74
+ class Accumulator:
75
+ def __init__(self, shape):
76
+ self.total = np.zeros(shape)
77
+
78
+ def accumulate(acc, values):
79
+ acc.total += values
80
+ return acc.total
81
+
82
+ if __name__ == "__main__":
83
+ # Arguments after the initializer are passed to it on the server.
84
+ with CallServer("accumulator", Accumulator, (512, 512)) as server:
85
+ for _ in range(10):
86
+ total = server.call(accumulate, np.ones((512, 512)))
87
+ print(total[0, 0]) # 10.0
88
+ ```
89
+
90
+ ### Standalone Server
91
+ A server can also run as a separate, long-lived process that clients connect to over TCP. Start it from the command line (the port is required):
92
+
93
+ ```bash
94
+ callserver --port 5000
95
+ ```
96
+
97
+ Then connect by passing a `(name, (address, port))` tuple instead of a name:
98
+
99
+ ```python
100
+ import numpy as np
101
+ from callserver import CallServer
102
+
103
+ with CallServer(("adder", ("127.0.0.1", 5000))) as server:
104
+ result = server.call(np.add, np.ones((1024, 1024)), np.ones((1024, 1024)))
105
+ print(result[0, 0]) # 2.0
106
+ ```
107
+
108
+ The server handles one client at a time and keeps running after a client disconnects. Functions sent to it must be importable by the server process.
109
+
110
+ > Server-side functions must be defined at module level so they can be pickled. The `if __name__ == "__main__":` guard is required on Windows and macOS, because the child process re-imports the main module.
111
+
112
+ ## Documentation
113
+
114
+ - [Usage guide](callserver/callserver.md): async calls, double-buffering, pre-allocated buffers, standalone servers, the API reference, and how it works.
115
+ - [Examples](examples): runnable scripts.
116
+
117
+ | Example | Shows |
118
+ | --- | --- |
119
+ | [simple_call.py](examples/simple_call.py) | Basic synchronous call |
120
+ | [server_state.py](examples/server_state.py) | Server-side state with a context initializer |
121
+ | [async_calls.py](examples/async_calls.py) | Async calls with double-buffering |
122
+ | [preallocated_buffers.py](examples/preallocated_buffers.py) | Zero-copy with pre-allocated shared memory |
123
+ | [standalone_client.py](examples/standalone_client.py) | Connecting to a standalone server |
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ pip install -e .[test]
129
+ python -m pytest
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,12 @@
1
+ callserver/__init__.py,sha256=30WA17EO6SlfsTlfJvyflUoZ5i-yhgV73l7qmeR8vCI,2278
2
+ callserver/__main__.py,sha256=_NcLxprCoPvMH3EY67oNQmekohRdsUijBv2pIKfDgss,86
3
+ callserver/allocationmanager.py,sha256=YfIEEgIYDyOGzPnlx24e_KtxIKBUVx-zUs24lZ419vg,6156
4
+ callserver/allocationset.py,sha256=FsTWmPqPYiIbyRTikL_QKUievoBz4IAzYwHwjp-ea-w,13928
5
+ callserver/callserver.md,sha256=CxJ7E4d-8K7av2X4E6CWzWgkM_P6SuGmZ6_0PlmNKs8,16273
6
+ callserver/callserver.py,sha256=OZf8HYemEtSLgfhFxKaquF7YelCSf5s_29Y7ND91vk4,18575
7
+ callserver-0.1.0.dist-info/licenses/LICENSE,sha256=LTNa9cWvBv1-sdawR17EUQqa0qHPhBOruWDUG3USeMw,1087
8
+ callserver-0.1.0.dist-info/METADATA,sha256=OAV1lCkg0R5rpXOyjcCZGKFA07w-49hyhxVKlGAGHBk,4725
9
+ callserver-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ callserver-0.1.0.dist-info/entry_points.txt,sha256=7mjHwrukbpFrDTOxW0gyuIeHyaCNnosidZLt0owH_Gg,54
11
+ callserver-0.1.0.dist-info/top_level.txt,sha256=cqSj8s_AUeodbipcxrZ5mrMHN1ksexfyR_M-Y9zUV20,11
12
+ callserver-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ callserver = callserver:run_console
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dexman-ai
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
+ callserver