fixtureqa 0.4.23__tar.gz → 0.4.25__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 (101) hide show
  1. {fixtureqa-0.4.23/fixtureqa.egg-info → fixtureqa-0.4.25}/PKG-INFO +1 -1
  2. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/perf.py +64 -14
  3. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/perf_payload.py +7 -2
  4. fixtureqa-0.4.23/fixture/static/assets/index-B3ccKxdv.js → fixtureqa-0.4.25/fixture/static/assets/index-76KAxdQ5.js +1 -1
  5. fixtureqa-0.4.23/fixture/static/assets/index-meqdijDf.css → fixtureqa-0.4.25/fixture/static/assets/index-C9bcm_lN.css +1 -1
  6. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/static/index.html +2 -2
  7. {fixtureqa-0.4.23 → fixtureqa-0.4.25/fixtureqa.egg-info}/PKG-INFO +1 -1
  8. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixtureqa.egg-info/SOURCES.txt +2 -2
  9. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/pyproject.toml +1 -1
  10. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_perf_api.py +46 -5
  11. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_perf_engine.py +29 -0
  12. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/LICENSE +0 -0
  13. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/README.md +0 -0
  14. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/__init__.py +0 -0
  15. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/__main__.py +0 -0
  16. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/__init__.py +0 -0
  17. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/app.py +0 -0
  18. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/connection_manager.py +0 -0
  19. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/deps.py +0 -0
  20. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/__init__.py +0 -0
  21. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/admin.py +0 -0
  22. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/auth.py +0 -0
  23. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/branding.py +0 -0
  24. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/custom_tags.py +0 -0
  25. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/fix_spec.py +0 -0
  26. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/messages.py +0 -0
  27. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/scenarios.py +0 -0
  28. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/sessions.py +0 -0
  29. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/setup.py +0 -0
  30. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/spec_overlay.py +0 -0
  31. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/templates.py +0 -0
  32. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/routers/ws.py +0 -0
  33. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/api/schemas.py +0 -0
  34. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/config/__init__.py +0 -0
  35. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/__init__.py +0 -0
  36. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/atomic_io.py +0 -0
  37. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/auth.py +0 -0
  38. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/config_store.py +0 -0
  39. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/custom_tag_store.py +0 -0
  40. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/db_migrations.py +0 -0
  41. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/events.py +0 -0
  42. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/fix_application.py +0 -0
  43. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/fix_builder.py +0 -0
  44. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/fix_parser.py +0 -0
  45. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/fix_spec_parser.py +0 -0
  46. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/fix_tags.py +0 -0
  47. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/fix_time.py +0 -0
  48. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/housekeeping.py +0 -0
  49. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/inbound.py +0 -0
  50. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/json_store.py +0 -0
  51. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/latency_csv_writer.py +0 -0
  52. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/message_log.py +0 -0
  53. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/message_store.py +0 -0
  54. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/models.py +0 -0
  55. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/perf_charts.py +0 -0
  56. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/perf_engine.py +0 -0
  57. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/perf_models.py +0 -0
  58. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/perf_stats.py +0 -0
  59. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/perf_store.py +0 -0
  60. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/perf_writer.py +0 -0
  61. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/scenario_runner.py +0 -0
  62. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/scenario_store.py +0 -0
  63. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/session.py +0 -0
  64. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/session_manager.py +0 -0
  65. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/spec_overlay_store.py +0 -0
  66. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/template_store.py +0 -0
  67. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/user_store.py +0 -0
  68. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/core/venue_responses.py +0 -0
  69. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/fix_specs/FIX42.xml +0 -0
  70. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/fix_specs/FIX44.xml +0 -0
  71. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/server.py +0 -0
  72. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  73. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  74. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  75. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/static/favicon.svg +0 -0
  76. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixture/ui/__init__.py +0 -0
  77. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixtureqa.egg-info/dependency_links.txt +0 -0
  78. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixtureqa.egg-info/entry_points.txt +0 -0
  79. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixtureqa.egg-info/requires.txt +0 -0
  80. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/fixtureqa.egg-info/top_level.txt +0 -0
  81. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/setup.cfg +0 -0
  82. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_atomic_io.py +0 -0
  83. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_auth.py +0 -0
  84. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_config_store.py +0 -0
  85. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_connection_manager.py +0 -0
  86. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_db_migrations.py +0 -0
  87. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_fix_builder.py +0 -0
  88. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_health.py +0 -0
  89. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_inbound.py +0 -0
  90. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_inbound_validation.py +0 -0
  91. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_message_store.py +0 -0
  92. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_perf_models.py +0 -0
  93. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_perf_payload.py +0 -0
  94. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_perf_rehydrate.py +0 -0
  95. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_scenarios.py +0 -0
  96. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_session_auto_response.py +0 -0
  97. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_session_lifecycle.py +0 -0
  98. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_session_manager_concurrency.py +0 -0
  99. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_sessions.py +0 -0
  100. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_templates.py +0 -0
  101. {fixtureqa-0.4.23 → fixtureqa-0.4.25}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.23
