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.
- xllify/XLLIFY_DIST_VERSION +2 -0
- xllify/__init__.py +101 -0
- xllify/__main__.py +343 -0
- xllify/diagnostics.py +78 -0
- xllify/funcinfo.py +375 -0
- xllify/install.py +251 -0
- xllify/py.typed +1 -0
- xllify/rpc_server.py +639 -0
- xllify/rtd_client.py +576 -0
- xllify-0.8.9.dist-info/METADATA +407 -0
- xllify-0.8.9.dist-info/RECORD +15 -0
- xllify-0.8.9.dist-info/WHEEL +5 -0
- xllify-0.8.9.dist-info/entry_points.txt +5 -0
- xllify-0.8.9.dist-info/licenses/LICENSE +21 -0
- xllify-0.8.9.dist-info/top_level.txt +1 -0
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()
|