fixcore-engine 0.1.1__tar.gz → 0.1.2__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.
- {fixcore_engine-0.1.1/fixcore_engine.egg-info → fixcore_engine-0.1.2}/PKG-INFO +1 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui.py +26 -11
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui_ui/app.js +45 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui_ui/index.html +7 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui_ui/style.css +13 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/transport/initiator.py +14 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2/fixcore_engine.egg-info}/PKG-INFO +1 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/pyproject.toml +1 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/LICENSE +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/README.md +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/application.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui_ui/fixcore_logo.svg +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/base.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/factory.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/file_log.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/screen.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/cracker.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/data_dictionary.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/exceptions.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/field.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/message.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/session.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/session_id.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/session_settings.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/state.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/base.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/factory.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/file_store.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/memory.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/transport/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/transport/acceptor.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/transport/framer.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/SOURCES.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/dependency_links.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/entry_points.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/requires.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/top_level.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/setup.cfg +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_cracker.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_data_dictionary.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_file_log.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_file_store.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_framer.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_integration.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_message.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_session.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_session_id.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_store.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_transport.py +0 -0
|
@@ -224,6 +224,8 @@ async def _dispatch(method: str, body: dict[str, Any]) -> Any:
|
|
|
224
224
|
)
|
|
225
225
|
if method == "update_session":
|
|
226
226
|
return await _update_session(body.get("old_key", ""), body)
|
|
227
|
+
if method == "stop":
|
|
228
|
+
return await _stop(body.get("sid_key", ""))
|
|
227
229
|
return {"ok": False, "error": f"Unknown method: {method!r}"}
|
|
228
230
|
|
|
229
231
|
|
|
@@ -377,24 +379,37 @@ async def _logon(sid_key: str) -> dict[str, Any]:
|
|
|
377
379
|
|
|
378
380
|
|
|
379
381
|
async def _logout(sid_key: str) -> dict[str, Any]:
|
|
382
|
+
"""Graceful FIX logout — sends Logout message and disables auto-reconnect."""
|
|
383
|
+
entry = _sessions.get(sid_key)
|
|
384
|
+
if entry is None:
|
|
385
|
+
return {"ok": False, "error": "Unknown session"}
|
|
386
|
+
transport = entry.get("transport")
|
|
387
|
+
if transport is None:
|
|
388
|
+
return {"ok": False, "error": "No transport"}
|
|
389
|
+
try:
|
|
390
|
+
if isinstance(transport, SocketInitiator):
|
|
391
|
+
await transport.logout()
|
|
392
|
+
else:
|
|
393
|
+
session = next(iter(transport.sessions.values()), None)
|
|
394
|
+
if session and session.is_logged_on:
|
|
395
|
+
await session.send_logout()
|
|
396
|
+
entry["started"] = False
|
|
397
|
+
except Exception as exc:
|
|
398
|
+
return {"ok": False, "error": str(exc)}
|
|
399
|
+
return {"ok": True}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
async def _stop(sid_key: str) -> dict[str, Any]:
|
|
403
|
+
"""Hard stop — kills the transport immediately without sending FIX Logout."""
|
|
380
404
|
entry = _sessions.get(sid_key)
|
|
381
405
|
if entry is None:
|
|
382
406
|
return {"ok": False, "error": "Unknown session"}
|
|
383
407
|
transport = entry.get("transport")
|
|
384
408
|
if transport is None:
|
|
385
409
|
return {"ok": False, "error": "No transport"}
|
|
386
|
-
session = next(iter(transport.sessions.values()), None)
|
|
387
|
-
if session is None:
|
|
388
|
-
return {"ok": False, "error": "No session"}
|
|
389
|
-
if not session.is_logged_on:
|
|
390
|
-
try:
|
|
391
|
-
await transport.stop()
|
|
392
|
-
entry["started"] = False
|
|
393
|
-
except Exception as exc:
|
|
394
|
-
return {"ok": False, "error": str(exc)}
|
|
395
|
-
return {"ok": True}
|
|
396
410
|
try:
|
|
397
|
-
await
|
|
411
|
+
await transport.stop()
|
|
412
|
+
entry["started"] = False
|
|
398
413
|
except Exception as exc:
|
|
399
414
|
return {"ok": False, "error": str(exc)}
|
|
400
415
|
return {"ok": True}
|
|
@@ -76,6 +76,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
76
76
|
connectWS();
|
|
77
77
|
apiCall('get_version').then(v => { $id('version').textContent = v; });
|
|
78
78
|
startPolling();
|
|
79
|
+
loadLog();
|
|
80
|
+
rebuildLog();
|
|
79
81
|
updateAutoClordidVisibility();
|
|
80
82
|
updateClordidPreview();
|
|
81
83
|
$id('chk-auto-clordid').addEventListener('change', () => {
|
|
@@ -153,6 +155,7 @@ function dotClass(state) {
|
|
|
153
155
|
function selectSession(key) {
|
|
154
156
|
activeSid = key;
|
|
155
157
|
refreshSessions();
|
|
158
|
+
rebuildLog();
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
function renderSessionDetail(s) {
|
|
@@ -293,6 +296,14 @@ $id('btn-logout').addEventListener('click', () => {
|
|
|
293
296
|
});
|
|
294
297
|
});
|
|
295
298
|
|
|
299
|
+
$id('btn-stop').addEventListener('click', () => {
|
|
300
|
+
if (!activeSid) return;
|
|
301
|
+
apiCall('stop', { sid_key: activeSid }).then(res => {
|
|
302
|
+
if (!res.ok) showToast('Stop: ' + res.error, 'error');
|
|
303
|
+
else { showToast('Session stopped'); refreshSessions(); }
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
296
307
|
$id('btn-edit').addEventListener('click', () => {
|
|
297
308
|
if (!activeSid) return;
|
|
298
309
|
openEditModal(activeSid);
|
|
@@ -343,6 +354,24 @@ function generateClordId() {
|
|
|
343
354
|
return prefix + String(clordIdSeq).padStart(6, '0');
|
|
344
355
|
}
|
|
345
356
|
|
|
357
|
+
// ── Send panel collapse ───────────────────────────────────────────────────
|
|
358
|
+
const SEND_COLLAPSED_KEY = 'fixcore_send_collapsed';
|
|
359
|
+
|
|
360
|
+
function setSendCollapsed(collapsed) {
|
|
361
|
+
$id('panel-send').classList.toggle('panel-send-collapsed', collapsed);
|
|
362
|
+
try { localStorage.setItem(SEND_COLLAPSED_KEY, collapsed ? '1' : '0'); } catch (_) {}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
$id('btn-toggle-send').addEventListener('click', () => {
|
|
366
|
+
const collapsed = !$id('panel-send').classList.contains('panel-send-collapsed');
|
|
367
|
+
setSendCollapsed(collapsed);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Restore collapse state on load
|
|
371
|
+
try {
|
|
372
|
+
if (localStorage.getItem(SEND_COLLAPSED_KEY) === '1') setSendCollapsed(true);
|
|
373
|
+
} catch (_) {}
|
|
374
|
+
|
|
346
375
|
// ── Message builder ───────────────────────────────────────────────────────
|
|
347
376
|
const MSG_TYPE_DEFAULTS = {
|
|
348
377
|
'D': [{tag:'11',value:'ORD0001'},{tag:'55',value:'AAPL'},{tag:'54',value:'1'},
|
|
@@ -522,10 +551,24 @@ $id('paste-fix-input').addEventListener('keydown', e => {
|
|
|
522
551
|
});
|
|
523
552
|
|
|
524
553
|
// ── Message log ───────────────────────────────────────────────────────────
|
|
554
|
+
const LOG_STORAGE_KEY = 'fixcore_log';
|
|
555
|
+
|
|
556
|
+
function saveLog() {
|
|
557
|
+
try { localStorage.setItem(LOG_STORAGE_KEY, JSON.stringify(logEntries)); } catch (_) {}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function loadLog() {
|
|
561
|
+
try {
|
|
562
|
+
const raw = localStorage.getItem(LOG_STORAGE_KEY);
|
|
563
|
+
if (raw) logEntries = JSON.parse(raw);
|
|
564
|
+
} catch (_) {}
|
|
565
|
+
}
|
|
566
|
+
|
|
525
567
|
function addLogEntry(dir, sid, raw) {
|
|
526
568
|
const entry = { dir, sid, raw, time: nowStr() };
|
|
527
569
|
logEntries.push(entry);
|
|
528
570
|
if (logEntries.length > 2000) logEntries.shift();
|
|
571
|
+
saveLog();
|
|
529
572
|
appendLogRow(entry);
|
|
530
573
|
}
|
|
531
574
|
|
|
@@ -563,6 +606,7 @@ function shortSid(sid) {
|
|
|
563
606
|
}
|
|
564
607
|
|
|
565
608
|
function matchesFilter(entry) {
|
|
609
|
+
if (activeSid && entry.sid !== activeSid) return false;
|
|
566
610
|
if (logFilter === 'all') return true;
|
|
567
611
|
if (logFilter === 'in') return entry.dir === 'in';
|
|
568
612
|
if (logFilter === 'out') return entry.dir === 'out';
|
|
@@ -588,6 +632,7 @@ $id('panel-log').querySelectorAll('.filter-btn').forEach(btn => {
|
|
|
588
632
|
|
|
589
633
|
$id('btn-clear-log').addEventListener('click', () => {
|
|
590
634
|
logEntries = [];
|
|
635
|
+
localStorage.removeItem(LOG_STORAGE_KEY);
|
|
591
636
|
$id('log-entries').innerHTML = '';
|
|
592
637
|
});
|
|
593
638
|
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
<div class="detail-actions">
|
|
44
44
|
<button class="btn btn-primary" id="btn-logon">Logon</button>
|
|
45
45
|
<button class="btn btn-warn" id="btn-logout">Logout</button>
|
|
46
|
+
<button class="btn btn-secondary" id="btn-stop">Stop</button>
|
|
46
47
|
<button class="btn btn-secondary" id="btn-reset">Reset SeqNums</button>
|
|
47
48
|
<button class="btn btn-secondary" id="btn-edit">Edit</button>
|
|
48
49
|
<button class="btn btn-danger" id="btn-remove">Remove</button>
|
|
@@ -52,7 +53,11 @@
|
|
|
52
53
|
|
|
53
54
|
<!-- Send message panel -->
|
|
54
55
|
<section class="panel" id="panel-send">
|
|
55
|
-
<div class="panel-title"
|
|
56
|
+
<div class="panel-title panel-title-collapsible" id="btn-toggle-send">
|
|
57
|
+
<span>Send Message</span>
|
|
58
|
+
<span class="collapse-chevron" id="send-chevron">▲</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div id="panel-send-body">
|
|
56
61
|
<div class="send-row">
|
|
57
62
|
<label class="send-label">MsgType</label>
|
|
58
63
|
<select id="msg-type-select" class="select-msg-type">
|
|
@@ -87,6 +92,7 @@
|
|
|
87
92
|
<button class="btn btn-secondary" id="btn-add-field">+ Add Field</button>
|
|
88
93
|
<button class="btn btn-primary" id="btn-send">Send Message</button>
|
|
89
94
|
</div>
|
|
95
|
+
</div><!-- /panel-send-body -->
|
|
90
96
|
</section>
|
|
91
97
|
|
|
92
98
|
<!-- Message log -->
|
|
@@ -138,6 +138,19 @@ body {
|
|
|
138
138
|
color: var(--text-dim);
|
|
139
139
|
margin-bottom: 10px;
|
|
140
140
|
}
|
|
141
|
+
.panel-title-collapsible {
|
|
142
|
+
display: flex;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
align-items: center;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
user-select: none;
|
|
147
|
+
margin-bottom: 10px;
|
|
148
|
+
}
|
|
149
|
+
.panel-title-collapsible:hover { color: var(--text); }
|
|
150
|
+
.collapse-chevron { font-size: 10px; transition: transform .2s; }
|
|
151
|
+
.panel-send-collapsed .collapse-chevron { transform: rotate(180deg); }
|
|
152
|
+
.panel-send-collapsed #panel-send-body { display: none; }
|
|
153
|
+
.panel-send-collapsed { padding-bottom: 14px; }
|
|
141
154
|
.placeholder {
|
|
142
155
|
color: var(--text-dim);
|
|
143
156
|
font-style: italic;
|
|
@@ -42,6 +42,7 @@ class SocketInitiator:
|
|
|
42
42
|
self._sessions: dict[SessionID, Session] = {}
|
|
43
43
|
self._tasks: list[asyncio.Task[None]] = []
|
|
44
44
|
self._stop_event = asyncio.Event()
|
|
45
|
+
self._no_reconnect = asyncio.Event()
|
|
45
46
|
|
|
46
47
|
for sid in settings.session_ids:
|
|
47
48
|
conn_type = settings.get_or(sid, "ConnectionType", "initiator").lower()
|
|
@@ -59,6 +60,7 @@ class SocketInitiator:
|
|
|
59
60
|
async def start(self) -> None:
|
|
60
61
|
"""Launch a connect-loop task for each configured initiator session."""
|
|
61
62
|
self._stop_event.clear()
|
|
63
|
+
self._no_reconnect.clear()
|
|
62
64
|
for sid, session in self._sessions.items():
|
|
63
65
|
task = asyncio.create_task(
|
|
64
66
|
self._connect_loop(sid, session),
|
|
@@ -73,6 +75,17 @@ class SocketInitiator:
|
|
|
73
75
|
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
74
76
|
self._tasks.clear()
|
|
75
77
|
|
|
78
|
+
async def logout(self) -> None:
|
|
79
|
+
"""Send FIX Logout for all active sessions and disable auto-reconnect.
|
|
80
|
+
|
|
81
|
+
The connect loop will exit cleanly after the connection drops rather
|
|
82
|
+
than sleeping and retrying. Call :meth:`start` to reconnect later.
|
|
83
|
+
"""
|
|
84
|
+
self._no_reconnect.set()
|
|
85
|
+
for session in self._sessions.values():
|
|
86
|
+
if session.is_logged_on:
|
|
87
|
+
await session.send_logout()
|
|
88
|
+
|
|
76
89
|
async def __aenter__(self) -> "SocketInitiator":
|
|
77
90
|
await self.start()
|
|
78
91
|
return self
|
|
@@ -132,7 +145,7 @@ class SocketInitiator:
|
|
|
132
145
|
pass
|
|
133
146
|
framer.reset()
|
|
134
147
|
|
|
135
|
-
if not self._stop_event.is_set():
|
|
148
|
+
if not self._stop_event.is_set() and not self._no_reconnect.is_set():
|
|
136
149
|
await self._interruptible_sleep(reconnect)
|
|
137
150
|
|
|
138
151
|
async def _interruptible_sleep(self, seconds: float) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|