xllify 0.8.9__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,2 @@
1
+ 0.8.9
2
+
xllify/__init__.py ADDED
@@ -0,0 +1,101 @@
1
+ """
2
+ xllify - Python SDK for creating Excel XLL add-ins
3
+
4
+ This package provides a simple way to expose Python functions to Excel
5
+ via xllify using RPC.
6
+ """
7
+
8
+ from xllify.rpc_server import XllifyRPCServer, Parameter
9
+ from xllify.rtd_client import RTDClient, TopicUpdate, RTDCommand, CellValue, Matrix, ExcelValue
10
+
11
+ __version__ = "0.8.7"
12
+ __all__ = [
13
+ "XllifyRPCServer",
14
+ "Parameter",
15
+ "RTDClient",
16
+ "TopicUpdate",
17
+ "RTDCommand",
18
+ "CellValue",
19
+ "Matrix",
20
+ "ExcelValue",
21
+ "get_server",
22
+ "fn",
23
+ "start_server",
24
+ "configure_batching",
25
+ "configure_spawn_count",
26
+ ]
27
+
28
+ # Create default server instance for convenience
29
+ _default_server = None
30
+
31
+ # Global spawn count configuration
32
+ _spawn_count: int = 1
33
+
34
+
35
+ def get_server() -> XllifyRPCServer:
36
+ """Get or create the default XllifyRPCServer instance."""
37
+ global _default_server
38
+ if _default_server is None:
39
+ _default_server = XllifyRPCServer()
40
+ return _default_server
41
+
42
+
43
+ # Convenience exports
44
+ fn = lambda *args, **kwargs: get_server().fn(*args, **kwargs)
45
+ fn.__doc__ = """Decorator to register a Python function as an Excel function. Alias for XllifyRPCServer.fn()"""
46
+
47
+ start_server = lambda: get_server().start()
48
+ start_server.__doc__ = (
49
+ """Start the default RPC server. This is an alias to XllifyRPCServer.start()"""
50
+ )
51
+
52
+
53
+ def configure_batching(
54
+ enabled: bool = True, batch_size: int = 500, batch_timeout_ms: int = 50
55
+ ) -> None:
56
+ """
57
+ Configure batching behavior for RTD updates on the default server.
58
+
59
+ Call this before starting the server to customize batching settings.
60
+ Batching improves performance by sending multiple updates together.
61
+
62
+ Args:
63
+ enabled: Enable batching (default: True)
64
+ batch_size: Maximum number of updates to batch together (default: 500)
65
+ batch_timeout_ms: Maximum time to wait before flushing batch in milliseconds (default: 50)
66
+
67
+ Example:
68
+ import xllify
69
+
70
+ xllify.configure_batching(batch_size=1000, batch_timeout_ms=100)
71
+
72
+ @xllify.fn("xllipy.Hello")
73
+ def hello(name: str) -> str:
74
+ return f"Hello, {name}!"
75
+ """
76
+ get_server().configure_batching(
77
+ enabled=enabled, batch_size=batch_size, batch_timeout_ms=batch_timeout_ms
78
+ )
79
+
80
+
81
+ def configure_spawn_count(count: int) -> None:
82
+ """
83
+ Configure the number of Python processes to spawn for this module.
84
+
85
+ This function both sets a runtime global that the RPC server reads and
86
+ serves as a marker for funcinfo to extract the spawn_count value.
87
+
88
+ Args:
89
+ count: Number of processes to spawn (must be positive)
90
+
91
+ Example:
92
+ import xllify
93
+
94
+ xllify.configure_spawn_count(4)
95
+
96
+ @xllify.fn("xllipy.Hello")
97
+ def hello(name: str) -> str:
98
+ return f"Hello, {name}!"
99
+ """
100
+ global _spawn_count
101
+ _spawn_count = count
xllify/__main__.py ADDED
@@ -0,0 +1,343 @@
1
+ """
2
+ CLI entry point for running xllify Python scripts
3
+
4
+ Usage:
5
+ python -m xllify my_functions.py
6
+ python -m xllify my_functions.py --process-name myapp
7
+ """
8
+
9
+ import sys
10
+ import argparse
11
+ import importlib.util
12
+ import os
13
+ import time
14
+ import logging
15
+ from pathlib import Path
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Configure logging based on environment variable
20
+ if os.getenv("XLLIFY_PY_DEBUG"):
21
+ logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
22
+ else:
23
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
24
+
25
+
26
+ def load_module_from_file(file_path: str, module_name: str = "xllify_user_module"):
27
+ """Load a Python module from a file path"""
28
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
29
+ if spec is None or spec.loader is None:
30
+ raise ImportError(f"Could not load module from {file_path}")
31
+
32
+ module = importlib.util.module_from_spec(spec)
33
+ sys.modules[module_name] = module
34
+ spec.loader.exec_module(module)
35
+ return module
36
+
37
+
38
+ def run_server_with_reload(file_path: str, server_name: str, xll_name: str):
39
+ """
40
+ Run the server with auto-reload on file changes.
41
+ Only reloads the user module, not the entire server/sockets.
42
+
43
+ Args:
44
+ file_path: Path to the Python script to watch
45
+ server_name: Name of the server variable (or None for default)
46
+ xll_name: XLL name for metadata path (or None for default)
47
+ """
48
+ from xllify import XllifyRPCServer, get_server
49
+ import threading
50
+
51
+ script_path = Path(file_path)
52
+ last_mtime = script_path.stat().st_mtime
53
+
54
+ from xllify import __version__
55
+
56
+ print(
57
+ r"""
58
+ _ _ _ __
59
+ __ | | (_)/ _|_ _
60
+ \ \/ / | | |_| | | |
61
+ > <| | | _| |_| |
62
+ /_/\_\_|_|_| \__, |
63
+ |___/
64
+ """
65
+ )
66
+ print(f" v{__version__}\n")
67
+
68
+ logger.info(f"Auto-reload enabled. Watching: {file_path}")
69
+ logger.info("Press Ctrl+C to stop.")
70
+
71
+ def load_and_setup_server():
72
+ """Load/reload the user module and setup the server"""
73
+ if "xllify_user_module" in sys.modules:
74
+ del sys.modules["xllify_user_module"]
75
+
76
+ import xllify
77
+
78
+ xllify._default_server = None
79
+
80
+ # Load the module
81
+ try:
82
+ module = load_module_from_file(file_path)
83
+ except Exception as e:
84
+ logger.error(f"Error loading module: {e}")
85
+ return None
86
+
87
+ server = None
88
+ if server_name:
89
+ if not hasattr(module, server_name):
90
+ logger.error(f"Module does not have attribute '{server_name}'")
91
+ return None
92
+
93
+ server = getattr(module, server_name)
94
+ if not isinstance(server, XllifyRPCServer):
95
+ logger.error(f"'{server_name}' is not an XllifyRPCServer instance")
96
+ return None
97
+ else:
98
+ if hasattr(module, "server") and isinstance(module.server, XllifyRPCServer):
99
+ server = module.server
100
+ else:
101
+ server = get_server()
102
+
103
+ # Override xll_name based on arguments
104
+ if xll_name:
105
+ server.xll_name = xll_name
106
+
107
+ if not server.functions:
108
+ logger.warning("No functions registered. Use @xllify.fn() decorator.")
109
+ return None
110
+
111
+ return server
112
+
113
+ # Initial load
114
+ server = load_and_setup_server()
115
+ if server is None:
116
+ logger.error("Failed to start server.")
117
+ sys.exit(1)
118
+
119
+ logger.info(f"xllify external function RPC server from: {file_path}")
120
+
121
+ server.running = True
122
+ watcher_active = True
123
+
124
+ # Start file watcher thread
125
+ def watch_file():
126
+ nonlocal last_mtime
127
+ logger.info(f"File watcher active. Monitoring: {file_path}")
128
+ while watcher_active and server.running:
129
+ time.sleep(1)
130
+ try:
131
+ current_mtime = script_path.stat().st_mtime
132
+ if current_mtime != last_mtime:
133
+ logger.info(f"RELOAD: Detected change in {file_path}")
134
+ last_mtime = current_mtime
135
+
136
+ # Reload the module
137
+ new_server = load_and_setup_server()
138
+ if new_server is not None:
139
+ server.functions.clear()
140
+ server.functions.update(new_server.functions)
141
+ server._write_function_metadata()
142
+
143
+ # Clear RTD cache to force Excel to refresh
144
+ try:
145
+ server.rtd_client.evict_all()
146
+ logger.info("RELOAD: RTD cache cleared")
147
+ except Exception as e:
148
+ logger.warning(f"RELOAD: Failed to clear RTD cache: {e}")
149
+
150
+ logger.info(f"RELOAD: Success! Functions: {list(server.functions.keys())}")
151
+ else:
152
+ logger.error("RELOAD: Failed! Keeping previous version.")
153
+
154
+ except FileNotFoundError:
155
+ logger.warning(f"File was deleted: {file_path}")
156
+ except Exception as e:
157
+ logger.error(f"Error watching file: {e}")
158
+ logger.debug("Traceback:", exc_info=True)
159
+
160
+ watcher_thread = threading.Thread(target=watch_file, daemon=True)
161
+ watcher_thread.start()
162
+
163
+ # Start the server (will block)
164
+ try:
165
+ server.start()
166
+ except KeyboardInterrupt:
167
+ logger.info("\nShutdown complete")
168
+ watcher_active = False
169
+ sys.exit(0)
170
+
171
+
172
+ def main():
173
+ parser = argparse.ArgumentParser(
174
+ description="Run xllify Python RPC server",
175
+ formatter_class=argparse.RawDescriptionHelpFormatter,
176
+ epilog="""
177
+ Examples:
178
+ python -m xllify my_functions.py
179
+ python -m xllify my_functions.py --process-name myapp
180
+ python -m xllify my_functions:server
181
+ python -m xllify --clear-cache
182
+ """,
183
+ )
184
+
185
+ parser.add_argument(
186
+ "module",
187
+ nargs="?",
188
+ help="Python file to run (e.g., my_functions.py or my_functions:server)",
189
+ )
190
+
191
+ parser.add_argument(
192
+ "--clear-cache",
193
+ action="store_true",
194
+ help="Clear RTD cache (send EVICTALL command to default endpoint)",
195
+ )
196
+
197
+ parser.add_argument(
198
+ "--endpoint",
199
+ default=None,
200
+ help="Custom RTD endpoint for --clear-cache (default: tcp://127.0.0.1:55555)",
201
+ )
202
+
203
+ parser.add_argument(
204
+ "--reload",
205
+ action="store_true",
206
+ help="Enable auto-reload: restart server when script file changes",
207
+ )
208
+
209
+ parser.add_argument(
210
+ "--xll-name",
211
+ default="xllify_addin",
212
+ help="XLL name for metadata path (default: xllify_addin, creates AppData/Local/xllify/{xll-name}/xrpc/)",
213
+ )
214
+
215
+ args = parser.parse_args()
216
+
217
+ # Handle --clear-cache
218
+ if args.clear_cache:
219
+ from xllify import RTDClient
220
+
221
+ endpoint = args.endpoint if args.endpoint else "tcp://127.0.0.1:55555"
222
+ logger.info(f"Clearing RTD cache at {endpoint}...")
223
+ try:
224
+ client = RTDClient()
225
+ if client.evict_all():
226
+ logger.info("✓ Cache cleared successfully")
227
+ sys.exit(0)
228
+ else:
229
+ logger.error("✗ Failed to clear cache (RTD server may not be running)")
230
+ sys.exit(1)
231
+ except Exception as e:
232
+ logger.error(f"✗ Error: {e}")
233
+ sys.exit(1)
234
+
235
+ if not args.module:
236
+ logger.error("module argument is required")
237
+ parser.print_help()
238
+ sys.exit(1)
239
+
240
+ if ":" in args.module:
241
+ file_path, server_name = args.module.split(":", 1)
242
+ else:
243
+ file_path = args.module
244
+ server_name = None
245
+
246
+ if not Path(file_path).exists():
247
+ logger.error(f"File not found: {file_path}")
248
+ sys.exit(1)
249
+
250
+ if args.reload:
251
+ run_server_with_reload(file_path, server_name, args.xll_name)
252
+ return
253
+
254
+
255
+ try:
256
+ module = load_module_from_file(file_path)
257
+ except Exception as e:
258
+ logger.error(f"Error loading modules: {e}")
259
+ sys.exit(1)
260
+
261
+ from xllify import XllifyRPCServer, get_server
262
+
263
+ server = None
264
+
265
+ if server_name:
266
+ if not hasattr(module, server_name):
267
+ logger.error(f"Module does not have attribute '{server_name}'")
268
+ sys.exit(1)
269
+
270
+ server = getattr(module, server_name)
271
+ if not isinstance(server, XllifyRPCServer):
272
+ logger.error(f"'{server_name}' is not an XllifyRPCServer instance")
273
+ sys.exit(1)
274
+ else:
275
+ # Check if module has a 'server' variable
276
+ if hasattr(module, "server") and isinstance(module.server, XllifyRPCServer):
277
+ server = module.server
278
+ else:
279
+ server = get_server()
280
+
281
+ # Override xll_name based on arguments
282
+ if args.xll_name:
283
+ server.xll_name = args.xll_name
284
+
285
+ if not server.functions:
286
+ logger.warning("No functions registered. Use @xllify.fn() decorator.")
287
+ logger.warning("Example:\n @xllify.fn('MyFunc')\n def my_func():\n return 'Hello'")
288
+ sys.exit(1)
289
+
290
+ from xllify import __version__
291
+
292
+ print(
293
+ r"""
294
+ _ _ _ __
295
+ __ | | (_)/ _|_ _
296
+ \ \/ / | | |_| | | |
297
+ > <| | | _| |_| |
298
+ /_/\_\_|_|_| \__, |
299
+ |___/
300
+ """
301
+ )
302
+ print(f" v{__version__}\n")
303
+
304
+ logger.info(f"xllify external function RPC server from: {file_path}")
305
+ try:
306
+ server.start()
307
+ except KeyboardInterrupt:
308
+ logger.info("\nShutdown complete")
309
+ sys.exit(0)
310
+
311
+
312
+ def clear_cache_main():
313
+ """Entry point for xllify-clear-cache command"""
314
+ from xllify import RTDClient
315
+
316
+ parser = argparse.ArgumentParser(
317
+ description="Clear xllify cache", formatter_class=argparse.RawDescriptionHelpFormatter
318
+ )
319
+
320
+ parser.add_argument(
321
+ "--endpoint", default=None, help="Custom endpoint (default: tcp://127.0.0.1:55555)"
322
+ )
323
+
324
+ args = parser.parse_args()
325
+
326
+ endpoint = args.endpoint if args.endpoint else "tcp://127.0.0.1:55555"
327
+ logger.info(f"Clearing cache at {endpoint}...")
328
+
329
+ try:
330
+ client = RTDClient()
331
+ if client.evict_all():
332
+ logger.info("Cache cleared successfully")
333
+ sys.exit(0)
334
+ else:
335
+ logger.error("Failed to clear cache (server may not be running)")
336
+ sys.exit(1)
337
+ except Exception as e:
338
+ logger.error(f"Error: {e}")
339
+ sys.exit(1)
340
+
341
+
342
+ if __name__ == "__main__":
343
+ main()
xllify/diagnostics.py ADDED
@@ -0,0 +1,78 @@
1
+ """
2
+ Diagnostic tools for xllify RTD connection issues
3
+ """
4
+
5
+ import socket
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def check_port_open(host: str, port: int, timeout: float = 1.0) -> bool:
12
+ """
13
+ Check if a TCP port is open and accepting connections
14
+
15
+ Args:
16
+ host: Hostname or IP address
17
+ port: Port number
18
+ timeout: Connection timeout in seconds
19
+
20
+ Returns:
21
+ True if port is open and accepting connections
22
+ """
23
+ try:
24
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
25
+ sock.settimeout(timeout)
26
+ result = sock.connect_ex((host, port))
27
+ sock.close()
28
+ return result == 0
29
+ except Exception as e:
30
+ logger.debug(f"Port check failed for {host}:{port} - {e}")
31
+ return False
32
+
33
+
34
+ def diagnose_rtd_connection():
35
+ """
36
+ Run diagnostics on RTD server connection
37
+ Checks if the ZeroMQ endpoints are available
38
+ """
39
+ print("=== xllify RTD Connection Diagnostics ===\n")
40
+
41
+ # Check ROUTER endpoint (tcp://127.0.0.1:55555)
42
+ router_host = "127.0.0.1"
43
+ router_port = 55555
44
+
45
+ print(f"Checking ROUTER endpoint: {router_host}:{router_port}")
46
+ if check_port_open(router_host, router_port):
47
+ print(" ✓ Port is OPEN - RTD server is likely running")
48
+ else:
49
+ print(" ✗ Port is CLOSED - RTD server may not be started")
50
+ print("\n Possible causes:")
51
+ print(" 1. Excel is not running")
52
+ print(" 2. Excel RTD server (AsyncRTD.dll) is not loaded")
53
+ print(" 3. Excel has not made any RTD calls yet (ServerStart not called)")
54
+ print(" 4. Firewall is blocking the connection")
55
+ print("\n To fix:")
56
+ print(
57
+ ' - Open Excel and create an RTD formula: =RTD("xllify.asyncrtd", "", "__XLLIFY_KEEPALIVE__")'
58
+ )
59
+ print(" - Make sure the xllify XLL add-in is loaded")
60
+
61
+ print()
62
+
63
+ # Check PUB endpoint (tcp://127.0.0.1:55556)
64
+ pub_host = "127.0.0.1"
65
+ pub_port = 55556
66
+
67
+ print(f"Checking PUB endpoint: {pub_host}:{pub_port}")
68
+ if check_port_open(pub_host, pub_port):
69
+ print(" ✓ Port is OPEN - RTD cache notifications available")
70
+ else:
71
+ print(" ✗ Port is CLOSED - Same issue as ROUTER endpoint")
72
+
73
+ print("\n=== End Diagnostics ===")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ logging.basicConfig(level=logging.INFO)
78
+ diagnose_rtd_connection()