fixtureqa 0.4.20__tar.gz → 0.4.21__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 (100) hide show
  1. {fixtureqa-0.4.20/fixtureqa.egg-info → fixtureqa-0.4.21}/PKG-INFO +1 -1
  2. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/perf.py +13 -8
  3. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_charts.py +37 -10
  4. fixtureqa-0.4.20/fixture/static/assets/index-BhxUj8V-.js → fixtureqa-0.4.21/fixture/static/assets/index-BzzfsRz2.js +2 -2
  5. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/index.html +1 -1
  6. {fixtureqa-0.4.20 → fixtureqa-0.4.21/fixtureqa.egg-info}/PKG-INFO +1 -1
  7. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/SOURCES.txt +1 -1
  8. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/pyproject.toml +1 -1
  9. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_api.py +3 -1
  10. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_engine.py +17 -0
  11. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/LICENSE +0 -0
  12. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/README.md +0 -0
  13. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/__init__.py +0 -0
  14. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/__main__.py +0 -0
  15. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/__init__.py +0 -0
  16. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/app.py +0 -0
  17. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/connection_manager.py +0 -0
  18. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/deps.py +0 -0
  19. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/__init__.py +0 -0
  20. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/admin.py +0 -0
  21. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/auth.py +0 -0
  22. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/branding.py +0 -0
  23. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/custom_tags.py +0 -0
  24. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/fix_spec.py +0 -0
  25. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/messages.py +0 -0
  26. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/scenarios.py +0 -0
  27. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/sessions.py +0 -0
  28. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/setup.py +0 -0
  29. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/spec_overlay.py +0 -0
  30. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/templates.py +0 -0
  31. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/routers/ws.py +0 -0
  32. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/api/schemas.py +0 -0
  33. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/config/__init__.py +0 -0
  34. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/__init__.py +0 -0
  35. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/atomic_io.py +0 -0
  36. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/auth.py +0 -0
  37. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/config_store.py +0 -0
  38. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/custom_tag_store.py +0 -0
  39. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/db_migrations.py +0 -0
  40. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/events.py +0 -0
  41. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_application.py +0 -0
  42. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_builder.py +0 -0
  43. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_parser.py +0 -0
  44. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_spec_parser.py +0 -0
  45. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_tags.py +0 -0
  46. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/fix_time.py +0 -0
  47. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/housekeeping.py +0 -0
  48. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/inbound.py +0 -0
  49. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/json_store.py +0 -0
  50. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/latency_csv_writer.py +0 -0
  51. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/message_log.py +0 -0
  52. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/message_store.py +0 -0
  53. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/models.py +0 -0
  54. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_engine.py +0 -0
  55. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_models.py +0 -0
  56. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_payload.py +0 -0
  57. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_stats.py +0 -0
  58. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_store.py +0 -0
  59. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/perf_writer.py +0 -0
  60. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/scenario_runner.py +0 -0
  61. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/scenario_store.py +0 -0
  62. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/session.py +0 -0
  63. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/session_manager.py +0 -0
  64. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/spec_overlay_store.py +0 -0
  65. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/template_store.py +0 -0
  66. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/user_store.py +0 -0
  67. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/core/venue_responses.py +0 -0
  68. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/fix_specs/FIX42.xml +0 -0
  69. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/fix_specs/FIX44.xml +0 -0
  70. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/server.py +0 -0
  71. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  72. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  73. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/assets/index-o_l1KBYd.css +0 -0
  74. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  75. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/static/favicon.svg +0 -0
  76. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixture/ui/__init__.py +0 -0
  77. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/dependency_links.txt +0 -0
  78. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/entry_points.txt +0 -0
  79. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/requires.txt +0 -0
  80. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/fixtureqa.egg-info/top_level.txt +0 -0
  81. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/setup.cfg +0 -0
  82. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_atomic_io.py +0 -0
  83. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_auth.py +0 -0
  84. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_config_store.py +0 -0
  85. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_connection_manager.py +0 -0
  86. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_db_migrations.py +0 -0
  87. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_fix_builder.py +0 -0
  88. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_health.py +0 -0
  89. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_inbound.py +0 -0
  90. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_inbound_validation.py +0 -0
  91. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_message_store.py +0 -0
  92. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_models.py +0 -0
  93. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_payload.py +0 -0
  94. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_perf_rehydrate.py +0 -0
  95. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_scenarios.py +0 -0
  96. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_session_lifecycle.py +0 -0
  97. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_session_manager_concurrency.py +0 -0
  98. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_sessions.py +0 -0
  99. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_templates.py +0 -0
  100. {fixtureqa-0.4.20 → fixtureqa-0.4.21}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.20
