fixtureqa 0.4.8__tar.gz → 0.4.9__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 (99) hide show
  1. {fixtureqa-0.4.8/fixtureqa.egg-info → fixtureqa-0.4.9}/PKG-INFO +1 -1
  2. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_engine.py +4 -2
  3. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_store.py +19 -2
  4. {fixtureqa-0.4.8 → fixtureqa-0.4.9/fixtureqa.egg-info}/PKG-INFO +1 -1
  5. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/pyproject.toml +1 -1
  6. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_api.py +29 -0
  7. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_engine.py +21 -0
  8. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/LICENSE +0 -0
  9. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/README.md +0 -0
  10. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/__init__.py +0 -0
  11. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/__main__.py +0 -0
  12. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/__init__.py +0 -0
  13. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/app.py +0 -0
  14. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/connection_manager.py +0 -0
  15. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/deps.py +0 -0
  16. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/__init__.py +0 -0
  17. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/admin.py +0 -0
  18. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/auth.py +0 -0
  19. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/branding.py +0 -0
  20. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/custom_tags.py +0 -0
  21. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/fix_spec.py +0 -0
  22. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/messages.py +0 -0
  23. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/perf.py +0 -0
  24. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/scenarios.py +0 -0
  25. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/sessions.py +0 -0
  26. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/setup.py +0 -0
  27. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/spec_overlay.py +0 -0
  28. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/templates.py +0 -0
  29. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/ws.py +0 -0
  30. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/schemas.py +0 -0
  31. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/config/__init__.py +0 -0
  32. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/__init__.py +0 -0
  33. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/atomic_io.py +0 -0
  34. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/auth.py +0 -0
  35. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/config_store.py +0 -0
  36. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/custom_tag_store.py +0 -0
  37. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/db_migrations.py +0 -0
  38. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/events.py +0 -0
  39. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/exec_csv_writer.py +0 -0
  40. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_application.py +0 -0
  41. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_builder.py +0 -0
  42. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_parser.py +0 -0
  43. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_spec_parser.py +0 -0
  44. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_tags.py +0 -0
  45. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_time.py +0 -0
  46. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/housekeeping.py +0 -0
  47. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/inbound.py +0 -0
  48. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/json_store.py +0 -0
  49. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/message_log.py +0 -0
  50. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/message_store.py +0 -0
  51. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/models.py +0 -0
  52. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_models.py +0 -0
  53. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_payload.py +0 -0
  54. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_stats.py +0 -0
  55. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_writer.py +0 -0
  56. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/scenario_runner.py +0 -0
  57. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/scenario_store.py +0 -0
  58. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/session.py +0 -0
  59. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/session_manager.py +0 -0
  60. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/spec_overlay_store.py +0 -0
  61. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/template_store.py +0 -0
  62. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/user_store.py +0 -0
  63. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/venue_responses.py +0 -0
  64. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/fix_specs/FIX42.xml +0 -0
  65. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/fix_specs/FIX44.xml +0 -0
  66. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/server.py +0 -0
  67. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  68. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/index-BwQf-cei.css +0 -0
  69. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  70. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/index-DrmyYeG0.js +0 -0
  71. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  72. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/favicon.svg +0 -0
  73. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/index.html +0 -0
  74. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/ui/__init__.py +0 -0
  75. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/SOURCES.txt +0 -0
  76. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/dependency_links.txt +0 -0
  77. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/entry_points.txt +0 -0
  78. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/requires.txt +0 -0
  79. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/top_level.txt +0 -0
  80. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/setup.cfg +0 -0
  81. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_atomic_io.py +0 -0
  82. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_auth.py +0 -0
  83. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_config_store.py +0 -0
  84. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_connection_manager.py +0 -0
  85. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_db_migrations.py +0 -0
  86. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_fix_builder.py +0 -0
  87. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_health.py +0 -0
  88. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_inbound.py +0 -0
  89. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_inbound_validation.py +0 -0
  90. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_message_store.py +0 -0
  91. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_models.py +0 -0
  92. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_payload.py +0 -0
  93. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_rehydrate.py +0 -0
  94. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_scenarios.py +0 -0
  95. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_session_lifecycle.py +0 -0
  96. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_session_manager_concurrency.py +0 -0
  97. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_sessions.py +0 -0
  98. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_templates.py +0 -0
  99. {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.8
3
+ Version: 0.4.9
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -206,7 +206,7 @@ class PerfRun:
206
206
  # client mode, count-bounded: external venue may never fill — bounded settle.
207
207
  deadline = time.perf_counter() + max(_GRACE_DRAIN_S,
208
208
  self.config.test.scenario_timeout_ms / 1000.0)
209
- while self._pending and time.perf_counter() < deadline:
209
+ while self._pending and time.perf_counter() < deadline and not self._stopped_by_user:
210
210
  await asyncio.sleep(0.02)
211
211
  else:
212
212
  await asyncio.sleep(_GRACE_DRAIN_S)
@@ -252,7 +252,9 @@ class PerfRun:
252
252
  last_activity = time.perf_counter()
253
253
  while True:
254
254
  now = time.perf_counter()
255
- if not self._pending or now >= cap:
255
+ # Stop pressed mid-drain must win immediately — the backlog gets
256
+ # finalized as lost instead of ground through at the fill rate.
257
+ if self._stopped_by_user or not self._pending or now >= cap:
256
258
  break
257
259
  cur_sent = self.stats.fills_sent + self.stats.acks_sent
258
260
  if cur_sent != last_sent:
@@ -6,6 +6,7 @@ parameter is unused.
6
6
  from __future__ import annotations
7
7
 
8
8
  import os
9
+ import uuid
9
10
  from typing import Optional
10
11
 
11
12
  from .json_store import JsonListStore
@@ -28,8 +29,24 @@ class PerfStore(JsonListStore):
28
29
  return item["config"] if item else None
29
30
 
30
31
  def save(self, config: dict) -> str:
31
- item = self.create_item("", {"name": config.get("name", ""), "config": config})
32
- return item["config_id"]
32
+ """Upsert by name: saving a config whose name already exists overwrites
33
+ it in place (same config_id), so the dropdown never accumulates
34
+ duplicates. Pre-existing duplicates of that name are collapsed into the
35
+ first one. One locked load-modify-save so the find+replace is atomic."""
36
+ name = config.get("name", "")
37
+ with self._lock:
38
+ items = self._load("")
39
+ existing = [it for it in items if it.get("name") == name]
40
+ if existing:
41
+ keep = existing[0]
42
+ keep["config"] = config
43
+ items = [it for it in items if it.get("name") != name or it is keep]
44
+ self._save("", items)
45
+ return keep["config_id"]
46
+ item = {"name": name, "config": config, "config_id": str(uuid.uuid4())}
47
+ items.append(item)
48
+ self._save("", items)
49
+ return item["config_id"]
33
50
 
34
51
  def delete(self, config_id: str) -> bool:
35
52
  return self.delete_item("", config_id)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.8
3
+ Version: 0.4.9
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fixtureqa"
7
- version = "0.4.8"
7
+ version = "0.4.9"
8
8
  description = "FIXture — FIX Protocol Testing Tool"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -113,6 +113,35 @@ def test_perf_configs_crud(authed):
113
113
  assert client.get(f"/api/perf/configs/{cid}").status_code == 404
114
114
 
115
115
 
116
+ def test_perf_config_save_same_name_overwrites(authed):
117
+ client, _ = authed
118
+ body = _run_body("c", "v")
119
+ body["name"] = "upsert-me"
120
+ cid1 = client.post("/api/perf/configs", json=body).json()["config_id"]
121
+ body["rate"]["orders_per_window"] = 99 # same name, new content
122
+ cid2 = client.post("/api/perf/configs", json=body).json()["config_id"]
123
+ assert cid2 == cid1 # overwrote in place
124
+ names = [c["name"] for c in client.get("/api/perf/configs").json()]
125
+ assert names.count("upsert-me") == 1 # no duplicate dropdown rows
126
+ got = client.get(f"/api/perf/configs/{cid1}").json()
127
+ assert got["rate"]["orders_per_window"] == 99 # latest content won
128
+ client.delete(f"/api/perf/configs/{cid1}")
129
+
130
+
131
+ def test_perf_store_collapses_legacy_duplicates(tmp_path):
132
+ # Duplicates saved before the upsert fix collapse into the first entry the
133
+ # next time that name is saved.
134
+ from fixture.core.perf_store import PerfStore
135
+ store = PerfStore(str(tmp_path))
136
+ a = store.create_item("", {"name": "dup", "config": {"name": "dup", "v": 1}})
137
+ store.create_item("", {"name": "dup", "config": {"name": "dup", "v": 2}})
138
+ assert sum(c["name"] == "dup" for c in store.list()) == 2
139
+ cid = store.save({"name": "dup", "v": 3})
140
+ assert cid == a["config_id"] # first entry keeps its id
141
+ assert sum(c["name"] == "dup" for c in store.list()) == 1
142
+ assert store.get(cid)["v"] == 3
143
+
144
+
116
145
  def test_perf_run_rejects_unstarted_session(authed):
117
146
  client, _ = authed
118
147
  port = _free_port()
@@ -699,6 +699,27 @@ async def test_venue_answers_inbound_amend():
699
699
  assert run.stats.amends_received == 1
700
700
 
701
701
 
702
+ async def test_stop_during_drain_wins_immediately():
703
+ # Starved fill leg → the post-duration drain would grind for minutes
704
+ # (cap ≈ 2× backlog/fill_rate). Stop pressed mid-drain must end the run
705
+ # promptly, finalizing the backlog as lost, not wait out the drain.
706
+ cfg = _cfg(
707
+ rate=RateConfig(orders_per_window=50, order_window_ms=100, # ~500/s in
708
+ fills_per_window=1, fill_window_ms=1000), # 1 fill/s out
709
+ test=RunTestConfig(duration=1),
710
+ )
711
+ run = _run(cfg)
712
+ run.start()
713
+ await asyncio.sleep(1.3) # duration fired → inside drain
714
+ assert run.status == "running" # still draining the backlog
715
+ t0 = time.perf_counter()
716
+ await asyncio.wait_for(run.stop(), timeout=10)
717
+ assert time.perf_counter() - t0 < 5 # did not ride out the drain
718
+ assert run.status == "stopped"
719
+ assert not run._pending # backlog finalized
720
+ assert run.stats.orders_sent > run.stats.completions # it really was starved
721
+
722
+
702
723
  async def test_venue_amend_without_pending_replace():
703
724
  run = _run(_cfg(amend=AmendConfig(enabled=True)))
704
725
  await run._handle_amend({35: "G", 11: "NEW-2", 41: "OLD-2", 376: "VC2",
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