3
+ Version: 0.4.25
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -44,6 +44,32 @@ Admin = Annotated[User, Depends(require_platform_admin)]
44
44
 
45
45
  _TERMINAL = ("completed", "stopped", "error")
46
46
 
47
+ # Per-message persistence — durable_seqnums (fsync per message) and log_messages
48
+ # (a messages.db write per message) — happens inline on the event loop's
49
+ # send/receive path. Measured sustainable rate is ~36 msg/s on a local disk and
50
+ # materially worse on a Docker/WSL2 named volume; past that the loop stalls on
51
+ # fsync and the whole server appears frozen. We hold a conservative ceiling so a
52
+ # logged run at a low rate still works, while a high-rate logged run is refused
53
+ # up front (before the run starts) rather than wedging the server.
54
+ DURABLE_SAFE_MSG_RATE = 30 # messages/second a logged/durable session can sustain
55
+
56
+
57
+ def _est_msgs_per_order(body: RunConfig) -> int:
58
+ """Rough peak FIX message count generated per order, for the durability rate
59
+ guard: 35=D out + 35=8 ack + fills, plus a request+reply pair for each of the
60
+ amend / cancel flavours when enabled. Deliberately an upper-ish bound."""
61
+ n = 2 + body.payload.fill.fills_per_order # order + ack + fills
62
+ if body.amend.enabled:
63
+ n += 2 # 35=G + 35=8 replace
64
+ if body.cancel.enabled:
65
+ n += 2 # 35=F + 35=8/9 reply
66
+ return max(1, n)
67
+
68
+
69
+ def _order_rate_per_s(body: RunConfig) -> float:
70
+ r = body.rate
71
+ return r.orders_per_window * 1000.0 / r.order_window_ms
72
+
47
73
 
48
74
  def _require_started(sm: SessionManager, sid: str, label: str):
49
75
  try:
@@ -68,10 +94,45 @@ async def create_run(body: RunConfig, sm: SM, reg: REG, ts: TS, admin: Admin):
68
94
  # (a sync def route runs in a threadpool with no running loop).
69
95
  need_client = body.mode in ("loopback", "manager", "client")
70
96
  need_venue = body.mode in ("loopback", "manager", "venue")
97
+ participating: list[tuple[str, object]] = [] # (label, config) for fixture legs
71
98
  if need_client:
72
- _require_started(sm, body.client_session_id, "client")
99
+ participating.append(("client", _require_started(sm, body.client_session_id, "client").config))
73
100
  if need_venue:
