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.
Files changed (54) hide show
  1. {fixcore_engine-0.1.1/fixcore_engine.egg-info → fixcore_engine-0.1.2}/PKG-INFO +1 -1
  2. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui.py +26 -11
  3. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui_ui/app.js +45 -0
  4. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui_ui/index.html +7 -1
  5. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui_ui/style.css +13 -0
  6. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/transport/initiator.py +14 -1
  7. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2/fixcore_engine.egg-info}/PKG-INFO +1 -1
  8. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/pyproject.toml +1 -1
  9. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/LICENSE +0 -0
  10. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/README.md +0 -0
  11. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/__init__.py +0 -0
  12. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/application.py +0 -0
  13. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/gui_ui/fixcore_logo.svg +0 -0
  14. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/__init__.py +0 -0
  15. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/base.py +0 -0
  16. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/factory.py +0 -0
  17. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/file_log.py +0 -0
  18. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/log/screen.py +0 -0
  19. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/__init__.py +0 -0
  20. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/cracker.py +0 -0
  21. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/data_dictionary.py +0 -0
  22. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/exceptions.py +0 -0
  23. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/field.py +0 -0
  24. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/message/message.py +0 -0
  25. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/__init__.py +0 -0
  26. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/session.py +0 -0
  27. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/session_id.py +0 -0
  28. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/session_settings.py +0 -0
  29. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/session/state.py +0 -0
  30. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/__init__.py +0 -0
  31. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/base.py +0 -0
  32. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/factory.py +0 -0
  33. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/file_store.py +0 -0
  34. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/store/memory.py +0 -0
  35. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/transport/__init__.py +0 -0
  36. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/transport/acceptor.py +0 -0
  37. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore/transport/framer.py +0 -0
  38. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/SOURCES.txt +0 -0
  39. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/dependency_links.txt +0 -0
  40. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/entry_points.txt +0 -0
  41. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/requires.txt +0 -0
  42. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/fixcore_engine.egg-info/top_level.txt +0 -0
  43. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/setup.cfg +0 -0
  44. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_cracker.py +0 -0
  45. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_data_dictionary.py +0 -0
  46. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_file_log.py +0 -0
  47. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_file_store.py +0 -0
  48. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_framer.py +0 -0
  49. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_integration.py +0 -0
  50. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_message.py +0 -0
  51. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_session.py +0 -0
  52. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_session_id.py +0 -0
  53. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_store.py +0 -0
  54. {fixcore_engine-0.1.1 → fixcore_engine-0.1.2}/tests/test_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixcore-engine
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Pure Python FIX protocol engine
5
5
  Author-email: Aidan Chisholm <aidan.chisholm@gmail.com>
6
6
  License: MIT License
@@ -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 session.send_logout()
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">Send Message</div>
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixcore-engine
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Pure Python FIX protocol engine
5
5
  Author-email: Aidan Chisholm <aidan.chisholm@gmail.com>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fixcore-engine"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Pure Python FIX protocol engine"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
File without changes
File without changes
File without changes