fixtureqa 0.9.8__tar.gz → 0.10.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.9.8/fixtureqa.egg-info → fixtureqa-0.10.0}/PKG-INFO +1 -1
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/fix_spec.py +28 -10
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/sessions.py +15 -5
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/specs.py +3 -3
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/schemas.py +6 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_spec_parser.py +50 -10
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/models.py +18 -3
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/session.py +54 -8
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/session_manager.py +15 -1
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/spec_store.py +21 -12
- fixtureqa-0.10.0/fixture/static/assets/index-DrDKHUxW.js +102 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/index.html +1 -1
- {fixtureqa-0.9.8 → fixtureqa-0.10.0/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/SOURCES.txt +1 -1
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/pyproject.toml +1 -1
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_config_store.py +2 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_specs.py +8 -2
- fixtureqa-0.9.8/fixture/static/assets/index-B5sIIunI.js +0 -102
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/LICENSE +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/README.md +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/__init__.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/__main__.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/app.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/deps.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/auth.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/events.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_charts.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_engine.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/value_gen.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/server.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/assets/index-B-hpSZWZ.css +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/setup.cfg +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_auth.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_health.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_inbound.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_message_store.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_non_stop_session.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_api.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_engine.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_session_auto_response.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_sessions.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_templates.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_timezone_data.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_value_gen.py +0 -0
- {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_ws.py +0 -0
|
@@ -11,37 +11,55 @@ from ...core.user_store import User
|
|
|
11
11
|
from ..deps import get_current_user, get_spec_overlay_store, get_spec_store
|
|
12
12
|
from ..schemas import FieldRefResponse, MessageDefResponse, MessageSummaryResponse
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
from fixcore.message import bundled_app_specs, bundled_begin_strings
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
BUNDLED = {"FIX42.xml", "FIX44.xml"}
|
|
16
|
+
router = APIRouter(tags=["fix-spec"])
|
|
18
17
|
|
|
19
18
|
SO = Annotated[SpecOverlayStore, Depends(get_spec_overlay_store)]
|
|
20
19
|
SS = Annotated[SpecStore, Depends(get_spec_store)]
|
|
21
20
|
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
22
21
|
|
|
23
22
|
|
|
23
|
+
def _supported() -> set[str]:
|
|
24
|
+
"""Selectable BeginStrings — every transport-capable bundled spec."""
|
|
25
|
+
return set(bundled_begin_strings().values())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _bundled_names() -> set[str]:
|
|
29
|
+
"""All bundled spec filenames accepted as an explicit ``spec`` override
|
|
30
|
+
(transport specs + FIX 5.0 application specs)."""
|
|
31
|
+
return set(bundled_begin_strings()) | set(bundled_app_specs())
|
|
32
|
+
|
|
33
|
+
|
|
24
34
|
def _validate_begin_string(begin_string: str) -> None:
|
|
25
|
-
if begin_string not in
|
|
35
|
+
if begin_string not in _supported():
|
|
26
36
|
raise HTTPException(
|
|
27
37
|
status_code=400,
|
|
28
|
-
detail=f"Unsupported begin_string '{begin_string}'. Use: {sorted(
|
|
38
|
+
detail=f"Unsupported begin_string '{begin_string}'. Use: {sorted(_supported())}",
|
|
29
39
|
)
|
|
30
40
|
|
|
31
41
|
|
|
42
|
+
def _is_allowed_spec(spec: str, user: User, spec_store: SpecStore) -> bool:
|
|
43
|
+
"""A ``spec`` override is allowed if it's a bundled name, a bundled
|
|
44
|
+
``transport+app`` combined ref, or a custom upload the caller owns."""
|
|
45
|
+
bundled = _bundled_names()
|
|
46
|
+
if "+" in spec:
|
|
47
|
+
return all(part in bundled for part in spec.split("+", 1))
|
|
48
|
+
return spec in bundled or spec_store.owns_path(user.uid, spec)
|
|
49
|
+
|
|
50
|
+
|
|
32
51
|
def _resolve_spec_ref(
|
|
33
52
|
begin_string: str, spec: str | None, user: User, spec_store: SpecStore
|
|
34
53
|
) -> str:
|
|
35
|
-
"""The fixcore spec ref to browse: an explicit ``spec`` (a bundled name,
|
|
36
|
-
custom upload the caller owns) overrides
|
|
54
|
+
"""The fixcore spec ref to browse: an explicit ``spec`` (a bundled name, a
|
|
55
|
+
``transport+app`` combined ref, or a custom upload the caller owns) overrides
|
|
56
|
+
the version-derived bundled spec.
|
|
37
57
|
|
|
38
58
|
A custom path the caller doesn't own is rejected so the browser can't be
|
|
39
59
|
used to read arbitrary files.
|
|
40
60
|
"""
|
|
41
61
|
if spec:
|
|
42
|
-
if spec
|
|
43
|
-
return spec
|
|
44
|
-
if spec_store.owns_path(user.uid, spec):
|
|
62
|
+
if _is_allowed_spec(spec, user, spec_store):
|
|
45
63
|
return spec
|
|
46
64
|
raise HTTPException(status_code=403, detail="Spec not accessible")
|
|
47
65
|
_validate_begin_string(begin_string)
|
|
@@ -17,19 +17,23 @@ 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
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
def _bundled_specs() -> set[str]:
|
|
21
|
+
"""Bundled spec names fixcore resolves by itself — transport dictionaries
|
|
22
|
+
(FIX 4.x + FIXT.1.1) plus the FIX 5.0 application dictionaries."""
|
|
23
|
+
from fixcore.message import bundled_app_specs, bundled_begin_strings
|
|
24
|
+
return set(bundled_begin_strings()) | set(bundled_app_specs())
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
def _validate_data_dictionary(value: Optional[str], user: User, spec_store: SpecStore) -> None:
|
|
25
28
|
"""A session may reference a bundled spec or a custom upload it owns — never
|
|
26
|
-
an arbitrary filesystem path (which would let it read any file on start).
|
|
27
|
-
|
|
29
|
+
an arbitrary filesystem path (which would let it read any file on start).
|
|
30
|
+
Covers both data_dictionary_path and a FIXT session's app_data_dictionary."""
|
|
31
|
+
if not value or value in _bundled_specs():
|
|
28
32
|
return
|
|
29
33
|
if not spec_store.owns_path(user.uid, value):
|
|
30
34
|
raise HTTPException(
|
|
31
35
|
status_code=400,
|
|
32
|
-
detail="
|
|
36
|
+
detail="dictionary must be a bundled spec name or one of your uploaded specs",
|
|
33
37
|
)
|
|
34
38
|
|
|
35
39
|
|
|
@@ -109,6 +113,8 @@ def _session_response(sm: SessionManager, session_id: str, us: Optional[UserStor
|
|
|
109
113
|
auto_fill=cfg.auto_fill,
|
|
110
114
|
auto_fill_delay_ms=cfg.auto_fill_delay_ms,
|
|
111
115
|
data_dictionary_path=cfg.data_dictionary_path,
|
|
116
|
+
app_data_dictionary=cfg.app_data_dictionary,
|
|
117
|
+
default_appl_ver_id=cfg.default_appl_ver_id,
|
|
112
118
|
status=state.status.name,
|
|
113
119
|
last_sent_seq_num=state.last_sent_seq_num,
|
|
114
120
|
last_recv_seq_num=state.last_recv_seq_num,
|
|
@@ -135,6 +141,7 @@ def list_sessions(sm: SM, us: US, user: CurrentUser):
|
|
|
135
141
|
@router.post("/", response_model=SessionResponse, status_code=201)
|
|
136
142
|
async def create_session(body: SessionConfigRequest, sm: SM, us: US, ss: SS, user: CurrentUser):
|
|
137
143
|
_validate_data_dictionary(body.data_dictionary_path, user, ss)
|
|
144
|
+
_validate_data_dictionary(body.app_data_dictionary, user, ss)
|
|
138
145
|
cfg = _config_from_request(body, owner_uid=user.uid)
|
|
139
146
|
|
|
140
147
|
try:
|
|
@@ -173,6 +180,7 @@ def import_sessions(body: list[SessionConfigRequest], sm: SM, us: US, ss: SS, us
|
|
|
173
180
|
for item in body:
|
|
174
181
|
try:
|
|
175
182
|
_validate_data_dictionary(item.data_dictionary_path, user, ss)
|
|
183
|
+
_validate_data_dictionary(item.app_data_dictionary, user, ss)
|
|
176
184
|
cfg = _config_from_request(item, owner_uid=user.uid)
|
|
177
185
|
session_id = sm.add_session(cfg)
|
|
178
186
|
except (HTTPException, ValueError, KeyError):
|
|
@@ -190,6 +198,8 @@ def get_session(session_id: str, _cfg: OwnedSession, sm: SM, us: US, user: Curre
|
|
|
190
198
|
def update_session(session_id: str, body: UpdateSessionRequest, _cfg: OwnedSession, sm: SM, us: US, ss: SS, user: CurrentUser):
|
|
191
199
|
if "data_dictionary_path" in body.model_fields_set:
|
|
192
200
|
_validate_data_dictionary(body.data_dictionary_path, user, ss)
|
|
201
|
+
if "app_data_dictionary" in body.model_fields_set:
|
|
202
|
+
_validate_data_dictionary(body.app_data_dictionary, user, ss)
|
|
193
203
|
try:
|
|
194
204
|
sm.update_session(session_id, body)
|
|
195
205
|
except RuntimeError as e:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Custom FIX spec (DataDictionary) upload/list/delete.
|
|
2
2
|
|
|
3
|
-
Bundled specs (
|
|
4
|
-
the per-user *custom* uploads and lists
|
|
5
|
-
offer one combined dropdown.
|
|
3
|
+
Bundled specs (FIX 4.0–4.4, FIXT11 + FIX50x) ship inside fixcore; this router
|
|
4
|
+
manages the per-user *custom* uploads and lists them together so the session
|
|
5
|
+
builder can offer one combined dropdown.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from typing import Annotated
|
|
@@ -23,6 +23,8 @@ class SessionConfigRequest(BaseModel):
|
|
|
23
23
|
send_redundant_resend_requests: bool = False
|
|
24
24
|
enable_last_msg_seq_num_processed: bool = False # stamp LastMsgSeqNumProcessed(369)
|
|
25
25
|
data_dictionary_path: Optional[str] = None
|
|
26
|
+
app_data_dictionary: Optional[str] = None # FIXT.1.1: FIX 5.0 application spec
|
|
27
|
+
default_appl_ver_id: str = "" # FIXT.1.1: tag 1137 (7/8/9 or label)
|
|
26
28
|
use_file_log: bool = True
|
|
27
29
|
start_time: str = "00:00:00"
|
|
28
30
|
end_time: str = "00:00:00"
|
|
@@ -92,6 +94,8 @@ class SessionResponse(BaseModel):
|
|
|
92
94
|
auto_fill: bool
|
|
93
95
|
auto_fill_delay_ms: int
|
|
94
96
|
data_dictionary_path: Optional[str]
|
|
97
|
+
app_data_dictionary: Optional[str]
|
|
98
|
+
default_appl_ver_id: str
|
|
95
99
|
status: str
|
|
96
100
|
last_sent_seq_num: int
|
|
97
101
|
last_recv_seq_num: int
|
|
@@ -122,6 +126,8 @@ class UpdateSessionRequest(BaseModel):
|
|
|
122
126
|
send_redundant_resend_requests: Optional[bool] = None
|
|
123
127
|
enable_last_msg_seq_num_processed: Optional[bool] = None
|
|
124
128
|
data_dictionary_path: Optional[str] = None
|
|
129
|
+
app_data_dictionary: Optional[str] = None
|
|
130
|
+
default_appl_ver_id: Optional[str] = None
|
|
125
131
|
use_file_log: Optional[bool] = None
|
|
126
132
|
start_time: Optional[str] = None
|
|
127
133
|
end_time: Optional[str] = None
|
|
@@ -2,27 +2,38 @@
|
|
|
2
2
|
Structured FIX field/message definitions for the GUI spec browser.
|
|
3
3
|
|
|
4
4
|
This is a thin adapter over fixcore's :class:`~fixcore.message.DataDictionary`
|
|
5
|
-
— fixture no longer parses spec XML itself. fixcore
|
|
6
|
-
|
|
7
|
-
field lists here are complete
|
|
5
|
+
— fixture no longer parses spec XML itself. fixcore bundles the FIX 4.0–4.4 and
|
|
6
|
+
FIX 5.0 (FIXT.1.1) dictionaries and (crucially) expands ``<component>``
|
|
7
|
+
references, so the field lists here are complete (NewOrderSingle's Instrument /
|
|
8
8
|
OrderQtyData / Parties fields, etc.) where the old in-house parser silently
|
|
9
9
|
dropped every component-sourced field.
|
|
10
10
|
|
|
11
|
-
A *spec ref* is whatever
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
A *spec ref* is whatever :func:`_load_dd` accepts: a bundled spec name
|
|
12
|
+
(``"FIX42.xml"``), an absolute path to a custom uploaded spec, or a
|
|
13
|
+
``transport+app`` pair for FIX 5.0 (``"FIXT11.xml+FIX50SP2.xml"``) loaded as a
|
|
14
|
+
combined dictionary.
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
17
18
|
|
|
18
19
|
import logging
|
|
19
20
|
from dataclasses import dataclass, field
|
|
21
|
+
from functools import cache
|
|
20
22
|
|
|
21
|
-
from fixcore.message import
|
|
23
|
+
from fixcore.message import (
|
|
24
|
+
bundled_begin_strings,
|
|
25
|
+
load_combined_data_dictionary,
|
|
26
|
+
load_data_dictionary,
|
|
27
|
+
)
|
|
22
28
|
from fixcore.message.data_dictionary import GroupDef
|
|
23
29
|
|
|
24
30
|
logger = logging.getLogger(__name__)
|
|
25
31
|
|
|
32
|
+
# The FIXT.1.1 transport spec and the default FIX 5.0 application spec used when
|
|
33
|
+
# browsing a FIXT session's combined dictionary without an explicit app version.
|
|
34
|
+
TRANSPORT_SPEC = "FIXT11.xml"
|
|
35
|
+
DEFAULT_FIXT_APP_SPEC = "FIX50SP2.xml"
|
|
36
|
+
|
|
26
37
|
|
|
27
38
|
@dataclass
|
|
28
39
|
class FieldDef:
|
|
@@ -63,13 +74,42 @@ class _Spec:
|
|
|
63
74
|
_CACHE: dict[str, _Spec] = {}
|
|
64
75
|
|
|
65
76
|
|
|
77
|
+
@cache
|
|
78
|
+
def _begin_string_to_spec() -> dict[str, str]:
|
|
79
|
+
"""Inverse of fixcore's ``bundled_begin_strings`` — version label → spec name
|
|
80
|
+
(e.g. ``"FIX.4.2" -> "FIX42.xml"``, ``"FIXT.1.1" -> "FIXT11.xml"``)."""
|
|
81
|
+
return {ver: name for name, ver in bundled_begin_strings().items()}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def bundled_spec_for_begin_string(begin_string: str) -> str:
|
|
85
|
+
"""The bundled transport spec name for a FIX BeginString (fixcore resolves
|
|
86
|
+
it). Falls back to FIX44.xml for an unknown version."""
|
|
87
|
+
return _begin_string_to_spec().get(begin_string, "FIX44.xml")
|
|
88
|
+
|
|
89
|
+
|
|
66
90
|
def spec_ref_for_version(begin_string: str) -> str:
|
|
67
|
-
"""The
|
|
68
|
-
|
|
91
|
+
"""The fixcore spec ref to browse for a BeginString.
|
|
92
|
+
|
|
93
|
+
FIX 4.x → its bundled single-file spec. FIXT.1.1 → a combined
|
|
94
|
+
``transport+app`` ref (the default FIX.5.0SP2 application layer) that
|
|
95
|
+
:func:`_load_dd` resolves via ``load_combined_data_dictionary``.
|
|
96
|
+
"""
|
|
97
|
+
if begin_string.startswith("FIXT"):
|
|
98
|
+
return f"{TRANSPORT_SPEC}+{DEFAULT_FIXT_APP_SPEC}"
|
|
99
|
+
return bundled_spec_for_begin_string(begin_string)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _load_dd(spec_ref: str):
|
|
103
|
+
"""Load a DataDictionary from a spec ref. A ``transport+app`` ref (FIXT.1.1)
|
|
104
|
+
is loaded as a combined dictionary; anything else is a single spec."""
|
|
105
|
+
if "+" in spec_ref:
|
|
106
|
+
transport, app = spec_ref.split("+", 1)
|
|
107
|
+
return load_combined_data_dictionary(transport, app)
|
|
108
|
+
return load_data_dictionary(spec_ref)
|
|
69
109
|
|
|
70
110
|
|
|
71
111
|
def _build_spec(spec_ref: str) -> _Spec:
|
|
72
|
-
dd =
|
|
112
|
+
dd = _load_dd(spec_ref)
|
|
73
113
|
|
|
74
114
|
field_defs: dict[int, FieldDef] = {
|
|
75
115
|
tag: FieldDef(number=fd.number, name=fd.name, type=fd.type, values=dict(fd.values))
|
|
@@ -34,8 +34,10 @@ class SessionConfig:
|
|
|
34
34
|
session_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
35
35
|
display_name: str = ""
|
|
36
36
|
|
|
37
|
-
# FIX identity
|
|
38
|
-
|
|
37
|
+
# FIX identity. Any transport-capable bundled version: "FIX.4.0".."FIX.4.4"
|
|
38
|
+
# or "FIXT.1.1" (the FIX 5.0 transport layer — pair with app_data_dictionary
|
|
39
|
+
# / default_appl_ver_id below).
|
|
40
|
+
begin_string: str = "FIX.4.2"
|
|
39
41
|
sender_comp_id: str = ""
|
|
40
42
|
target_comp_id: str = ""
|
|
41
43
|
|
|
@@ -62,9 +64,22 @@ class SessionConfig:
|
|
|
62
64
|
check_latency: bool = False
|
|
63
65
|
max_latency: int = 120 # MaxLatency (s): allowed SendingTime skew when check_latency
|
|
64
66
|
|
|
65
|
-
# Data dictionary (None = use bundled spec)
|
|
67
|
+
# Data dictionary (None = use bundled spec). For a FIXT.1.1 session this is
|
|
68
|
+
# ignored — the dictionary comes from the FIXT11 transport spec combined with
|
|
69
|
+
# the FIX 5.0 application spec below.
|
|
66
70
|
data_dictionary_path: Optional[str] = None
|
|
67
71
|
|
|
72
|
+
# FIX 5.0 / FIXT.1.1 only. FIX 5.0 splits the spec into a frozen transport
|
|
73
|
+
# layer (FIXT11.xml) plus a versioned application dictionary; fixcore combines
|
|
74
|
+
# them (DataDictionary.combined). For a FIXT session set the application
|
|
75
|
+
# dictionary either explicitly (app_data_dictionary: a bundled name like
|
|
76
|
+
# "FIX50SP2.xml" or a custom upload's path) or implicitly via
|
|
77
|
+
# default_appl_ver_id (tag 1137: "7"=FIX.5.0, "8"=FIX.5.0SP1, "9"=FIX.5.0SP2,
|
|
78
|
+
# or a label like "FIX.5.0SP2"), which is also stamped on the outbound Logon.
|
|
79
|
+
# Ignored for non-FIXT sessions.
|
|
80
|
+
app_data_dictionary: Optional[str] = None
|
|
81
|
+
default_appl_ver_id: str = ""
|
|
82
|
+
|
|
68
83
|
# Store / log paths (use per-session subdirs to avoid collisions)
|
|
69
84
|
file_store_path: str = "./store"
|
|
70
85
|
file_log_path: str = "./log"
|
|
@@ -331,7 +331,6 @@ class Session:
|
|
|
331
331
|
def _build_settings(self) -> SessionSettings:
|
|
332
332
|
"""Build fixcore SessionSettings from SessionConfig."""
|
|
333
333
|
ct = "initiator" if self.config.connection_type == ConnectionType.INITIATOR else "acceptor"
|
|
334
|
-
dd_path = self._resolve_data_dictionary()
|
|
335
334
|
|
|
336
335
|
store_path = os.path.abspath(
|
|
337
336
|
os.path.join(self.config.file_store_path, self.config.session_id)
|
|
@@ -384,7 +383,7 @@ class Session:
|
|
|
384
383
|
f"SendRedundantResendRequests={yn(self.config.send_redundant_resend_requests)}",
|
|
385
384
|
f"EnableLastMsgSeqNumProcessed={yn(self.config.enable_last_msg_seq_num_processed)}",
|
|
386
385
|
*schedule_lines,
|
|
387
|
-
|
|
386
|
+
*self._dictionary_lines(),
|
|
388
387
|
# Always load the dictionary so inbound repeating groups decode into
|
|
389
388
|
# proper instances (otherwise duplicate group-member tags collapse to
|
|
390
389
|
# the last value). Whether non-conforming messages are *rejected* is a
|
|
@@ -420,29 +419,76 @@ class Session:
|
|
|
420
419
|
|
|
421
420
|
return SessionSettings.from_string("\n".join(lines))
|
|
422
421
|
|
|
422
|
+
@property
|
|
423
|
+
def _is_fixt(self) -> bool:
|
|
424
|
+
"""FIX 5.0 rides on a FIXT.1.1 transport — these sessions use a combined
|
|
425
|
+
transport+application dictionary rather than a single one."""
|
|
426
|
+
return self.config.begin_string.startswith("FIXT")
|
|
427
|
+
|
|
428
|
+
def _dictionary_lines(self) -> list[str]:
|
|
429
|
+
"""The fixcore DataDictionary config lines for this session.
|
|
430
|
+
|
|
431
|
+
Non-FIXT: a single ``DataDictionary=`` (bundled name or custom path).
|
|
432
|
+
FIXT.1.1: ``TransportDataDictionary`` + ``AppDataDictionary`` (and the
|
|
433
|
+
negotiated ``DefaultApplVerID``), which fixcore merges into one combined
|
|
434
|
+
dictionary (see the FIX 5.0 design in fixcore). At least one of
|
|
435
|
+
AppDataDictionary / DefaultApplVerID is present (enforced on save).
|
|
436
|
+
"""
|
|
437
|
+
if self._is_fixt:
|
|
438
|
+
from .fix_spec_parser import TRANSPORT_SPEC
|
|
439
|
+
lines = [f"TransportDataDictionary={TRANSPORT_SPEC}"]
|
|
440
|
+
app_ref = self._app_spec_ref()
|
|
441
|
+
if app_ref:
|
|
442
|
+
lines.append(f"AppDataDictionary={app_ref}")
|
|
443
|
+
if self.config.default_appl_ver_id:
|
|
444
|
+
lines.append(f"DefaultApplVerID={self.config.default_appl_ver_id}")
|
|
445
|
+
return lines
|
|
446
|
+
return [f"DataDictionary={self._resolve_data_dictionary()}"]
|
|
447
|
+
|
|
448
|
+
def _app_spec_ref(self) -> str:
|
|
449
|
+
"""The FIX 5.0 application spec ref for a FIXT session: a custom upload's
|
|
450
|
+
absolute path, or a bundled name fixcore resolves. "" if unset (then
|
|
451
|
+
fixcore infers it from DefaultApplVerID)."""
|
|
452
|
+
val = self.config.app_data_dictionary
|
|
453
|
+
if not val:
|
|
454
|
+
return ""
|
|
455
|
+
return os.path.abspath(val) if os.path.isfile(val) else val
|
|
456
|
+
|
|
423
457
|
def _resolve_data_dictionary(self) -> str:
|
|
424
|
-
"""The DataDictionary value for
|
|
425
|
-
fixcore resolves from its packaged dictionaries, or an absolute path
|
|
426
|
-
a custom uploaded spec.
|
|
458
|
+
"""The DataDictionary value for a non-FIXT session — a bundled spec name
|
|
459
|
+
that fixcore resolves from its packaged dictionaries, or an absolute path
|
|
460
|
+
to a custom uploaded spec.
|
|
427
461
|
|
|
428
462
|
fixcore's ``load_data_dictionary`` (used by both the transport and
|
|
429
463
|
:meth:`data_dictionary`) checks disk first and only then falls back to a
|
|
430
464
|
bundled name, so a real file always wins.
|
|
431
465
|
"""
|
|
466
|
+
from .fix_spec_parser import bundled_spec_for_begin_string
|
|
432
467
|
val = self.config.data_dictionary_path
|
|
433
468
|
if val:
|
|
434
469
|
# A custom spec is stored as an on-disk path; anything else (e.g.
|
|
435
470
|
# "FIX44.xml") is a bundled spec name fixcore resolves by itself.
|
|
436
471
|
return os.path.abspath(val) if os.path.isfile(val) else val
|
|
437
472
|
# Default: fixcore's bundled dictionary for this session's FIX version.
|
|
438
|
-
return
|
|
473
|
+
return bundled_spec_for_begin_string(self.config.begin_string)
|
|
439
474
|
|
|
440
475
|
def data_dictionary(self):
|
|
441
476
|
"""The loaded DataDictionary for this session's spec (cached by fixcore),
|
|
442
477
|
or None if it can't be loaded. Used to build outbound messages with
|
|
443
|
-
correct repeating-group structure.
|
|
444
|
-
|
|
478
|
+
correct repeating-group structure. For FIXT.1.1 this is the combined
|
|
479
|
+
transport+application dictionary."""
|
|
480
|
+
from fixcore.message import (
|
|
481
|
+
appl_ver_id_to_spec,
|
|
482
|
+
load_combined_data_dictionary,
|
|
483
|
+
load_data_dictionary,
|
|
484
|
+
)
|
|
445
485
|
try:
|
|
486
|
+
if self._is_fixt:
|
|
487
|
+
from .fix_spec_parser import TRANSPORT_SPEC
|
|
488
|
+
app = self._app_spec_ref() or appl_ver_id_to_spec(
|
|
489
|
+
self.config.default_appl_ver_id
|
|
490
|
+
)
|
|
491
|
+
return load_combined_data_dictionary(TRANSPORT_SPEC, app)
|
|
446
492
|
return load_data_dictionary(self._resolve_data_dictionary())
|
|
447
493
|
except Exception:
|
|
448
494
|
return None
|
|
@@ -262,8 +262,22 @@ class SessionManager:
|
|
|
262
262
|
raise ValueError("sender_comp_id is required")
|
|
263
263
|
if not config.target_comp_id:
|
|
264
264
|
raise ValueError("target_comp_id is required")
|
|
265
|
-
|
|
265
|
+
from fixcore.message import appl_ver_id_to_spec, bundled_begin_strings
|
|
266
|
+
if config.begin_string not in set(bundled_begin_strings().values()):
|
|
266
267
|
raise ValueError(f"Unsupported FIX version: {config.begin_string!r}")
|
|
268
|
+
# FIX 5.0 over FIXT.1.1: the transport BeginString carries no application
|
|
269
|
+
# messages, so a FIXT session needs an application dictionary — either an
|
|
270
|
+
# explicit AppDataDictionary or a DefaultApplVerID to infer it from.
|
|
271
|
+
if config.begin_string.startswith("FIXT"):
|
|
272
|
+
if not (config.app_data_dictionary or config.default_appl_ver_id):
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"A FIXT.1.1 session requires an application dictionary: set "
|
|
275
|
+
"app_data_dictionary or default_appl_ver_id"
|
|
276
|
+
)
|
|
277
|
+
if config.default_appl_ver_id:
|
|
278
|
+
# Reject an unrecognised tag-1137 value/label up front (fixcore
|
|
279
|
+
# would otherwise only raise at engine start).
|
|
280
|
+
appl_ver_id_to_spec(config.default_appl_ver_id)
|
|
267
281
|
# Schedule-combo rules (mirror fixcore so a bad combo fails on save with a
|
|
268
282
|
# clear message instead of at engine start). Only meaningful when a
|
|
269
283
|
# windowed schedule is in play, i.e. not non-stop.
|
|
@@ -6,10 +6,10 @@ Custom specs are uploaded QuickFIX-format XML files persisted at
|
|
|
6
6
|
JSON stores, but the payload is raw XML (validated by parsing it through
|
|
7
7
|
fixcore's DataDictionary before it is accepted).
|
|
8
8
|
|
|
9
|
-
The bundled FIX 4.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
one combined dropdown.
|
|
9
|
+
The bundled FIX 4.0–4.4 / FIX 5.0 dictionaries are *not* stored here — they ship
|
|
10
|
+
inside fixcore. :func:`SpecStore.list_specs` returns the bundled transport specs
|
|
11
|
+
and FIX 5.0 application specs alongside the user's uploaded ones so the UI can
|
|
12
|
+
show one combined dropdown.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
@@ -18,7 +18,11 @@ import os
|
|
|
18
18
|
import re
|
|
19
19
|
import threading
|
|
20
20
|
|
|
21
|
-
from fixcore.message import
|
|
21
|
+
from fixcore.message import (
|
|
22
|
+
bundled_app_specs,
|
|
23
|
+
bundled_begin_strings,
|
|
24
|
+
load_data_dictionary,
|
|
25
|
+
)
|
|
22
26
|
|
|
23
27
|
from .atomic_io import atomic_write_text
|
|
24
28
|
|
|
@@ -82,15 +86,20 @@ class SpecStore:
|
|
|
82
86
|
|
|
83
87
|
Shape::
|
|
84
88
|
|
|
85
|
-
{"bundled": [{"name", "value", "kind": "bundled"}, ...],
|
|
89
|
+
{"bundled": [{"name", "value", "version", "kind": "bundled"}, ...],
|
|
90
|
+
"app": [{"name", "value", "version", "kind": "app"}, ...],
|
|
86
91
|
"custom": [{"name", "value", "version", "kind": "custom"}, ...]}
|
|
87
92
|
|
|
88
|
-
``
|
|
89
|
-
a
|
|
90
|
-
|
|
93
|
+
``bundled`` are the transport-capable specs (FIX 4.x + FIXT.1.1) usable as
|
|
94
|
+
a ``DataDictionary``; ``app`` are the FIX 5.0 application dictionaries
|
|
95
|
+
usable as a FIXT session's ``AppDataDictionary``. ``value`` is what gets
|
|
96
|
+
stored on the SessionConfig: a bare bundled name (resolved by fixcore) for
|
|
97
|
+
bundled/app specs, and an absolute path for custom ones.
|
|
91
98
|
"""
|
|
92
|
-
bundled = [{"name": n, "value": n, "kind": "bundled"}
|
|
93
|
-
for n in
|
|
99
|
+
bundled = [{"name": n, "value": n, "version": v, "kind": "bundled"}
|
|
100
|
+
for n, v in bundled_begin_strings().items()]
|
|
101
|
+
app = [{"name": n, "value": n, "version": v, "kind": "app"}
|
|
102
|
+
for n, v in bundled_app_specs().items()]
|
|
94
103
|
custom: list[dict] = []
|
|
95
104
|
d = self._dir(uid)
|
|
96
105
|
if os.path.isdir(d):
|
|
@@ -105,7 +114,7 @@ class SpecStore:
|
|
|
105
114
|
pass
|
|
106
115
|
custom.append({"name": fn, "value": path,
|
|
107
116
|
"version": version, "kind": "custom"})
|
|
108
|
-
return {"bundled": bundled, "custom": custom}
|
|
117
|
+
return {"bundled": bundled, "app": app, "custom": custom}
|
|
109
118
|
|
|
110
119
|
def delete_spec(self, uid: str, name: str) -> bool:
|
|
111
120
|
name = self._sanitize(name)
|