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.
- {fixtureqa-0.15.0/fixtureqa.egg-info → fixtureqa-0.16.1}/PKG-INFO +3 -2
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/README.md +10 -7
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/app.py +6 -3
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/deps.py +5 -0
- fixtureqa-0.16.1/fixture/api/routers/atdl.py +150 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/sessions.py +19 -26
- fixtureqa-0.16.1/fixture/core/atdl_parser.py +371 -0
- fixtureqa-0.16.1/fixture/core/atdl_store.py +145 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_application.py +29 -14
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_engine.py +8 -2
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/scenario_runner.py +7 -4
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/session.py +16 -4
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/session_manager.py +14 -5
- fixtureqa-0.16.1/fixture/static/assets/index-Cn1j6RQg.js +103 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/index.html +1 -1
- {fixtureqa-0.15.0 → fixtureqa-0.16.1/fixtureqa.egg-info}/PKG-INFO +3 -2
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/SOURCES.txt +7 -1
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/requires.txt +2 -1
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/pyproject.toml +3 -2
- fixtureqa-0.16.1/tests/test_atdl.py +412 -0
- fixtureqa-0.16.1/tests/test_fix_application.py +62 -0
- fixtureqa-0.16.1/tests/test_send_errors.py +105 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_sessions.py +23 -0
- fixtureqa-0.15.0/fixture/static/assets/index-DQvZ5mvq.js +0 -102
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/LICENSE +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/__init__.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/__main__.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/specs.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/auth.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/events.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/models.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_charts.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/spec_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/value_gen.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/server.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/assets/index-D0a-mw9U.css +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/setup.cfg +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_auth.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_config_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_fix_spec_tree.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_health.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_inbound.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_message_store.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_msg_user_attribution.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_non_stop_session.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_api.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_engine.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_session_auto_response.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_session_visibility.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_specs.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_templates.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_timezone_data.py +0 -0
- {fixtureqa-0.15.0 → fixtureqa-0.16.1}/tests/test_value_gen.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|

|
|
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.
|
|
12
|
-
- **
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
235
|
-
|
|
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
|
-
|
|
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}
|