3
+ Version: 0.4.21
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -212,13 +212,18 @@ def run_export(run_id: str, reg: REG, pw: PW, _admin: Admin,
212
212
 
213
213
  @router.get("/runs/{run_id}/chart")
214
214
  async def run_chart(run_id: str, reg: REG, _admin: Admin,
215
- metric: str = Query("one_way"), agg: str = Query("both")):
215
+ metric: str = Query("one_way"), agg: str = Query("both"),
216
+ scale: str = Query("log")):
216
217
  """Per-message latency chart (PNG) from the run's recorded csv.gz. Rendered
217
- off-loop on first request and cached per (metric, agg) — the CSV is immutable
218
- once the run finishes. Needs record_latency; 404 otherwise, 409 while live."""
219
- if metric not in ("one_way", "round_trip", "both") or agg not in ("scatter", "percentile", "both"):
218
+ off-loop on first request and cached per (metric, agg, scale) — the CSV is
219
+ immutable once the run finishes. Needs record_latency; 404 otherwise, 409
220
+ while live."""
221
+ if (metric not in ("one_way", "round_trip", "both")
222
+ or agg not in ("scatter", "percentile", "both")
223
+ or scale not in ("log", "linear")):
220
224
  raise HTTPException(status_code=422,
221
- detail="metric must be one_way|round_trip|both; agg must be scatter|percentile|both")
225
+ detail="metric must be one_way|round_trip|both; "
226
+ "agg must be scatter|percentile|both; scale must be log|linear")
222
227
  if not reg.exists(run_id):
223
228
  raise HTTPException(status_code=404, detail="Run not found")
224
229
  if _run_is_live(reg, run_id):
@@ -234,13 +239,13 @@ async def run_chart(run_id: str, reg: REG, _admin: Admin,
234
239
  one_way_label = None
235
240
  if st is not None and st.mode not in ("loopback", "manager"):
236
241
  one_way_label = "one-way (tag-60 wire send → recv; needs synced clocks)"
237
- out_path = f"{csv_path[:-len('.csv.gz')]}_{metric}_{agg}.png"
242
+ out_path = f"{csv_path[:-len('.csv.gz')]}_{metric}_{agg}_{scale}.png"
238
243
  if not os.path.exists(out_path):
239
244
  try:
240
245
  await asyncio.to_thread(
241
246
  render_latency_chart, csv_path, out_path,
242
- metric=metric, agg=agg, title=f"Per-message latency — {run_id}",
243
- one_way_label=one_way_label)
247
+ metric=metric, agg=agg, logy=(scale == "log"),
248
+ title=f"Per-message latency — {run_id}", one_way_label=one_way_label)
244
249
  except ValueError as e:
245
250
  # No chartable points (e.g. recording produced only un-latencied rows).
246
251
  raise HTTPException(status_code=404, detail=str(e))
@@ -53,10 +53,25 @@ def _pctl(sorted_vals: list[float], q: float) -> float:
53
53
  return sorted_vals[i]
54
54
 
55
55
 
56
+ def _humanize_us(v, _pos=None) -> str:
57
+ """Format a microsecond value as a self-describing tick label, picking the
58
+ unit per tick so a log axis spanning µs→s reads '500µs / 1ms / 10ms / 1s'
59
+ instead of 10^1 / 10^2. Handles negatives (tag-60 clock skew)."""
60
+ if v == 0:
61
+ return "0"
62
+ sign, a = ("-" if v < 0 else ""), abs(v)
63
+ if a < 1_000:
64
+ return f"{sign}{a:g}µs"
65
+ if a < 1_000_000:
66
+ return f"{sign}{a / 1_000:g}ms"
67
+ return f"{sign}{a / 1_000_000:g}s"
68
+
69
+
56
70
  def _read_points(csv_gz_path: str, metric: str):
57
- """Group (x_seconds_since_start, y_ms) points by kind from the gz CSV for one
58
- metric ('one_way' or 'round_trip'): y = that latency column, x = its send
59
- column rebased to seconds since the first send."""
71
+ """Group (x_seconds_since_start, y_microseconds) points by kind from the gz
72
+ CSV for one metric ('one_way' or 'round_trip'): y = that latency column (raw
73
+ µs the axis humanizes per tick), x = its send column rebased to seconds
74
+ since the first send."""
60
75
  y_col, x_col, _ = _METRIC[metric]
61
76
  by_kind: dict[str, list[tuple[float, float]]] = defaultdict(list)
62
77
  t0: Optional[float] = None
@@ -68,11 +83,11 @@ def _read_points(csv_gz_path: str, metric: str):
68
83
  continue
69
84
  try:
70
85
  x = float(xraw)
71
- y_ms = float(yraw) / 1000.0
86
+ y_us = float(yraw)
72
87
  except ValueError:
73
88
  continue
74
89
  t0 = x if t0 is None else min(t0, x)
75
- by_kind[r.get("kind") or "unknown"].append((x, y_ms))
90
+ by_kind[r.get("kind") or "unknown"].append((x, y_us))
76
91
  if t0 is not None:
77
92
  for pts in by_kind.values():
78
93
  pts[:] = [(x - t0, y) for (x, y) in pts]
@@ -98,15 +113,25 @@ def _draw(ax, by_kind, agg: str, bucket_s: float, logy: bool, ylabel: str, title
98
113
  color=color, label=None if agg == "both" else kind,
99
114
  edgecolors="none")
100
115
  if agg in ("percentile", "both"):
101
- bx, p50, p99 = _bucketed(pts, bucket_s)
116
+ bx, p50, p99, pmax = _bucketed(pts, bucket_s)
102
117
  if bx:
103
118
  ax.plot(bx, p50, color=color, lw=1.6, label=f"{kind} p50")
104
119
  ax.plot(bx, p99, color=color, lw=1.0, ls="--", alpha=0.7,
105
120
  label=f"{kind} p99")
121
+ # p100 / worst-case per bucket — the outlier envelope (faint dots)
122
+ ax.plot(bx, pmax, color=color, lw=0.8, ls=":", alpha=0.55,
123
+ label=f"{kind} max")
124
+ from matplotlib.ticker import FuncFormatter, LogLocator, NullFormatter
106
125
  ax.set_xlabel("run time (s, from first send)")
107
126
  ax.set_ylabel(ylabel + (" [log]" if logy else ""))
108
127
  if logy:
109
128
  ax.set_yscale("log")
129
+ # 1-2-5 per decade as labelled ticks (so narrow ranges still get several
130
+ # labels); finer minors as unlabelled gridlines.
131
+ ax.yaxis.set_major_locator(LogLocator(base=10, subs=(1.0, 2.0, 5.0)))
132
+ ax.yaxis.set_minor_locator(LogLocator(base=10, subs=tuple(x / 10 for x in range(10, 100))))
133
+ ax.yaxis.set_minor_formatter(NullFormatter())
134
+ ax.yaxis.set_major_formatter(FuncFormatter(_humanize_us))
110
135
  ax.grid(True, which="both", ls=":", alpha=0.4)
111
136
  ax.legend(loc="upper left", fontsize=8, ncol=2, framealpha=0.85)
112
137
  ax.set_title(title)
@@ -147,7 +172,7 @@ def render_latency_chart(csv_gz_path: str, out_path: str, *, metric: str = "one_
147
172
  base = title or "Per-message latency"
148
173
  for ax, (m, by_kind) in zip(axes, panels):
149
174
  label = one_way_label if (m == "one_way" and one_way_label) else _METRIC[m][2]
150
- _draw(ax, by_kind, agg, bucket_s, logy, f"latency (ms) — {label}",
175
+ _draw(ax, by_kind, agg, bucket_s, logy, f"latency — {label}",
151
176
  base if len(panels) == 1 else f"{base} — {label}")
152
177
  fig.tight_layout()
153
178
  fig.savefig(out_path)
@@ -155,16 +180,18 @@ def render_latency_chart(csv_gz_path: str, out_path: str, *, metric: str = "one_
155
180
 
156
181
 
157
182
  def _bucketed(sorted_pts: list[tuple[float, float]], bucket_s: float):
158
- """Per-bucket (center_x, p50, p99) over fixed-width time buckets."""
183
+ """Per-bucket (center_x, p50, p99, max) over fixed-width time buckets
184
+ (max = p100, the worst-case latency in the bucket)."""
159
185
  if bucket_s <= 0:
160
186
  bucket_s = 1.0
161
187
  buckets: dict[int, list[float]] = defaultdict(list)
162
188
  for x, y in sorted_pts:
163
189
  buckets[int(x // bucket_s)].append(y)
164
- bx, p50, p99 = [], [], []
190
+ bx, p50, p99, pmax = [], [], [], []
165
191
  for b in sorted(buckets):
166
192
  ys = sorted(buckets[b])
167
193
  bx.append((b + 0.5) * bucket_s)
168
194
  p50.append(_pctl(ys, 0.50))
169
195
  p99.append(_pctl(ys, 0.99))
170
- return bx, p50, p99
196
+ pmax.append(ys[-1])
197
+ return bx, p50, p99, pmax