fixtureqa 0.14.0__tar.gz → 0.15.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.15.0}/PKG-INFO +1 -1
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/connection_manager.py +6 -5
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/deps.py +20 -2
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/messages.py +5 -4
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/sessions.py +10 -5
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/ws.py +5 -4
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/session_manager.py +5 -4
- fixtureqa-0.14.0/fixture/static/assets/index-Bh-VNEs4.js → fixtureqa-0.15.0/fixture/static/assets/index-DQvZ5mvq.js +23 -23
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/static/index.html +1 -1
- {fixtureqa-0.14.0 → fixtureqa-0.15.0/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/SOURCES.txt +2 -1
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/pyproject.toml +1 -1
- fixtureqa-0.15.0/tests/test_session_visibility.py +84 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/LICENSE +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/README.md +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/__main__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/app.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/specs.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/auth.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/events.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/models.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/perf_charts.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/perf_engine.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/session.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/spec_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/value_gen.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/server.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/static/assets/index-D0a-mw9U.css +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/setup.cfg +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_auth.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_config_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_fix_spec_tree.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_health.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_inbound.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_message_store.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_msg_user_attribution.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_non_stop_session.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_perf_api.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_perf_engine.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_session_auto_response.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_sessions.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_specs.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_templates.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_timezone_data.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_value_gen.py +0 -0
- {fixtureqa-0.14.0 → fixtureqa-0.15.0}/tests/test_ws.py +0 -0
|
@@ -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:
|
|
@@ -66,8 +66,10 @@ def get_owned_session(
|
|
|
66
66
|
user: User = Depends(get_current_user),
|
|
67
67
|
):
|
|
68
68
|
"""Resolve the `session_id` path param to its SessionConfig, enforcing
|
|
69
|
-
that the caller owns it (or is admin).
|
|
70
|
-
|
|
69
|
+
that the caller owns it (or is admin). Use this for CONTROL actions
|
|
70
|
+
(start/stop, edit, delete, clear, seqnums). Viewing and sending are open to
|
|
71
|
+
every user via get_viewable_session — sessions are visible (and sendable) to
|
|
72
|
+
all."""
|
|
71
73
|
sm: SessionManager = request.app.state.session_manager
|
|
72
74
|
cfg = sm.get_config(session_id)
|
|
73
75
|
if cfg is None:
|
|
@@ -77,6 +79,22 @@ def get_owned_session(
|
|
|
77
79
|
return cfg
|
|
78
80
|
|
|
79
81
|
|
|
82
|
+
def get_viewable_session(
|
|
83
|
+
session_id: str,
|
|
84
|
+
request: HTTPConnection,
|
|
85
|
+
_user: User = Depends(get_current_user),
|
|
86
|
+
):
|
|
87
|
+
"""Resolve the `session_id` path param to its SessionConfig for READ access.
|
|
88
|
+
Every authenticated user may view any session (FIX sessions are globally
|
|
89
|
+
visible); the only gate is that the session exists. Control actions still go
|
|
90
|
+
through get_owned_session for owner/admin enforcement."""
|
|
91
|
+
sm: SessionManager = request.app.state.session_manager
|
|
92
|
+
cfg = sm.get_config(session_id)
|
|
93
|
+
if cfg is None:
|
|
94
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
95
|
+
return cfg
|
|
96
|
+
|
|
97
|
+
|
|
80
98
|
def get_housekeeping(request: HTTPConnection) -> HousekeepingService:
|
|
81
99
|
return request.app.state.housekeeping
|
|
82
100
|
|
|
@@ -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)
|
|
@@ -150,11 +150,12 @@ class SessionManager:
|
|
|
150
150
|
return [s.config for s in self._sessions.values()]
|
|
151
151
|
|
|
152
152
|
def list_sessions_for_user(self, uid: str, is_admin: bool = False) -> List[SessionConfig]:
|
|
153
|
+
"""All FIX sessions are visible to every authenticated user (view-only).
|
|
154
|
+
Ownership no longer filters the list — it still gates control actions
|
|
155
|
+
(start/stop/send/edit/delete) at the router layer via get_owned_session.
|
|
156
|
+
`uid`/`is_admin` are kept for call-site compatibility but unused here."""
|
|
153
157
|
with self._lock:
|
|
154
|
-
|
|
155
|
-
if is_admin:
|
|
156
|
-
return configs
|
|
157
|
-
return [c for c in configs if c.owner_uid == uid]
|
|
158
|
+
return [s.config for s in self._sessions.values()]
|
|
158
159
|
|
|
159
160
|
def is_running(self, session_id: str) -> bool:
|
|
160
161
|
state = self.get_state(session_id)
|