fixtureqa 0.14.0__tar.gz → 0.16.0__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.14.0/fixtureqa.egg-info → fixtureqa-0.16.0}/PKG-INFO +2 -1
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/app.py +4 -1
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/connection_manager.py +6 -5
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/deps.py +25 -2
- fixtureqa-0.16.0/fixture/api/routers/atdl.py +148 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/messages.py +5 -4
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/sessions.py +10 -5
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/ws.py +5 -4
- fixtureqa-0.16.0/fixture/core/atdl_parser.py +359 -0
- fixtureqa-0.16.0/fixture/core/atdl_store.py +119 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/session_manager.py +5 -4
- fixtureqa-0.16.0/fixture/static/assets/index-DCRhkrrW.js +103 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/index.html +1 -1
- {fixtureqa-0.14.0 → fixtureqa-0.16.0/fixtureqa.egg-info}/PKG-INFO +2 -1
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/SOURCES.txt +6 -1
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/requires.txt +1 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/pyproject.toml +2 -1
- fixtureqa-0.16.0/tests/test_atdl.py +350 -0
- fixtureqa-0.16.0/tests/test_session_visibility.py +84 -0
- fixtureqa-0.14.0/fixture/static/assets/index-Bh-VNEs4.js +0 -102
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/LICENSE +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/README.md +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/__main__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/specs.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/auth.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/events.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/models.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_charts.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_engine.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/session.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/spec_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/value_gen.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/server.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/assets/index-D0a-mw9U.css +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/setup.cfg +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_auth.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_config_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_fix_spec_tree.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_health.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_inbound.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_message_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_msg_user_attribution.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_non_stop_session.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_api.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_engine.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_session_auto_response.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_sessions.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_specs.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_templates.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_timezone_data.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_value_gen.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_ws.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fixtureqa
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: FIXture — FIX Protocol Testing Tool
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
License-File: LICENSE
|
|
@@ -11,6 +11,7 @@ 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"
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -263,11 +263,12 @@ class ConnectionManager:
|
|
|
263
263
|
self._prune([ws for ws, ok in zip(targets, oks) if not ok])
|
|
264
264
|
|
|
265
265
|
async def _broadcast(self, session_id: str, payload: dict, owner_uid: str) -> None:
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
266
|
+
# FIX sessions are visible to every user, so session events (status +
|
|
267
|
+
# message frames) go to all connections subscribed to this session's
|
|
268
|
+
# room or the global "*" room. `owner_uid` is retained for the call
|
|
269
|
+
# signature but no longer filters delivery.
|
|
270
|
+
targets = list({**self._clients.get(session_id, {}),
|
|
271
|
+
**self._clients.get("*", {})}.keys())
|
|
271
272
|
await self._fan_out(targets, payload)
|
|
272
273
|
|
|
273
274
|
async def _broadcast_to_user(self, uid: str, data: dict) -> None:
|
|
@@ -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")
|
|
@@ -66,8 +67,10 @@ def get_owned_session(
|
|
|
66
67
|
user: User = Depends(get_current_user),
|
|
67
68
|
):
|
|
68
69
|
"""Resolve the `session_id` path param to its SessionConfig, enforcing
|
|
69
|
-
that the caller owns it (or is admin).
|
|
70
|
-
|
|
70
|
+
that the caller owns it (or is admin). Use this for CONTROL actions
|
|
71
|
+
(start/stop, edit, delete, clear, seqnums). Viewing and sending are open to
|
|
72
|
+
every user via get_viewable_session — sessions are visible (and sendable) to
|
|
73
|
+
all."""
|
|
71
74
|
sm: SessionManager = request.app.state.session_manager
|
|
72
75
|
cfg = sm.get_config(session_id)
|
|
73
76
|
if cfg is None:
|
|
@@ -77,6 +80,22 @@ def get_owned_session(
|
|
|
77
80
|
return cfg
|
|
78
81
|
|
|
79
82
|
|
|
83
|
+
def get_viewable_session(
|
|
84
|
+
session_id: str,
|
|
85
|
+
request: HTTPConnection,
|
|
86
|
+
_user: User = Depends(get_current_user),
|
|
87
|
+
):
|
|
88
|
+
"""Resolve the `session_id` path param to its SessionConfig for READ access.
|
|
89
|
+
Every authenticated user may view any session (FIX sessions are globally
|
|
90
|
+
visible); the only gate is that the session exists. Control actions still go
|
|
91
|
+
through get_owned_session for owner/admin enforcement."""
|
|
92
|
+
sm: SessionManager = request.app.state.session_manager
|
|
93
|
+
cfg = sm.get_config(session_id)
|
|
94
|
+
if cfg is None:
|
|
95
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
96
|
+
return cfg
|
|
97
|
+
|
|
98
|
+
|
|
80
99
|
def get_housekeeping(request: HTTPConnection) -> HousekeepingService:
|
|
81
100
|
return request.app.state.housekeeping
|
|
82
101
|
|
|
@@ -109,6 +128,10 @@ def get_spec_store(request: HTTPConnection) -> SpecStore:
|
|
|
109
128
|
return request.app.state.spec_store
|
|
110
129
|
|
|
111
130
|
|
|
131
|
+
def get_atdl_store(request: HTTPConnection) -> AtdlStore:
|
|
132
|
+
return request.app.state.atdl_store
|
|
133
|
+
|
|
134
|
+
|
|
112
135
|
def get_perf_registry(request: HTTPConnection):
|
|
113
136
|
return request.app.state.perf_registry
|
|
114
137
|
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
"enums": [{"enum_id": e.enum_id, "wire_value": e.wire_value}
|
|
59
|
+
for e in p.enums],
|
|
60
|
+
}
|
|
61
|
+
for p in s.parameters
|
|
62
|
+
],
|
|
63
|
+
"edits": [{"error_message": se.error_message, "edit": _edit_dict(se.edit)}
|
|
64
|
+
for se in s.edits],
|
|
65
|
+
"state_rules": [
|
|
66
|
+
{"param": sr.param, "enabled": sr.enabled, "visible": sr.visible,
|
|
67
|
+
"value": sr.value, "edit": _edit_dict(sr.edit)}
|
|
68
|
+
for sr in s.state_rules
|
|
69
|
+
],
|
|
70
|
+
"named_edits": {k: _edit_dict(v) for k, v in s.named_edits.items()},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("")
|
|
75
|
+
def list_atdl(store: AS, user: CurrentUser) -> list[dict]:
|
|
76
|
+
"""This user's uploaded ATDL files (name + strategy count)."""
|
|
77
|
+
return store.list(user.uid)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
81
|
+
async def upload_atdl(file: UploadFile, store: AS, user: CurrentUser) -> dict:
|
|
82
|
+
raw = await file.read()
|
|
83
|
+
if len(raw) > _MAX_ATDL_BYTES:
|
|
84
|
+
raise HTTPException(status_code=413, detail="ATDL file too large")
|
|
85
|
+
try:
|
|
86
|
+
text = raw.decode("utf-8")
|
|
87
|
+
except UnicodeDecodeError:
|
|
88
|
+
raise HTTPException(status_code=400, detail="ATDL must be UTF-8 XML")
|
|
89
|
+
try:
|
|
90
|
+
return store.save(user.uid, file.filename or "algos.xml", text)
|
|
91
|
+
except AtdlValidationError as e:
|
|
92
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.post("/validate", status_code=status.HTTP_200_OK)
|
|
96
|
+
async def validate_atdl(file: UploadFile, user: CurrentUser) -> dict:
|
|
97
|
+
"""Parse + lint an ATDL file **without saving it** (library/authoring check).
|
|
98
|
+
|
|
99
|
+
Returns ``valid: true`` with the parsed strategies and any non-fatal
|
|
100
|
+
``warnings`` on success, or ``valid: false`` with the parse ``error`` (HTTP
|
|
101
|
+
200 either way — a parse failure is a validation result, not a request error).
|
|
102
|
+
"""
|
|
103
|
+
raw = await file.read()
|
|
104
|
+
if len(raw) > _MAX_ATDL_BYTES:
|
|
105
|
+
raise HTTPException(status_code=413, detail="ATDL file too large")
|
|
106
|
+
try:
|
|
107
|
+
text = raw.decode("utf-8")
|
|
108
|
+
except UnicodeDecodeError:
|
|
109
|
+
return {"valid": False, "error": "ATDL must be UTF-8 XML"}
|
|
110
|
+
try:
|
|
111
|
+
parsed = parse_atdl(text)
|
|
112
|
+
except AtdlParseError as e:
|
|
113
|
+
return {"valid": False, "error": str(e)}
|
|
114
|
+
return {
|
|
115
|
+
"valid": True,
|
|
116
|
+
"name": file.filename or "",
|
|
117
|
+
"strategy_wire_tag": parsed.strategy_wire_tag,
|
|
118
|
+
"strategy_count": len(parsed.strategies),
|
|
119
|
+
"warnings": atdl_warnings(parsed),
|
|
120
|
+
"strategies": [_strategy_dict(s) for s in parsed.strategies],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@router.get("/{name}")
|
|
125
|
+
def get_atdl(name: str, store: AS, user: CurrentUser) -> dict:
|
|
126
|
+
"""Parsed strategies/parameters (+ lint warnings) for one uploaded ATDL file."""
|
|
127
|
+
try:
|
|
128
|
+
parsed = store.get_parsed(user.uid, name)
|
|
129
|
+
except AtdlValidationError as e:
|
|
130
|
+
# Missing file or a file that no longer parses.
|
|
131
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
132
|
+
return {
|
|
133
|
+
"name": name,
|
|
134
|
+
"strategy_wire_tag": parsed.strategy_wire_tag,
|
|
135
|
+
"warnings": atdl_warnings(parsed),
|
|
136
|
+
"strategies": [_strategy_dict(s) for s in parsed.strategies],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT)
|
|
141
|
+
def delete_atdl(name: str, store: AS, user: CurrentUser) -> Response:
|
|
142
|
+
try:
|
|
143
|
+
deleted = store.delete(user.uid, name)
|
|
144
|
+
except AtdlValidationError as e:
|
|
145
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
146
|
+
if not deleted:
|
|
147
|
+
raise HTTPException(status_code=404, detail="ATDL file not found")
|
|
148
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
@@ -9,13 +9,14 @@ from ...core.models import SessionConfig
|
|
|
9
9
|
from ...core.session_manager import SessionManager
|
|
10
10
|
from ...core.user_store import User, UserStore
|
|
11
11
|
from ..schemas import MessageEntryResponse
|
|
12
|
-
from ..deps import get_session_manager, get_owned_session, get_user_store
|
|
12
|
+
from ..deps import get_session_manager, get_owned_session, get_viewable_session, get_user_store
|
|
13
13
|
|
|
14
14
|
router = APIRouter(tags=["messages"])
|
|
15
15
|
|
|
16
16
|
SM = Annotated[SessionManager, Depends(get_session_manager)]
|
|
17
17
|
US = Annotated[UserStore, Depends(get_user_store)]
|
|
18
18
|
OwnedSession = Annotated[SessionConfig, Depends(get_owned_session)]
|
|
19
|
+
ViewableSession = Annotated[SessionConfig, Depends(get_viewable_session)]
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def _username_map(entries, users: UserStore) -> dict[str, str]:
|
|
@@ -48,7 +49,7 @@ def _to_response(e, usernames: dict[str, str]) -> MessageEntryResponse:
|
|
|
48
49
|
@router.get("/{session_id}/messages", response_model=list[MessageEntryResponse])
|
|
49
50
|
def get_messages(
|
|
50
51
|
session_id: str,
|
|
51
|
-
_cfg:
|
|
52
|
+
_cfg: ViewableSession,
|
|
52
53
|
sm: SM,
|
|
53
54
|
users: US,
|
|
54
55
|
direction: Optional[str] = Query(default=None, description="IN or OUT"),
|
|
@@ -81,7 +82,7 @@ def get_messages(
|
|
|
81
82
|
@router.get("/{session_id}/messages/search", response_model=list[MessageEntryResponse])
|
|
82
83
|
def search_messages(
|
|
83
84
|
session_id: str,
|
|
84
|
-
_cfg:
|
|
85
|
+
_cfg: ViewableSession,
|
|
85
86
|
sm: SM,
|
|
86
87
|
users: US,
|
|
87
88
|
q: str = Query(..., min_length=1, description="substring or regex query over raw FIX"),
|
|
@@ -106,7 +107,7 @@ def search_messages(
|
|
|
106
107
|
@router.get("/{session_id}/messages/export")
|
|
107
108
|
def export_messages(
|
|
108
109
|
session_id: str,
|
|
109
|
-
_cfg:
|
|
110
|
+
_cfg: ViewableSession,
|
|
110
111
|
sm: SM,
|
|
111
112
|
users: US,
|
|
112
113
|
fmt: str = Query(default="csv", description="Export format: csv or fix"),
|
|
@@ -7,7 +7,7 @@ from ...core.models import SessionConfig, ConnectionType, SessionRole
|
|
|
7
7
|
from ...core.user_store import User, UserStore
|
|
8
8
|
from ...core.spec_store import SpecStore
|
|
9
9
|
from ..schemas import SessionConfigRequest, SessionResponse, UpdateSessionRequest, SendMessageRequest, SeqNumRequest, SeqNumResponse
|
|
10
|
-
from ..deps import get_session_manager, get_current_user, get_owned_session, get_user_store, get_spec_store
|
|
10
|
+
from ..deps import get_session_manager, get_current_user, get_owned_session, get_viewable_session, get_user_store, get_spec_store
|
|
11
11
|
|
|
12
12
|
router = APIRouter(tags=["sessions"])
|
|
13
13
|
|
|
@@ -16,6 +16,7 @@ US = Annotated[UserStore, Depends(get_user_store)]
|
|
|
16
16
|
SS = Annotated[SpecStore, Depends(get_spec_store)]
|
|
17
17
|
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
18
18
|
OwnedSession = Annotated[SessionConfig, Depends(get_owned_session)]
|
|
19
|
+
ViewableSession = Annotated[SessionConfig, Depends(get_viewable_session)]
|
|
19
20
|
|
|
20
21
|
def _bundled_specs() -> set[str]:
|
|
21
22
|
"""Bundled spec names fixcore resolves by itself — transport dictionaries
|
|
@@ -126,8 +127,10 @@ def _session_response(sm: SessionManager, session_id: str, us: Optional[UserStor
|
|
|
126
127
|
|
|
127
128
|
|
|
128
129
|
def _owner_store(us: UserStore, user: User) -> Optional[UserStore]:
|
|
129
|
-
"""
|
|
130
|
-
|
|
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
|
|
131
134
|
|
|
132
135
|
|
|
133
136
|
@router.get("/", response_model=list[SessionResponse])
|
|
@@ -190,7 +193,7 @@ def import_sessions(body: list[SessionConfigRequest], sm: SM, us: US, ss: SS, us
|
|
|
190
193
|
|
|
191
194
|
|
|
192
195
|
@router.get("/{session_id}", response_model=SessionResponse)
|
|
193
|
-
def get_session(session_id: str, _cfg:
|
|
196
|
+
def get_session(session_id: str, _cfg: ViewableSession, sm: SM, us: US, user: CurrentUser):
|
|
194
197
|
return _session_response(sm, session_id, _owner_store(us, user))
|
|
195
198
|
|
|
196
199
|
|
|
@@ -240,8 +243,10 @@ async def stop_session(
|
|
|
240
243
|
|
|
241
244
|
|
|
242
245
|
@router.post("/{session_id}/send")
|
|
243
|
-
async def send_message(session_id: str, body: SendMessageRequest, _cfg:
|
|
246
|
+
async def send_message(session_id: str, body: SendMessageRequest, _cfg: ViewableSession,
|
|
244
247
|
sm: SM, user: CurrentUser):
|
|
248
|
+
# Sending is open to any user who can view the session (not just the owner);
|
|
249
|
+
# each message is still attributed to the actual sender via user_id below.
|
|
245
250
|
from ...core.fix_builder import build_message_from_raw
|
|
246
251
|
from ...core.fix_time import utc_timestamp
|
|
247
252
|
|
|
@@ -136,11 +136,12 @@ async def ws_single_session(
|
|
|
136
136
|
await websocket.close(code=4003, reason="Unauthorized")
|
|
137
137
|
return
|
|
138
138
|
|
|
139
|
-
#
|
|
139
|
+
# Sessions are visible to every authenticated user (read-only stream);
|
|
140
|
+
# the only requirement is that the session exists.
|
|
140
141
|
cfg = sm.get_config(session_id)
|
|
141
|
-
if cfg is None
|
|
142
|
-
logger.warning("WS /ws/%s:
|
|
143
|
-
await websocket.close(code=
|
|
142
|
+
if cfg is None:
|
|
143
|
+
logger.warning("WS /ws/%s: unknown session for uid=%s", session_id, uid)
|
|
144
|
+
await websocket.close(code=4004, reason="Session not found")
|
|
144
145
|
return
|
|
145
146
|
|
|
146
147
|
logger.info("WS /ws/%s: client connected uid=%s is_admin=%s", session_id, uid, is_admin)
|