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.
Files changed (114) hide show
  1. {fixtureqa-0.14.0/fixtureqa.egg-info → fixtureqa-0.16.0}/PKG-INFO +2 -1
  2. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/app.py +4 -1
  3. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/connection_manager.py +6 -5
  4. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/deps.py +25 -2
  5. fixtureqa-0.16.0/fixture/api/routers/atdl.py +148 -0
  6. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/messages.py +5 -4
  7. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/sessions.py +10 -5
  8. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/ws.py +5 -4
  9. fixtureqa-0.16.0/fixture/core/atdl_parser.py +359 -0
  10. fixtureqa-0.16.0/fixture/core/atdl_store.py +119 -0
  11. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/session_manager.py +5 -4
  12. fixtureqa-0.16.0/fixture/static/assets/index-DCRhkrrW.js +103 -0
  13. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/index.html +1 -1
  14. {fixtureqa-0.14.0 → fixtureqa-0.16.0/fixtureqa.egg-info}/PKG-INFO +2 -1
  15. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/SOURCES.txt +6 -1
  16. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/requires.txt +1 -0
  17. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/pyproject.toml +2 -1
  18. fixtureqa-0.16.0/tests/test_atdl.py +350 -0
  19. fixtureqa-0.16.0/tests/test_session_visibility.py +84 -0
  20. fixtureqa-0.14.0/fixture/static/assets/index-Bh-VNEs4.js +0 -102
  21. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/LICENSE +0 -0
  22. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/README.md +0 -0
  23. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/__init__.py +0 -0
  24. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/__main__.py +0 -0
  25. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/__init__.py +0 -0
  26. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/__init__.py +0 -0
  27. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/admin.py +0 -0
  28. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/auth.py +0 -0
  29. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/branding.py +0 -0
  30. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/custom_tags.py +0 -0
  31. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/fix_spec.py +0 -0
  32. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/perf.py +0 -0
  33. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/scenarios.py +0 -0
  34. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/setup.py +0 -0
  35. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/spec_overlay.py +0 -0
  36. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/specs.py +0 -0
  37. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/routers/templates.py +0 -0
  38. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/api/schemas.py +0 -0
  39. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/config/__init__.py +0 -0
  40. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/__init__.py +0 -0
  41. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/atomic_io.py +0 -0
  42. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/auth.py +0 -0
  43. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/config_store.py +0 -0
  44. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/custom_tag_store.py +0 -0
  45. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/db_migrations.py +0 -0
  46. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/events.py +0 -0
  47. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_application.py +0 -0
  48. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_builder.py +0 -0
  49. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_parser.py +0 -0
  50. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_spec_parser.py +0 -0
  51. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_tags.py +0 -0
  52. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/fix_time.py +0 -0
  53. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/housekeeping.py +0 -0
  54. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/inbound.py +0 -0
  55. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/json_store.py +0 -0
  56. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/latency_csv_writer.py +0 -0
  57. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/message_log.py +0 -0
  58. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/message_store.py +0 -0
  59. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/models.py +0 -0
  60. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_charts.py +0 -0
  61. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_engine.py +0 -0
  62. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_models.py +0 -0
  63. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_payload.py +0 -0
  64. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_stats.py +0 -0
  65. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_store.py +0 -0
  66. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/perf_writer.py +0 -0
  67. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/scenario_runner.py +0 -0
  68. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/scenario_store.py +0 -0
  69. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/session.py +0 -0
  70. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/spec_overlay_store.py +0 -0
  71. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/spec_store.py +0 -0
  72. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/template_store.py +0 -0
  73. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/user_store.py +0 -0
  74. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/value_gen.py +0 -0
  75. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/core/venue_responses.py +0 -0
  76. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/server.py +0 -0
  77. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  78. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  79. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/assets/index-D0a-mw9U.css +0 -0
  80. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  81. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/static/favicon.svg +0 -0
  82. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixture/ui/__init__.py +0 -0
  83. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
  84. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/entry_points.txt +0 -0
  85. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/fixtureqa.egg-info/top_level.txt +0 -0
  86. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/setup.cfg +0 -0
  87. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_atomic_io.py +0 -0
  88. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_auth.py +0 -0
  89. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_config_store.py +0 -0
  90. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_connection_manager.py +0 -0
  91. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_db_migrations.py +0 -0
  92. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_fix_builder.py +0 -0
  93. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_fix_spec_tree.py +0 -0
  94. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_health.py +0 -0
  95. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_inbound.py +0 -0
  96. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_inbound_validation.py +0 -0
  97. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_message_store.py +0 -0
  98. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_msg_user_attribution.py +0 -0
  99. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_non_stop_session.py +0 -0
  100. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_api.py +0 -0
  101. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_engine.py +0 -0
  102. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_models.py +0 -0
  103. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_payload.py +0 -0
  104. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_perf_rehydrate.py +0 -0
  105. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_scenarios.py +0 -0
  106. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_session_auto_response.py +0 -0
  107. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_session_lifecycle.py +0 -0
  108. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_session_manager_concurrency.py +0 -0
  109. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_sessions.py +0 -0
  110. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_specs.py +0 -0
  111. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_templates.py +0 -0
  112. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_timezone_data.py +0 -0
  113. {fixtureqa-0.14.0 → fixtureqa-0.16.0}/tests/test_value_gen.py +0 -0
  114. {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.14.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
- # 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:
@@ -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). One dependency instead of a
70
- _check_ownership copy in every session-scoped router."""
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: 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)