74
- _require_started(sm, body.venue_session_id, "venue")
101
+ participating.append(("venue", _require_started(sm, body.venue_session_id, "venue").config))
102
+
103
+ # Durability rate guard — BEFORE reg.create() (which starts the run). A
104
+ # session with durable_seqnums or log_messages persists every message inline
105
+ # on the event loop; above a low rate this stalls the loop and freezes the
106
+ # whole server. Allow a logged run at a safe low rate (useful to inspect the
107
+ # messages); refuse above it with the safe order rate spelled out.
108
+ logged = [(label, cfg) for label, cfg in participating
109
+ if cfg.durable_seqnums or cfg.log_messages]
110
+ durable_warnings: list[str] = []
111
+ if logged:
112
+ mpo = _est_msgs_per_order(body)
113
+ est_msg_rate = _order_rate_per_s(body) * mpo
114
+ safe_order_rate = DURABLE_SAFE_MSG_RATE / mpo
115
+
116
+ def _flags(cfg) -> str:
117
+ f = [n for n, on in (("durable_seqnums", cfg.durable_seqnums),
118
+ ("log_messages", cfg.log_messages)) if on]
119
+ return "+".join(f)
120
+ who = ", ".join(f"{label} ({_flags(cfg)})" for label, cfg in logged)
121
+
122
+ if est_msg_rate > DURABLE_SAFE_MSG_RATE:
123
+ raise HTTPException(status_code=409, detail=(
124
+ f"This run would generate ~{est_msg_rate:.0f} msg/s "
125
+ f"(~{mpo} msgs/order × {_order_rate_per_s(body):.0f} orders/s), but "
126
+ f"{who} persist every message inline on the event loop "
127
+ f"(fsync / messages.db write). Above ~{DURABLE_SAFE_MSG_RATE} msg/s "
128
+ f"this stalls the server. Either lower the order rate to "
129
+ f"≤ {safe_order_rate:.0f}/s to keep message logging, or convert the "
130
+ f"session(s) to a Performance session (durable_seqnums + log_messages "
131
+ f"off) for high-rate runs."))
132
+ durable_warnings.append(
133
+ f"message persistence is on for {who}; safe only up to "
134
+ f"~{safe_order_rate:.0f} orders/s (~{DURABLE_SAFE_MSG_RATE} msg/s) — at "
135
+ f"higher rates the event loop stalls on per-message fsync/DB writes")
75
136
 
76
137
  # CompIDs from the driving (client, else venue) session — for template tokens.
77
138
  drive_sid = body.client_session_id if need_client else body.venue_session_id
@@ -82,18 +143,7 @@ async def create_run(body: RunConfig, sm: SM, reg: REG, ts: TS, admin: Admin):
82
143
  except PerfConfigError as e:
83
144
  raise HTTPException(status_code=422, detail=str(e))
84
145
 
85
- # Soft perf warnings — don't block, but flag footguns.
86
- checked = []
87
- if need_client:
88
- checked.append((body.client_session_id, "client"))
89
- if need_venue:
90
- checked.append((body.venue_session_id, "venue"))
91
- for sid, label in checked:
92
- cfg = sm.get_state(sid).config
93
- if cfg.durable_seqnums:
94
- run.warnings.append(f"{label} session has durable_seqnums=True — fsync per message caps throughput")
95
- if cfg.log_messages:
96
- run.warnings.append(f"{label} session has log_messages=True — persistence churn adds loop latency")
146
+ run.warnings.extend(durable_warnings)
97
147
  return {"run_id": run.run_id}
98
148
 
99
149
 
@@ -503,8 +503,13 @@ class PayloadFactory:
503
503
  order_qty = ctx["qty"]
504
504
  msg.set_field(38, str(order_qty))
505
505
  self._apply_expiry(msg, fields)
506
- return msg, {"corr_id": corr, "clordid": clordid, "symbol": ctx["symbol"],
507
- "side": ctx["side"], "qty": ctx["qty"], "price": ctx["price"],
506
+ # Mirror the values that actually went on the wire (template-rendered when
507
+ # present, else the ctx defaults we just set) so amend/cancel 35=G/35=F
508
+ # echo the order's real Symbol(55)/Side(54), not the ORDER PAYLOAD section.
509
+ return msg, {"corr_id": corr, "clordid": clordid,
510
+ "symbol": rendered.get(55, ctx["symbol"]),
511
+ "side": rendered.get(54, ctx["side"]),
512
+ "qty": ctx["qty"], "price": ctx["price"],
508
513
  "order_qty": order_qty, "msg_type": mtype,
509
514
  "mirror_tags": self._mirror_tags(msg)}
510
515