fixtureqa 0.4.25__tar.gz → 0.4.26__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.25/fixtureqa.egg-info → fixtureqa-0.4.26}/PKG-INFO +1 -1
  2. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/perf.py +25 -21
  3. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_engine.py +25 -0
  4. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_models.py +23 -0
  5. fixtureqa-0.4.25/fixture/static/assets/index-76KAxdQ5.js → fixtureqa-0.4.26/fixture/static/assets/index-C7Iff2Mt.js +23 -23
  6. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/index.html +1 -1
  7. {fixtureqa-0.4.25 → fixtureqa-0.4.26/fixtureqa.egg-info}/PKG-INFO +1 -1
  8. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/SOURCES.txt +1 -1
  9. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/pyproject.toml +1 -1
  10. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_api.py +26 -0
  11. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_engine.py +21 -0
  12. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_models.py +26 -0
  13. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/LICENSE +0 -0
  14. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/README.md +0 -0
  15. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/__init__.py +0 -0
  16. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/__main__.py +0 -0
  17. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/__init__.py +0 -0
  18. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/app.py +0 -0
  19. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/connection_manager.py +0 -0
  20. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/deps.py +0 -0
  21. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/__init__.py +0 -0
  22. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/admin.py +0 -0
  23. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/auth.py +0 -0
  24. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/branding.py +0 -0
  25. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/custom_tags.py +0 -0
  26. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/fix_spec.py +0 -0
  27. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/messages.py +0 -0
  28. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/scenarios.py +0 -0
  29. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/sessions.py +0 -0
  30. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/setup.py +0 -0
  31. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/spec_overlay.py +0 -0
  32. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/templates.py +0 -0
  33. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/ws.py +0 -0
  34. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/schemas.py +0 -0
  35. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/config/__init__.py +0 -0
  36. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/__init__.py +0 -0
  37. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/atomic_io.py +0 -0
  38. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/auth.py +0 -0
  39. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/config_store.py +0 -0
  40. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/custom_tag_store.py +0 -0
  41. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/db_migrations.py +0 -0
  42. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/events.py +0 -0
  43. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_application.py +0 -0
  44. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_builder.py +0 -0
  45. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_parser.py +0 -0
  46. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_spec_parser.py +0 -0
  47. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_tags.py +0 -0
  48. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_time.py +0 -0
  49. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/housekeeping.py +0 -0
  50. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/inbound.py +0 -0
  51. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/json_store.py +0 -0
  52. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/latency_csv_writer.py +0 -0
  53. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/message_log.py +0 -0
  54. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/message_store.py +0 -0
  55. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/models.py +0 -0
  56. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_charts.py +0 -0
  57. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_payload.py +0 -0
  58. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_stats.py +0 -0
  59. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_store.py +0 -0
  60. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_writer.py +0 -0
  61. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/scenario_runner.py +0 -0
  62. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/scenario_store.py +0 -0
  63. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/session.py +0 -0
  64. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/session_manager.py +0 -0
  65. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/spec_overlay_store.py +0 -0
  66. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/template_store.py +0 -0
  67. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/user_store.py +0 -0
  68. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/venue_responses.py +0 -0
  69. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/fix_specs/FIX42.xml +0 -0
  70. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/fix_specs/FIX44.xml +0 -0
  71. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/server.py +0 -0
  72. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  73. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/assets/index-C9bcm_lN.css +0 -0
  74. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  75. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  76. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/favicon.svg +0 -0
  77. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/ui/__init__.py +0 -0
  78. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/dependency_links.txt +0 -0
  79. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/entry_points.txt +0 -0
  80. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/requires.txt +0 -0
  81. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/top_level.txt +0 -0
  82. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/setup.cfg +0 -0
  83. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_atomic_io.py +0 -0
  84. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_auth.py +0 -0
  85. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_config_store.py +0 -0
  86. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_connection_manager.py +0 -0
  87. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_db_migrations.py +0 -0
  88. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_fix_builder.py +0 -0
  89. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_health.py +0 -0
  90. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_inbound.py +0 -0
  91. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_inbound_validation.py +0 -0
  92. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_message_store.py +0 -0
  93. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_payload.py +0 -0
  94. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_rehydrate.py +0 -0
  95. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_scenarios.py +0 -0
  96. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_session_auto_response.py +0 -0
  97. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_session_lifecycle.py +0 -0
  98. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_session_manager_concurrency.py +0 -0
  99. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_sessions.py +0 -0
  100. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_templates.py +0 -0
  101. {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.25
3
+ Version: 0.4.26
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -17,7 +17,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSock
17
17
  from fastapi.responses import FileResponse, Response, StreamingResponse
18
18
 
19
19
  from ...core.models import SessionStatus
20
- from ...core.perf_models import RunConfig, RunStatus
20
+ from ...core.perf_models import RunConfig, RunStatus, est_msgs_per_order
21
21
  from ...core.perf_charts import render_latency_chart
22
22
  from ...core.perf_engine import RunRegistry
23
23
  from ...core.perf_payload import PerfConfigError
@@ -54,18 +54,6 @@ _TERMINAL = ("completed", "stopped", "error")
54
54
  DURABLE_SAFE_MSG_RATE = 30 # messages/second a logged/durable session can sustain
55
55
 
56
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
57
  def _order_rate_per_s(body: RunConfig) -> float:
70
58
  r = body.rate
71
59
  return r.orders_per_window * 1000.0 / r.order_window_ms
@@ -100,17 +88,32 @@ async def create_run(body: RunConfig, sm: SM, reg: REG, ts: TS, admin: Admin):
100
88
  if need_venue:
101
89
  participating.append(("venue", _require_started(sm, body.venue_session_id, "venue").config))
102
90
 
91
+ # Optional per-session msgs/s ceiling: validate the named session actually
92
+ # participates, then fold it into the effective order rate (the injector is
93
+ # throttled to min(configured, ceiling) — see PerfRun). mpo every message
94
+ # crosses the link, so each leg touches ~mpo messages per order.
95
+ participant_sids = {sid for sid in (body.client_session_id, body.venue_session_id) if sid}
96
+ csid = body.rate.ceiling_session_id
97
+ if body.rate.max_msgs_per_sec is not None and csid and csid not in participant_sids:
98
+ raise HTTPException(status_code=422, detail=(
99
+ f"rate.ceiling_session_id {csid!r} is not a session this run drives "
100
+ f"(participants: {', '.join(sorted(participant_sids)) or 'none'})"))
101
+ mpo = est_msgs_per_order(body)
102
+ effective_ops = _order_rate_per_s(body)
103
+ if body.rate.max_msgs_per_sec is not None:
104
+ effective_ops = min(effective_ops, body.rate.max_msgs_per_sec / mpo)
105
+
103
106
  # Durability rate guard — BEFORE reg.create() (which starts the run). A
104
107
  # session with durable_seqnums or log_messages persists every message inline
105
108
  # 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.
109
+ # whole server. Evaluated on the EFFECTIVE rate, so a msgs/s ceiling that
110
+ # holds a logged session under the limit is a legitimate escape hatch (the run
111
+ # is allowed, just throttled) instead of being refused outright.
108
112
  logged = [(label, cfg) for label, cfg in participating
109
113
  if cfg.durable_seqnums or cfg.log_messages]
110
114
  durable_warnings: list[str] = []
111
115
  if logged:
112
- mpo = _est_msgs_per_order(body)
113
- est_msg_rate = _order_rate_per_s(body) * mpo
116
+ est_msg_rate = effective_ops * mpo
114
117
  safe_order_rate = DURABLE_SAFE_MSG_RATE / mpo
115
118
 
116
119
  def _flags(cfg) -> str:
@@ -122,13 +125,14 @@ async def create_run(body: RunConfig, sm: SM, reg: REG, ts: TS, admin: Admin):
122
125
  if est_msg_rate > DURABLE_SAFE_MSG_RATE:
123
126
  raise HTTPException(status_code=409, detail=(
124
127
  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 "
128
+ f"(~{mpo} msgs/order × {effective_ops:.0f} orders/s), but "
126
129
  f"{who} persist every message inline on the event loop "
127
130
  f"(fsync / messages.db write). Above ~{DURABLE_SAFE_MSG_RATE} msg/s "
128
131
  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
+ f"≤ {safe_order_rate:.0f}/s, set a per-session messages/sec ceiling "
133
+ f"that holds the logged session under ~{DURABLE_SAFE_MSG_RATE} msg/s, "
134
+ f"or convert the session(s) to a Performance session (durable_seqnums "
135
+ f"+ log_messages off) for high-rate runs."))
132
136
  durable_warnings.append(
133
137
  f"message persistence is on for {who}; safe only up to "
134
138
  f"~{safe_order_rate:.0f} orders/s (~{DURABLE_SAFE_MSG_RATE} msg/s) — at "
@@ -35,6 +35,7 @@ from .latency_csv_writer import LatencyCsvWriter
35
35
  from .fix_time import utc_timestamp
36
36
  from .perf_models import (
37
37
  RunConfig, RunStatus, LiveSnapshot, ClientLeg, VenueLeg, SnapshotErrors, LatencyStats,
38
+ est_msgs_per_order,
38
39
  )
39
40
  from .perf_payload import PayloadFactory, ScenarioSelector
40
41
  from .perf_stats import BurstGate, PerfStats, TokenBucket
@@ -189,6 +190,17 @@ class PerfRun:
189
190
  self._order_bucket = _pacer(r.orders_per_window, r.order_window_ms, r.dispatch)
190
191
  self._fill_bucket = _pacer(r.fills_per_window, r.fill_window_ms, r.fill_dispatch)
191
192
 
193
+ # Optional per-session messages/sec ceiling: a second smooth bucket the
194
+ # injector acquires in series with _order_bucket, so the effective order
195
+ # rate is the min of the two. ceiling_ops = max_msgs_per_sec / est-msgs/order;
196
+ # expressed as TokenBucket(1, window) the rate is 1000/window_ms orders/s
197
+ # with capacity 1 (smooth, no over-capacity acquire(n) deadlock).
198
+ self._ceiling_bucket: Optional[TokenBucket] = None
199
+ if r.max_msgs_per_sec is not None:
200
+ mpo = est_msgs_per_order(config)
201
+ self._ceiling_ops = r.max_msgs_per_sec / mpo
202
+ self._ceiling_bucket = TokenBucket(1, max(1, round(1000 * mpo / r.max_msgs_per_sec)))
203
+
192
204
  self._client_sub = None
193
205
  self._venue_sub = None
194
206
  self._tasks: list[asyncio.Task] = []
@@ -230,6 +242,15 @@ class PerfRun:
230
242
  self.warnings.append(
231
243
  "cancel injection has no effect in venue mode (no client leg); "
232
244
  "inbound 35=F from the external client is still answered")
245
+ if self._ceiling_bucket is not None and self._has_client:
246
+ r = self.config.rate
247
+ order_ops = r.orders_per_window * 1000.0 / r.order_window_ms
248
+ if self._ceiling_ops < order_ops:
249
+ who = r.ceiling_session_id or "the chosen session"
250
+ self.warnings.append(
251
+ f"injector capped to ~{self._ceiling_ops:.0f} orders/s to hold "
252
+ f"{who} under {r.max_msgs_per_sec} msg/s (order rate "
253
+ f"{order_ops:.0f}/s would exceed it)")
233
254
  if self._has_client:
234
255
  self._client_sub = self._sm.subscribe_session(self.config.client_session_id)
235
256
  self._sm.mark_perf_active(self.config.client_session_id)
@@ -370,6 +391,8 @@ class PerfRun:
370
391
  await self._dispatch_scenario(scn)
371
392
  else:
372
393
  await self._order_bucket.acquire()
394
+ if self._ceiling_bucket is not None:
395
+ await self._ceiling_bucket.acquire()
373
396
  if not self._injecting:
374
397
  break
375
398
  msg, meta = self._factory.build_single_order()
@@ -453,6 +476,8 @@ class PerfRun:
453
476
 
454
477
  for tpl in scn.messages:
455
478
  await self._order_bucket.acquire()
479
+ if self._ceiling_bucket is not None:
480
+ await self._ceiling_bucket.acquire() # approx in scenario mode: per message
456
481
  if not self._injecting:
457
482
  return
458
483
  msg, meta = self._factory.build_template_message(tpl, ctx, refs)
@@ -40,6 +40,15 @@ class RateConfig(BaseModel):
40
40
  fill_ratio: float = Field(default=1.0, ge=0.0, le=1.0)
41
41
  allow_burst: bool = False
42
42
  max_burst_multiplier: float = Field(default=2.0, ge=1.0)
43
+ # Optional per-session messages/sec ceiling. The injector is throttled to the
44
+ # order rate that keeps the chosen session under `max_msgs_per_sec` (using
45
+ # est_msgs_per_order — every per-order message crosses the link, so each leg
46
+ # touches ~the same count). ceiling_session_id is the session it protects:
47
+ # informational (UI label, warning text, participant validation), not the math.
48
+ # Throttling only the injector keeps latency clean and can't deadlock the
49
+ # lifecycle (replies/acks stay immediate). None = no ceiling.
50
+ max_msgs_per_sec: Optional[int] = Field(default=None, gt=0)
51
+ ceiling_session_id: Optional[str] = None
43
52
 
44
53
 
45
54
  class TestConfig(BaseModel):
@@ -222,6 +231,20 @@ class RunConfig(BaseModel):
222
231
  return self
223
232
 
224
233
 
234
+ def est_msgs_per_order(cfg: "RunConfig") -> int:
235
+ """Rough peak FIX message count generated per order — the one formula shared
236
+ by the durability rate guard (api/routers/perf.py) and the per-session msgs/s
237
+ ceiling (perf_engine.py): 35=D out + 35=8 ack + fills, plus a request+reply
238
+ pair for each of the amend / cancel flavours when enabled. Deliberately an
239
+ upper-ish bound (a tighter divisor → a more conservative cap)."""
240
+ n = 2 + cfg.payload.fill.fills_per_order # order + ack + fills
241
+ if cfg.amend.enabled:
242
+ n += 2 # 35=G + 35=8 replace
243
+ if cfg.cancel.enabled:
244
+ n += 2 # 35=F + 35=8/9 reply
245
+ return max(1, n)
246
+
247
+
225
248
  # ---------------------------------------------------------------------------
226
249
  # Responses (read by key downstream — keep stable)
227
250
  # ---------------------------------------------------------------------------