fixtureqa 0.4.31__tar.gz → 0.4.33__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.31/fixtureqa.egg-info → fixtureqa-0.4.33}/PKG-INFO +1 -1
  2. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/perf_engine.py +46 -23
  3. fixtureqa-0.4.31/fixture/static/assets/index-BljNXGeO.js → fixtureqa-0.4.33/fixture/static/assets/index-C7XDCotT.js +9 -9
  4. fixtureqa-0.4.31/fixture/static/assets/index-BVMltRqp.css → fixtureqa-0.4.33/fixture/static/assets/index-Ycej0J6b.css +1 -1
  5. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/static/index.html +2 -2
  6. {fixtureqa-0.4.31 → fixtureqa-0.4.33/fixtureqa.egg-info}/PKG-INFO +1 -1
  7. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixtureqa.egg-info/SOURCES.txt +2 -2
  8. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/pyproject.toml +1 -1
  9. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_perf_engine.py +47 -0
  10. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/LICENSE +0 -0
  11. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/README.md +0 -0
  12. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/__init__.py +0 -0
  13. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/__main__.py +0 -0
  14. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/__init__.py +0 -0
  15. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/app.py +0 -0
  16. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/connection_manager.py +0 -0
  17. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/deps.py +0 -0
  18. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/__init__.py +0 -0
  19. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/admin.py +0 -0
  20. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/auth.py +0 -0
  21. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/branding.py +0 -0
  22. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/custom_tags.py +0 -0
  23. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/fix_spec.py +0 -0
  24. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/messages.py +0 -0
  25. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/perf.py +0 -0
  26. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/scenarios.py +0 -0
  27. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/sessions.py +0 -0
  28. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/setup.py +0 -0
  29. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/spec_overlay.py +0 -0
  30. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/templates.py +0 -0
  31. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/routers/ws.py +0 -0
  32. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/api/schemas.py +0 -0
  33. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/config/__init__.py +0 -0
  34. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/__init__.py +0 -0
  35. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/atomic_io.py +0 -0
  36. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/auth.py +0 -0
  37. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/config_store.py +0 -0
  38. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/custom_tag_store.py +0 -0
  39. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/db_migrations.py +0 -0
  40. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/events.py +0 -0
  41. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/fix_application.py +0 -0
  42. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/fix_builder.py +0 -0
  43. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/fix_parser.py +0 -0
  44. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/fix_spec_parser.py +0 -0
  45. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/fix_tags.py +0 -0
  46. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/fix_time.py +0 -0
  47. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/housekeeping.py +0 -0
  48. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/inbound.py +0 -0
  49. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/json_store.py +0 -0
  50. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/latency_csv_writer.py +0 -0
  51. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/message_log.py +0 -0
  52. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/message_store.py +0 -0
  53. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/models.py +0 -0
  54. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/perf_charts.py +0 -0
  55. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/perf_models.py +0 -0
  56. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/perf_payload.py +0 -0
  57. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/perf_stats.py +0 -0
  58. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/perf_store.py +0 -0
  59. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/perf_writer.py +0 -0
  60. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/scenario_runner.py +0 -0
  61. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/scenario_store.py +0 -0
  62. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/session.py +0 -0
  63. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/session_manager.py +0 -0
  64. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/spec_overlay_store.py +0 -0
  65. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/template_store.py +0 -0
  66. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/user_store.py +0 -0
  67. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/core/venue_responses.py +0 -0
  68. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/fix_specs/FIX42.xml +0 -0
  69. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/fix_specs/FIX44.xml +0 -0
  70. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/server.py +0 -0
  71. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  72. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  73. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  74. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/static/favicon.svg +0 -0
  75. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixture/ui/__init__.py +0 -0
  76. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixtureqa.egg-info/dependency_links.txt +0 -0
  77. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixtureqa.egg-info/entry_points.txt +0 -0
  78. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixtureqa.egg-info/requires.txt +0 -0
  79. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/fixtureqa.egg-info/top_level.txt +0 -0
  80. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/setup.cfg +0 -0
  81. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_atomic_io.py +0 -0
  82. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_auth.py +0 -0
  83. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_config_store.py +0 -0
  84. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_connection_manager.py +0 -0
  85. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_db_migrations.py +0 -0
  86. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_fix_builder.py +0 -0
  87. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_health.py +0 -0
  88. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_inbound.py +0 -0
  89. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_inbound_validation.py +0 -0
  90. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_message_store.py +0 -0
  91. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_perf_api.py +0 -0
  92. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_perf_models.py +0 -0
  93. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_perf_payload.py +0 -0
  94. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_perf_rehydrate.py +0 -0
  95. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_scenarios.py +0 -0
  96. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_session_auto_response.py +0 -0
  97. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_session_lifecycle.py +0 -0
  98. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_session_manager_concurrency.py +0 -0
  99. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_sessions.py +0 -0
  100. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_templates.py +0 -0
  101. {fixtureqa-0.4.31 → fixtureqa-0.4.33}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.31
3
+ Version: 0.4.33
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -311,10 +311,11 @@ class PerfRun:
311
311
  # User pressed Stop — wind down promptly; don't wait on the backlog.
312
312
  await asyncio.sleep(_GRACE_DRAIN_S)
313
313
  elif self._has_client and self._has_venue:
314
- # loopback/manager natural end (count OR duration): the in-process venue
315
- # still owes fills for the backlog — wait until they're all received,
316
- # rather than cutting off and marking in-flight orders lost.
317
- await self._drain_until_filled()
314
+ # loopback/manager/middle_manager natural end (count OR duration): the
315
+ # in-process venue still owes fills AND cancel/amend replies — wait until
316
+ # every chain is terminal, rather than cutting off and marking in-flight
317
+ # orders/cancels/amends lost.
318
+ await self._drain_until_quiescent()
318
319
  elif self._reached_max_orders:
319
320
  # client mode, count-bounded: external venue may never fill — bounded settle.
320
321
  deadline = time.perf_counter() + max(_GRACE_DRAIN_S,
@@ -363,16 +364,40 @@ class PerfRun:
363
364
  self._publish()
364
365
  self._writer.write_run(self._run_record(final=True))
365
366
 
366
- async def _drain_until_filled(self) -> None:
367
- """loopback/manager drain (count- OR duration-bounded): the in-process venue
368
- still has a backlog of orders to fill (rate-limited), so wait until it has
369
- filled the whole backlog and the client has received those fills — rather
370
- than cutting off at the stop condition and marking in-flight orders lost.
367
+ def _activity_count(self) -> int:
368
+ """Monotonic tally of every terminal-chain event fills/acks AND cancel/
369
+ amend replies so the drain's quiet window measures from the last reply,
370
+ not just the last fill."""
371
+ s = self.stats
372
+ return (s.fills_sent + s.acks_sent + s.cancel_rejects + s.cancels_canceled
373
+ + s.replaces_received + s.amend_rejects)
374
+
375
+ def _drain_quiescent(self, now: float, last_activity: float, quiet: float) -> bool:
376
+ """True when the in-process venue has nothing left in flight: inbound queue
377
+ and order book empty, the client's queue drained, NO cancel/amend reply still
378
+ outstanding on any pending order, and no chain event for a quiet window. The
379
+ in-flight-reply check is what makes a cancel/amend run reconcile to 100% —
380
+ without it the run would end on fill-quiescence while a 35=9/150=4/replace
381
+ ack was still on the wire (counted lost)."""
382
+ replies_inflight = any(e.get("cancel_inflight") or e.get("amend_inflight")
383
+ for e in self._pending.values())
384
+ return (self._venue_sub.qsize() == 0 and self._client_sub.qsize() == 0
385
+ and not self._venue_orders and not replies_inflight
386
+ and (now - last_activity) >= quiet)
387
+
388
+ async def _drain_until_quiescent(self) -> None:
389
+ """loopback/manager/middle_manager drain (count- OR duration-bounded): the
390
+ in-process venue still owes fills for the backlog AND cancel/amend replies for
391
+ any in-flight 35=F/35=G, so wait until every chain is terminal — rather than
392
+ cutting off at the stop condition and marking in-flight orders/cancels/amends
393
+ lost. (Cancel-on-ack cancels fired for orders acked during the drain are
394
+ awaited too — they land in _pending with cancel_inflight.)
371
395
 
372
396
  The timeout sweeper is paused (``_draining_full``) so backlogged orders
373
397
  aren't expired mid-drain; orders the venue won't fill (fill_ratio<1 / partial)
374
398
  settle out via quiescence and are finalized after. Bounded by a generous cap
375
- derived from the backlog and fill rate as a safety net against a stuck run."""
399
+ derived from the backlog, fill rate, amend grace and reply round-trip as a
400
+ safety net against a stuck run (e.g. a reply that never comes)."""
376
401
  self._draining_full = True
377
402
  r = self.config.rate
378
403
  fill_rate = r.fills_per_window / (r.fill_window_ms / 1000.0) # fills/sec
@@ -382,10 +407,15 @@ class PerfRun:
382
407
  # latest (no new holds once injection stops), so add one grace.
383
408
  backlog = (self._venue_sub.qsize() * max(1, self.config.payload.fill.fills_per_order)
384
409
  + sum(len(e["schedule"]) - e["idx"] for e in self._venue_orders.values()))
410
+ # Reply budget: cancel/amend replies are immediate but still round-trip — give
411
+ # the cap headroom (observed response p99, capped) so it can't fire before the
412
+ # last in-flight reply is due back.
413
+ reply_budget = min(30.0, self.stats.response_latency.stats()["p99_us"] / 1e6 * 2.0)
385
414
  cap = time.perf_counter() + max(30.0, (backlog / max(1.0, fill_rate)) * 2.0
386
- + self.config.amend.amend_wait_ms / 1000.0 + 10.0)
415
+ + self.config.amend.amend_wait_ms / 1000.0
416
+ + reply_budget + 10.0)
387
417
 
388
- last_sent = self.stats.fills_sent + self.stats.acks_sent
418
+ last_count = self._activity_count()
389
419
  last_activity = time.perf_counter()
390
420
  while True:
391
421
  now = time.perf_counter()
@@ -393,18 +423,11 @@ class PerfRun:
393
423
  # finalized as lost instead of ground through at the fill rate.
394
424
  if self._stopped_by_user or not self._pending or now >= cap:
395
425
  break
396
- cur_sent = self.stats.fills_sent + self.stats.acks_sent
397
- if cur_sent != last_sent:
398
- last_sent = cur_sent
426
+ cur = self._activity_count()
427
+ if cur != last_count:
428
+ last_count = cur
399
429
  last_activity = now
400
- # Done when the venue has drained its backlog (inbound queue AND
401
- # order book), the client has received the fills, and no new
402
- # fill/ack has gone out for a quiet window (covers the
403
- # rate-limited last order still being filled).
404
- quiet_ok = (self._venue_sub.qsize() == 0 and self._client_sub.qsize() == 0
405
- and not self._venue_orders
406
- and (now - last_activity) >= quiet)
407
- if quiet_ok:
430
+ if self._drain_quiescent(now, last_activity, quiet):
408
431
  break
409
432
  await asyncio.sleep(0.02)
410
433