fixtureqa 0.15.0__tar.gz → 0.16.1__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 (116) hide show
  1. {fixtureqa-0.15.0/fixtureqa.egg-info → fixtureqa-0.16.1}/PKG-INFO +3 -2
  2. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/README.md +10 -7
  3. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/app.py +6 -3
  4. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/deps.py +5 -0
  5. fixtureqa-0.16.1/fixture/api/routers/atdl.py +150 -0
  6. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/sessions.py +19 -26
  7. fixtureqa-0.16.1/fixture/core/atdl_parser.py +371 -0
  8. fixtureqa-0.16.1/fixture/core/atdl_store.py +145 -0
  9. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_application.py +29 -14
  10. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_engine.py +8 -2
  11. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/scenario_runner.py +7 -4
  12. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/session.py +16 -4
  13. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/session_manager.py +14 -5
  14. fixtureqa-0.16.1/fixture/static/assets/index-Cn1j6RQg.js +103 -0
  15. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/index.html +1 -1
  16. {fixtureqa-0.15.0 → fixtureqa-0.16.1/fixtureqa.egg-info}/PKG-INFO +3 -2
  17. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/SOURCES.txt +7 -1
  18. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/requires.txt +2 -1
  19. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/pyproject.toml +3 -2
  20. fixtureqa-0.16.1/tests/test_atdl.py +412 -0
  21. fixtureqa-0.16.1/tests/test_fix_application.py +62 -0
  22. fixtureqa-0.16.1/tests/test_send_errors.py +105 -0
  23. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_sessions.py +23 -0
  24. fixtureqa-0.15.0/fixture/static/assets/index-DQvZ5mvq.js +0 -102
  25. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/LICENSE +0 -0
  26. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/__init__.py +0 -0
  27. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/__main__.py +0 -0
  28. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/__init__.py +0 -0
  29. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/connection_manager.py +0 -0
  30. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/__init__.py +0 -0
  31. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/admin.py +0 -0
  32. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/auth.py +0 -0
  33. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/branding.py +0 -0
  34. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/custom_tags.py +0 -0
  35. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/fix_spec.py +0 -0
  36. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/messages.py +0 -0
  37. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/perf.py +0 -0
  38. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/scenarios.py +0 -0
  39. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/setup.py +0 -0
  40. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/spec_overlay.py +0 -0
  41. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/specs.py +0 -0
  42. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/templates.py +0 -0
  43. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/ws.py +0 -0
  44. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/schemas.py +0 -0
  45. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/config/__init__.py +0 -0
  46. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/__init__.py +0 -0
  47. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/atomic_io.py +0 -0
  48. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/auth.py +0 -0
  49. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/config_store.py +0 -0
  50. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/custom_tag_store.py +0 -0
  51. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/db_migrations.py +0 -0
  52. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/events.py +0 -0
  53. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_builder.py +0 -0
  54. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_parser.py +0 -0
  55. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_spec_parser.py +0 -0
  56. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_tags.py +0 -0
  57. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_time.py +0 -0
  58. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/housekeeping.py +0 -0
  59. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/inbound.py +0 -0
  60. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/json_store.py +0 -0
  61. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/latency_csv_writer.py +0 -0
  62. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/message_log.py +0 -0
  63. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/message_store.py +0 -0
  64. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/models.py +0 -0
  65. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_charts.py +0 -0
  66. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_models.py +0 -0
  67. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_payload.py +0 -0
  68. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_stats.py +0 -0
  69. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_store.py +0 -0
  70. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_writer.py +0 -0
  71. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/scenario_store.py +0 -0
  72. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/spec_overlay_store.py +0 -0
  73. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/spec_store.py +0 -0
  74. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/template_store.py +0 -0
  75. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/user_store.py +0 -0
  76. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/value_gen.py +0 -0
  77. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/venue_responses.py +0 -0
  78. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/server.py +0 -0
  79. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  80. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  81. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/assets/index-D0a-mw9U.css +0 -0
  82. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  83. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/favicon.svg +0 -0
  84. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/ui/__init__.py +0 -0
  85. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/dependency_links.txt +0 -0
  86. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/entry_points.txt +0 -0
  87. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/top_level.txt +0 -0
  88. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/setup.cfg +0 -0
  89. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_atomic_io.py +0 -0
  90. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_auth.py +0 -0
  91. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_config_store.py +0 -0
  92. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_connection_manager.py +0 -0
  93. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_db_migrations.py +0 -0
  94. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_fix_builder.py +0 -0
  95. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_fix_spec_tree.py +0 -0
  96. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_health.py +0 -0
  97. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_inbound.py +0 -0
  98. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_inbound_validation.py +0 -0
  99. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_message_store.py +0 -0
  100. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_msg_user_attribution.py +0 -0
  101. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_non_stop_session.py +0 -0
  102. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_api.py +0 -0
  103. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_engine.py +0 -0
  104. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_models.py +0 -0
  105. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_payload.py +0 -0
  106. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_rehydrate.py +0 -0
  107. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_scenarios.py +0 -0
  108. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_session_auto_response.py +0 -0
  109. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_session_lifecycle.py +0 -0
  110. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_session_manager_concurrency.py +0 -0
  111. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_session_visibility.py +0 -0
  112. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_specs.py +0 -0
  113. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_templates.py +0 -0
  114. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_timezone_data.py +0 -0
  115. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_value_gen.py +0 -0
  116. {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_ws.py +0 -0
@@ -1,16 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.15.0
3
+ Version: 0.16.1
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
7
- Requires-Dist: fixcore-engine==0.11.0
7
+ Requires-Dist: fixcore-engine==0.11.1
8
8
  Requires-Dist: fastapi>=0.111.0
9
9
  Requires-Dist: uvicorn[standard]>=0.29.0
10
10
  Requires-Dist: websockets>=12.0
11
11
  Requires-Dist: python-jose[cryptography]>=3.3.0
12
12
  Requires-Dist: passlib[bcrypt]>=1.7.4
13
13
  Requires-Dist: python-multipart>=0.0.9
14
+ Requires-Dist: defusedxml>=0.7.1
14
15
  Requires-Dist: matplotlib>=3.7
15
16
  Provides-Extra: dev
16
17
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  # FIXture
2
2
 
3
- A Python FIX protocol testing tool. Supports FIX 4.2 and 4.4, dynamic session creation at runtime (no restart required), and both initiator and acceptor roles per session.
3
+ A Python FIX protocol testing tool. Supports FIX 4.0–4.4 and FIX 5.0 (over FIXT.1.1), dynamic session creation at runtime (no restart required), and both initiator and acceptor roles per session.
4
4
 
5
5
  ![FIXture](frontend/src/assets/logo-dark.svg)
6
6
 
@@ -8,16 +8,19 @@ A Python FIX protocol testing tool. Supports FIX 4.2 and 4.4, dynamic session cr
8
8
 
9
9
  ## Features
10
10
 
11
- - **FIX 4.2 and 4.4** support with bundled data dictionaries
12
- - **Initiator and acceptor** roles, configurable per session
11
+ - **FIX 4.0–4.4 and FIX 5.0** every classic version plus FIX 5.0/SP1/SP2 over FIXT.1.1 (transport + application dictionary, ApplVerID on the Logon), with bundled data dictionaries
12
+ - **Custom data dictionaries** upload per-user spec XMLs and pick them per session; built-in spec browser for every bundled/uploaded dictionary (messages, fields, enums, repeating groups)
13
+ - **Initiator and acceptor** roles, configurable per session, with the full engine config surface (schedules, timers, resend/validation behaviour) in the session form
13
14
  - **Dynamic sessions** — add or remove sessions without restarting
14
- - **Multi-user auth** — JWT login, per-user session isolation, admin panel
15
- - **Message log** — real-time IN/OUT stream with field breakdown, filtering, cursor pagination
16
- - **Compose & send** — paste raw FIX or use message templates; auto-inject tag 60 and primary ID
15
+ - **Multi-user auth** — JWT login and admin panel; sessions are visible and sendable to every user, control (start/stop/edit/delete) stays with the owner or an admin, and each sent message is attributed to the user who sent it
16
+ - **Message log** — real-time IN/OUT stream with field breakdown, filtering, cursor pagination, and full-history search (substring or regex)
17
+ - **Compose & send** — paste raw FIX or use message templates; one message per line sends a batch in order; auto-inject tag 60 and primary ID; value generators (`uuid()`, `ORD-{seq(8)}`, …)
18
+ - **Algo orders (FIXatdl)** — upload a broker's ATDL file and build orders from a generated form: typed parameters, conditional enable/hide and cross-field validation (StateRules/StrategyEdits), the strategy identifier stamped automatically; validate-without-saving for authoring your own files
17
19
  - **Templates** — create/edit reusable message templates with FIX spec field definitions
20
+ - **Custom tags** — per-user tag definitions (name, type, enum labels) that decode in the message log; ATDL parameters can register themselves automatically
18
21
  - **Scenarios** — automated send/expect/delay test scripts with live pass/fail results; repeatable steps/scenarios, value generators (`uuid()`, `ORD-{seq(8)}`, …) in send values, `{recv:<tag>}` to echo an order's tags into the ack/fill that answers it, and cross-session **suites** that coordinate through real FIX traffic (e.g. a client scenario and a venue scenario run together)
19
22
  - **Venue simulation** — auto-ack and auto-fill responses for inbound NewOrderSingles
20
- - **Performance testing** — rate-controlled order injection with latency correlation (p50/p95/p99), live dashboard, run history and CSV export; order/exec shapes templatable per counterparty; amend (35=G) and cancel (35=F) flavours with their own reply latencies (see [PERF.md](PERF.md))
23
+ - **Performance testing** — rate-controlled order injection with latency correlation (p50/p95/p99), live dashboard, run history and CSV export; order/exec shapes templatable per counterparty; amend (35=G) and cancel (35=F) flavours with their own reply latencies; a 4-session relay topology (`client → relay → venue`) that measures per-hop pass-through latency (see [PERF.md](PERF.md))
21
24
  - **Log analysis** — Overview stats, Throughput chart, Reject detail, Latency measurement (p50/p95/p99)
22
25
  - **Export** — download session messages as CSV or raw FIX
23
26
  - **Sequence numbers** — view and reset TX/RX seqnums per session
@@ -13,13 +13,14 @@ from ..core.scenario_store import ScenarioStore, ScenarioGroupStore
13
13
  from ..core.scenario_runner import ScenarioRunner
14
14
  from ..core.spec_overlay_store import SpecOverlayStore
15
15
  from ..core.spec_store import SpecStore
16
+ from ..core.atdl_store import AtdlStore
16
17
  from ..core.template_store import TemplateStore
17
18
  from ..core.user_store import UserStore
18
19
  from .connection_manager import ConnectionManager
19
20
  from ..core.perf_engine import RunRegistry
20
21
  from ..core.perf_writer import PerfWriter
21
22
  from ..core.perf_store import PerfStore
22
- from .routers import sessions, messages, ws, auth, setup, admin, fix_spec, templates, scenarios, branding, custom_tags, spec_overlay, specs, perf
23
+ from .routers import sessions, messages, ws, auth, setup, admin, fix_spec, templates, scenarios, branding, custom_tags, spec_overlay, specs, atdl, perf
23
24
 
24
25
 
25
26
  @asynccontextmanager
@@ -53,10 +54,10 @@ async def lifespan(app: FastAPI):
53
54
  await app.state.perf_registry.shutdown()
54
55
  except Exception:
55
56
  pass
56
- # Force-stop all running sessions so their asyncio tasks can be cancelled cleanly
57
+ # Stop all running sessions so their asyncio tasks can be cancelled cleanly
57
58
  for sid in list(sm._sessions.keys()):
58
59
  try:
59
- await sm.stop_session(sid, force=True)
60
+ await sm.stop_session(sid)
60
61
  except Exception:
61
62
  pass
62
63
  sm.close() # flush SQLite writer
@@ -96,6 +97,7 @@ def create_app(
96
97
  app.state.custom_tag_store = CustomTagStore(data_dir)
97
98
  app.state.spec_overlay_store = SpecOverlayStore(data_dir)
98
99
  app.state.spec_store = SpecStore(data_dir)
100
+ app.state.atdl_store = AtdlStore(data_dir)
99
101
  perf_writer = PerfWriter(os.path.join(data_dir, "perf.db"))
100
102
  app.state.perf_writer = perf_writer
101
103
  app.state.perf_registry = RunRegistry(
@@ -122,6 +124,7 @@ def create_app(
122
124
  app.include_router(custom_tags.router, prefix="/api/custom-tags")
123
125
  app.include_router(spec_overlay.router, prefix="/api/spec-overlay")
124
126
  app.include_router(specs.router, prefix="/api/specs")
127
+ app.include_router(atdl.router, prefix="/api/atdl")
125
128
  app.include_router(perf.router, prefix="/api/perf")
126
129
  app.include_router(ws.router)
127
130
 
@@ -13,6 +13,7 @@ from ..core.scenario_runner import ScenarioRunner
13
13
  from ..core.custom_tag_store import CustomTagStore
14
14
  from ..core.spec_overlay_store import SpecOverlayStore
15
15
  from ..core.spec_store import SpecStore
16
+ from ..core.atdl_store import AtdlStore
16
17
  from .connection_manager import ConnectionManager
17
18
 
18
19
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
@@ -127,6 +128,10 @@ def get_spec_store(request: HTTPConnection) -> SpecStore:
127
128
  return request.app.state.spec_store
128
129
 
129
130
 
131
+ def get_atdl_store(request: HTTPConnection) -> AtdlStore:
132
+ return request.app.state.atdl_store
133
+
134
+
130
135
  def get_perf_registry(request: HTTPConnection):
131
136
  return request.app.state.perf_registry
132
137
 
@@ -0,0 +1,150 @@
1
+ """ATDL (Algorithmic Trading Definition Language) upload/list/delete + browse.
2
+
3
+ A user uploads a broker's FIXatdl XML; this router persists it per-user and
4
+ exposes the parsed strategies/parameters so the compose UI can render an algo
5
+ order-entry form. Mirrors the custom-spec router (:mod:`.specs`).
6
+ """
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import APIRouter, Depends, HTTPException, Response, UploadFile, status
11
+
12
+ from ...core.atdl_parser import AtdlParseError, atdl_warnings, parse_atdl
13
+ from ...core.atdl_store import AtdlStore, AtdlValidationError
14
+ from ...core.user_store import User
15
+ from ..deps import get_atdl_store, get_current_user
16
+
17
+ router = APIRouter(tags=["atdl"])
18
+
19
+ AS = Annotated[AtdlStore, Depends(get_atdl_store)]
20
+ CurrentUser = Annotated[User, Depends(get_current_user)]
21
+
22
+ # ATDL files are XML text; a large broker suite is well under 2 MB.
23
+ _MAX_ATDL_BYTES = 2 * 1024 * 1024
24
+
25
+
26
+ def _edit_dict(e) -> dict:
27
+ """Serialise an Edit expression tree (see core.atdl_parser.Edit)."""
28
+ return {
29
+ "operator": e.operator,
30
+ "field": e.field,
31
+ "field2": e.field2,
32
+ "value": e.value,
33
+ "has_value": e.has_value,
34
+ "logic": e.logic,
35
+ "ref": e.ref,
36
+ "id": e.id,
37
+ "children": [_edit_dict(c) for c in e.children],
38
+ }
39
+
40
+
41
+ def _strategy_dict(s) -> dict:
42
+ return {
43
+ "name": s.name,
44
+ "ui_rep": s.ui_rep,
45
+ "wire_value": s.wire_value,
46
+ "fix_msg_type": s.fix_msg_type,
47
+ "has_layout": s.has_layout,
48
+ # Retained for the UI's "advanced features" hint; now derived from the
49
+ # rules we actually parse (Phase 3) rather than mere presence.
50
+ "has_edits": bool(s.edits or s.state_rules),
51
+ "parameters": [
52
+ {
53
+ "name": p.name,
54
+ "fix_tag": p.fix_tag,
55
+ "type": p.type,
56
+ "kind": p.kind,
57
+ "required": p.required,
58
+ "true_wire_value": p.true_wire_value,
59
+ "false_wire_value": p.false_wire_value,
60
+ "enums": [{"enum_id": e.enum_id, "wire_value": e.wire_value}
61
+ for e in p.enums],
62
+ }
63
+ for p in s.parameters
64
+ ],
65
+ "edits": [{"error_message": se.error_message, "edit": _edit_dict(se.edit)}
66
+ for se in s.edits],
67
+ "state_rules": [
68
+ {"param": sr.param, "enabled": sr.enabled, "visible": sr.visible,
69
+ "value": sr.value, "edit": _edit_dict(sr.edit)}
70
+ for sr in s.state_rules
71
+ ],
72
+ "named_edits": {k: _edit_dict(v) for k, v in s.named_edits.items()},
73
+ }
74
+
75
+
76
+ @router.get("")
77
+ def list_atdl(store: AS, user: CurrentUser) -> list[dict]:
78
+ """This user's uploaded ATDL files (name + strategy count)."""
79
+ return store.list(user.uid)
80
+
81
+
82
+ @router.post("", status_code=status.HTTP_201_CREATED)
83
+ async def upload_atdl(file: UploadFile, store: AS, user: CurrentUser) -> dict:
84
+ raw = await file.read()
85
+ if len(raw) > _MAX_ATDL_BYTES:
86
+ raise HTTPException(status_code=413, detail="ATDL file too large")
87
+ try:
88
+ text = raw.decode("utf-8")
89
+ except UnicodeDecodeError:
90
+ raise HTTPException(status_code=400, detail="ATDL must be UTF-8 XML")
91
+ try:
92
+ return store.save(user.uid, file.filename or "algos.xml", text)
93
+ except AtdlValidationError as e:
94
+ raise HTTPException(status_code=400, detail=str(e))
95
+
96
+
97
+ @router.post("/validate", status_code=status.HTTP_200_OK)
98
+ async def validate_atdl(file: UploadFile, user: CurrentUser) -> dict:
99
+ """Parse + lint an ATDL file **without saving it** (library/authoring check).
100
+
101
+ Returns ``valid: true`` with the parsed strategies and any non-fatal
102
+ ``warnings`` on success, or ``valid: false`` with the parse ``error`` (HTTP
103
+ 200 either way — a parse failure is a validation result, not a request error).
104
+ """
105
+ raw = await file.read()
106
+ if len(raw) > _MAX_ATDL_BYTES:
107
+ raise HTTPException(status_code=413, detail="ATDL file too large")
108
+ try:
109
+ text = raw.decode("utf-8")
110
+ except UnicodeDecodeError:
111
+ return {"valid": False, "error": "ATDL must be UTF-8 XML"}
112
+ try:
113
+ parsed = parse_atdl(text)
114
+ except AtdlParseError as e:
115
+ return {"valid": False, "error": str(e)}
116
+ return {
117
+ "valid": True,
118
+ "name": file.filename or "",
119
+ "strategy_wire_tag": parsed.strategy_wire_tag,
120
+ "strategy_count": len(parsed.strategies),
121
+ "warnings": atdl_warnings(parsed),
122
+ "strategies": [_strategy_dict(s) for s in parsed.strategies],
123
+ }
124
+
125
+
126
+ @router.get("/{name}")
127
+ def get_atdl(name: str, store: AS, user: CurrentUser) -> dict:
128
+ """Parsed strategies/parameters (+ lint warnings) for one uploaded ATDL file."""
129
+ try:
130
+ parsed = store.get_parsed(user.uid, name)
131
+ except AtdlValidationError as e:
132
+ # Missing file or a file that no longer parses.
133
+ raise HTTPException(status_code=404, detail=str(e))
134
+ return {
135
+ "name": name,
136
+ "strategy_wire_tag": parsed.strategy_wire_tag,
137
+ "warnings": atdl_warnings(parsed),
138
+ "strategies": [_strategy_dict(s) for s in parsed.strategies],
139
+ }
140
+
141
+
142
+ @router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT)
143
+ def delete_atdl(name: str, store: AS, user: CurrentUser) -> Response:
144
+ try:
145
+ deleted = store.delete(user.uid, name)
146
+ except AtdlValidationError as e:
147
+ raise HTTPException(status_code=400, detail=str(e))
148
+ if not deleted:
149
+ raise HTTPException(status_code=404, detail="ATDL file not found")
150
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -1,5 +1,5 @@
1
1
  from typing import Annotated, Optional
2
- from fastapi import APIRouter, Depends, HTTPException, Query
2
+ from fastapi import APIRouter, Depends, HTTPException
3
3
 
4
4
  from ...core.message_store import MessageStore
5
5
  from ...core.session_manager import SessionManager
@@ -56,6 +56,8 @@ def _config_from_request(body: SessionConfigRequest, owner_uid: str) -> SessionC
56
56
 
57
57
  def _session_response(sm: SessionManager, session_id: str, us: Optional[UserStore] = None,
58
58
  counts: Optional[dict] = None) -> SessionResponse:
59
+ """Sessions are visible to all users, so every response carries
60
+ owner_username (resolved via `us`) for the UI owner badge / "Mine" filter."""
59
61
  cfg = sm.get_config(session_id)
60
62
  if cfg is None:
61
63
  raise HTTPException(status_code=404, detail="Session not found")
@@ -126,19 +128,11 @@ def _session_response(sm: SessionManager, session_id: str, us: Optional[UserStor
126
128
  )
127
129
 
128
130
 
129
- def _owner_store(us: UserStore, user: User) -> Optional[UserStore]:
130
- """Every response carries owner_username: sessions are visible to all users,
131
- so the UI owner badge / "Mine" filter needs the owner on every session,
132
- not just for admins."""
133
- return us
134
-
135
-
136
131
  @router.get("/", response_model=list[SessionResponse])
137
132
  def list_sessions(sm: SM, us: US, user: CurrentUser):
138
133
  configs = sm.list_sessions_for_user(user.uid, is_admin=user.is_admin)
139
- user_store = _owner_store(us, user)
140
134
  counts = sm.message_counts()
141
- return [_session_response(sm, cfg.session_id, user_store, counts) for cfg in configs]
135
+ return [_session_response(sm, cfg.session_id, us, counts) for cfg in configs]
142
136
 
143
137
 
144
138
  @router.post("/", response_model=SessionResponse, status_code=201)
@@ -168,7 +162,7 @@ async def create_session(body: SessionConfigRequest, sm: SM, us: US, ss: SS, use
168
162
  pass # best-effort rollback; surface the original start failure
169
163
  raise HTTPException(status_code=500, detail=f"Failed to start session: {e}")
170
164
 
171
- return _session_response(sm, session_id, _owner_store(us, user))
165
+ return _session_response(sm, session_id, us)
172
166
 
173
167
 
174
168
  @router.post("/import", response_model=list[SessionResponse], status_code=201)
@@ -188,13 +182,13 @@ def import_sessions(body: list[SessionConfigRequest], sm: SM, us: US, ss: SS, us
188
182
  session_id = sm.add_session(cfg)
189
183
  except (HTTPException, ValueError, KeyError):
190
184
  continue # invalid / conflicting entry — skip just this one
191
- created.append(_session_response(sm, session_id, _owner_store(us, user)))
185
+ created.append(_session_response(sm, session_id, us))
192
186
  return created
193
187
 
194
188
 
195
189
  @router.get("/{session_id}", response_model=SessionResponse)
196
190
  def get_session(session_id: str, _cfg: ViewableSession, sm: SM, us: US, user: CurrentUser):
197
- return _session_response(sm, session_id, _owner_store(us, user))
191
+ return _session_response(sm, session_id, us)
198
192
 
199
193
 
200
194
  @router.patch("/{session_id}", response_model=SessionResponse)
@@ -209,7 +203,7 @@ def update_session(session_id: str, body: UpdateSessionRequest, _cfg: OwnedSessi
209
203
  raise HTTPException(status_code=409, detail=str(e))
210
204
  except (ValueError, KeyError) as e:
211
205
  raise HTTPException(status_code=422, detail=str(e))
212
- return _session_response(sm, session_id, _owner_store(us, user))
206
+ return _session_response(sm, session_id, us)
213
207
 
214
208
 
215
209
  @router.delete("/{session_id}", status_code=204)
@@ -226,20 +220,13 @@ async def start_session(session_id: str, _cfg: OwnedSession, sm: SM, us: US, use
226
220
  await sm.start_session(session_id)
227
221
  except RuntimeError as e:
228
222
  raise HTTPException(status_code=409, detail=str(e))
229
- return _session_response(sm, session_id, _owner_store(us, user))
223
+ return _session_response(sm, session_id, us)
230
224
 
231
225
 
232
226
  @router.post("/{session_id}/stop", response_model=SessionResponse)
233
- async def stop_session(
234
- session_id: str,
235
- _cfg: OwnedSession,
236
- sm: SM,
237
- us: US,
238
- user: CurrentUser,
239
- force: bool = Query(default=False),
240
- ):
241
- await sm.stop_session(session_id, force=force)
242
- return _session_response(sm, session_id, _owner_store(us, user))
227
+ async def stop_session(session_id: str, _cfg: OwnedSession, sm: SM, us: US, user: CurrentUser):
228
+ await sm.stop_session(session_id)
229
+ return _session_response(sm, session_id, us)
243
230
 
244
231
 
245
232
  @router.post("/{session_id}/send")
@@ -258,7 +245,13 @@ async def send_message(session_id: str, body: SendMessageRequest, _cfg: Viewable
258
245
  except ValueError as e:
259
246
  raise HTTPException(status_code=422, detail=str(e))
260
247
 
261
- ok = await sm.send_message(session_id, msg, user_id=user.uid)
248
+ from ...core.session_manager import SendError
249
+ try:
250
+ ok = await sm.send_message(session_id, msg, user_id=user.uid)
251
+ except SendError as e:
252
+ # A real send failure on a logged-on session — report the engine's
253
+ # error instead of the misleading "not logged on".
254
+ raise HTTPException(status_code=500, detail=f"Send failed: {e}")
262
255
  if not ok:
263
256
  raise HTTPException(status_code=409, detail="Session not logged on")
264
257
  return {"ok": True}