fixtureqa 0.13.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.
Files changed (110) hide show
  1. {fixtureqa-0.13.0/fixtureqa.egg-info → fixtureqa-0.15.0}/PKG-INFO +1 -1
  2. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/connection_manager.py +6 -5
  3. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/deps.py +20 -2
  4. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/messages.py +5 -4
  5. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/sessions.py +10 -5
  6. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/ws.py +5 -4
  7. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/session_manager.py +5 -4
  8. fixtureqa-0.15.0/fixture/static/assets/index-DQvZ5mvq.js +102 -0
  9. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/static/index.html +1 -1
  10. {fixtureqa-0.13.0 → fixtureqa-0.15.0/fixtureqa.egg-info}/PKG-INFO +1 -1
  11. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/SOURCES.txt +2 -1
  12. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/pyproject.toml +1 -1
  13. fixtureqa-0.15.0/tests/test_session_visibility.py +84 -0
  14. fixtureqa-0.13.0/fixture/static/assets/index--0vO6mor.js +0 -102
  15. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/LICENSE +0 -0
  16. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/README.md +0 -0
  17. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/__init__.py +0 -0
  18. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/__main__.py +0 -0
  19. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/__init__.py +0 -0
  20. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/app.py +0 -0
  21. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/__init__.py +0 -0
  22. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/admin.py +0 -0
  23. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/auth.py +0 -0
  24. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/branding.py +0 -0
  25. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/custom_tags.py +0 -0
  26. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/fix_spec.py +0 -0
  27. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/perf.py +0 -0
  28. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/scenarios.py +0 -0
  29. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/setup.py +0 -0
  30. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/spec_overlay.py +0 -0
  31. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/specs.py +0 -0
  32. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/routers/templates.py +0 -0
  33. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/api/schemas.py +0 -0
  34. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/config/__init__.py +0 -0
  35. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/__init__.py +0 -0
  36. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/atomic_io.py +0 -0
  37. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/auth.py +0 -0
  38. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/config_store.py +0 -0
  39. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/custom_tag_store.py +0 -0
  40. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/db_migrations.py +0 -0
  41. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/events.py +0 -0
  42. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/fix_application.py +0 -0
  43. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/fix_builder.py +0 -0
  44. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/fix_parser.py +0 -0
  45. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/fix_spec_parser.py +0 -0
  46. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/fix_tags.py +0 -0
  47. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/fix_time.py +0 -0
  48. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/housekeeping.py +0 -0
  49. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/inbound.py +0 -0
  50. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/json_store.py +0 -0
  51. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/latency_csv_writer.py +0 -0
  52. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/message_log.py +0 -0
  53. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/message_store.py +0 -0
  54. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/models.py +0 -0
  55. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/perf_charts.py +0 -0
  56. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/perf_engine.py +0 -0
  57. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/perf_models.py +0 -0
  58. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/perf_payload.py +0 -0
  59. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/perf_stats.py +0 -0
  60. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/perf_store.py +0 -0
  61. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/perf_writer.py +0 -0
  62. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/scenario_runner.py +0 -0
  63. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/scenario_store.py +0 -0
  64. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/session.py +0 -0
  65. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/spec_overlay_store.py +0 -0
  66. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/spec_store.py +0 -0
  67. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/template_store.py +0 -0
  68. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/user_store.py +0 -0
  69. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/value_gen.py +0 -0
  70. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/core/venue_responses.py +0 -0
  71. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/server.py +0 -0
  72. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  73. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  74. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/static/assets/index-D0a-mw9U.css +0 -0
  75. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  76. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/static/favicon.svg +0 -0
  77. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixture/ui/__init__.py +0 -0
  78. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
  79. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/entry_points.txt +0 -0
  80. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/requires.txt +0 -0
  81. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/fixtureqa.egg-info/top_level.txt +0 -0
  82. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/setup.cfg +0 -0
  83. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_atomic_io.py +0 -0
  84. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_auth.py +0 -0
  85. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_config_store.py +0 -0
  86. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_connection_manager.py +0 -0
  87. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_db_migrations.py +0 -0
  88. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_fix_builder.py +0 -0
  89. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_fix_spec_tree.py +0 -0
  90. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_health.py +0 -0
  91. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_inbound.py +0 -0
  92. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_inbound_validation.py +0 -0
  93. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_message_store.py +0 -0
  94. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_msg_user_attribution.py +0 -0
  95. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_non_stop_session.py +0 -0
  96. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_perf_api.py +0 -0
  97. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_perf_engine.py +0 -0
  98. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_perf_models.py +0 -0
  99. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_perf_payload.py +0 -0
  100. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_perf_rehydrate.py +0 -0
  101. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_scenarios.py +0 -0
  102. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_session_auto_response.py +0 -0
  103. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_session_lifecycle.py +0 -0
  104. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_session_manager_concurrency.py +0 -0
  105. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_sessions.py +0 -0
  106. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_specs.py +0 -0
  107. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_templates.py +0 -0
  108. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_timezone_data.py +0 -0
  109. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_value_gen.py +0 -0
  110. {fixtureqa-0.13.0 → fixtureqa-0.15.0}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.13.0
3
+ Version: 0.15.0
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -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
- # A connection receives the event if it is an admin, or its uid
267
- # matches the session owner.
268
- targets = [ws for ws, info in {**self._clients.get(session_id, {}),
269
- **self._clients.get("*", {})}.items()
270
- if info.is_admin or info.uid == owner_uid]
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). One dependency instead of a
70
- _check_ownership copy in every session-scoped router."""
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: OwnedSession,
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: OwnedSession,
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: OwnedSession,
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
- """Only admin responses carry owner_username (the UI owner badge / Mine filter)."""
130
- return us if user.is_admin else None
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: OwnedSession, sm: SM, us: US, user: CurrentUser):
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: OwnedSession,
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
- # Check ownership of this specific session
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 or (not is_admin and cfg.owner_uid != uid):
142
- logger.warning("WS /ws/%s: access denied for uid=%s", session_id, uid)
143
- await websocket.close(code=4003, reason="Access denied")
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
- configs = [s.config for s in self._sessions.values()]
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)