fixtureqa 0.11.0__tar.gz → 0.12.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.11.0/fixtureqa.egg-info → fixtureqa-0.12.0}/PKG-INFO +1 -1
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/fix_spec.py +80 -2
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/schemas.py +23 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_spec_parser.py +88 -2
- fixtureqa-0.12.0/fixture/static/assets/index-C-Ig00O9.js +102 -0
- fixtureqa-0.11.0/fixture/static/assets/index-uoEGYkf_.css → fixtureqa-0.12.0/fixture/static/assets/index-D0a-mw9U.css +1 -1
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/index.html +2 -2
- {fixtureqa-0.11.0 → fixtureqa-0.12.0/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/SOURCES.txt +3 -2
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/pyproject.toml +1 -1
- fixtureqa-0.12.0/tests/test_fix_spec_tree.py +131 -0
- fixtureqa-0.11.0/fixture/static/assets/index-CfkKS026.js +0 -102
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/LICENSE +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/README.md +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/__init__.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/__main__.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/app.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/deps.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/specs.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/auth.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/events.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/models.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_charts.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_engine.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/session.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/spec_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/value_gen.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/server.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/setup.cfg +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_auth.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_config_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_health.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_inbound.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_message_store.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_non_stop_session.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_api.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_engine.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_session_auto_response.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_sessions.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_specs.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_templates.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_timezone_data.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_value_gen.py +0 -0
- {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_ws.py +0 -0
|
@@ -3,13 +3,17 @@ from typing import Annotated
|
|
|
3
3
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
4
4
|
|
|
5
5
|
from ...core.fix_spec_parser import (
|
|
6
|
-
get_fields, get_message,
|
|
6
|
+
GroupNode, get_fields, get_message, get_message_tree, get_messages,
|
|
7
|
+
spec_ref_for_version,
|
|
7
8
|
)
|
|
8
9
|
from ...core.spec_overlay_store import SpecOverlayStore
|
|
9
10
|
from ...core.spec_store import SpecStore
|
|
10
11
|
from ...core.user_store import User
|
|
11
12
|
from ..deps import get_current_user, get_spec_overlay_store, get_spec_store
|
|
12
|
-
from ..schemas import
|
|
13
|
+
from ..schemas import (
|
|
14
|
+
FieldNodeResponse, FieldRefResponse, GroupNodeResponse, MessageDefResponse,
|
|
15
|
+
MessageSummaryResponse,
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
from fixcore.message import bundled_app_specs, bundled_begin_strings
|
|
15
19
|
|
|
@@ -140,3 +144,77 @@ def get_message_def(
|
|
|
140
144
|
)
|
|
141
145
|
|
|
142
146
|
raise HTTPException(status_code=404, detail=f"Message type '{msg_type}' not found in {begin_string}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _node_to_response(node) -> FieldNodeResponse | GroupNodeResponse:
|
|
150
|
+
if isinstance(node, GroupNode):
|
|
151
|
+
return GroupNodeResponse(
|
|
152
|
+
tag=node.tag,
|
|
153
|
+
name=node.name,
|
|
154
|
+
required=node.required,
|
|
155
|
+
delimiter=node.delimiter,
|
|
156
|
+
children=[_node_to_response(c) for c in node.children],
|
|
157
|
+
)
|
|
158
|
+
return FieldNodeResponse(
|
|
159
|
+
tag=node.tag,
|
|
160
|
+
name=node.name,
|
|
161
|
+
type=node.type,
|
|
162
|
+
required=node.required,
|
|
163
|
+
values=node.values,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@router.get("/messages/{msg_type}/tree",
|
|
168
|
+
response_model=list[FieldNodeResponse | GroupNodeResponse])
|
|
169
|
+
def get_message_tree_def(
|
|
170
|
+
msg_type: str,
|
|
171
|
+
begin_string: str = Query("FIX.4.2"),
|
|
172
|
+
spec: str | None = Query(None),
|
|
173
|
+
overlay_store: SO = ...,
|
|
174
|
+
spec_store: SS = ...,
|
|
175
|
+
user: CurrentUser = ...,
|
|
176
|
+
):
|
|
177
|
+
"""Group-preserving field tree for one message type (spec viewer)."""
|
|
178
|
+
spec_ref = _resolve_spec_ref(begin_string, spec, user, spec_store)
|
|
179
|
+
|
|
180
|
+
tree = get_message_tree(spec_ref, msg_type)
|
|
181
|
+
if tree is not None:
|
|
182
|
+
return [_node_to_response(n) for n in tree]
|
|
183
|
+
|
|
184
|
+
# Custom overlay message types have no repeating groups — flat leaves only.
|
|
185
|
+
overlay = overlay_store.get_overlay(user.uid)
|
|
186
|
+
field_defs = get_fields(spec_ref)
|
|
187
|
+
for om in overlay.get("messages", []):
|
|
188
|
+
if om.get("msg_type") != msg_type:
|
|
189
|
+
continue
|
|
190
|
+
nodes: list[FieldNodeResponse | GroupNodeResponse] = []
|
|
191
|
+
for f in om.get("fields", []):
|
|
192
|
+
tag = f.get("tag", 0)
|
|
193
|
+
fd = field_defs.get(tag)
|
|
194
|
+
nodes.append(FieldNodeResponse(
|
|
195
|
+
tag=tag,
|
|
196
|
+
name=fd.name if fd else f"Tag{tag}",
|
|
197
|
+
type=fd.type if fd else "STRING",
|
|
198
|
+
required=f.get("required", False),
|
|
199
|
+
values=fd.values if fd else {},
|
|
200
|
+
))
|
|
201
|
+
return nodes
|
|
202
|
+
|
|
203
|
+
raise HTTPException(status_code=404, detail=f"Message type '{msg_type}' not found in {begin_string}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@router.get("/fields", response_model=list[FieldRefResponse])
|
|
207
|
+
def list_fields(
|
|
208
|
+
begin_string: str = Query("FIX.4.2"),
|
|
209
|
+
spec: str | None = Query(None),
|
|
210
|
+
spec_store: SS = ...,
|
|
211
|
+
user: CurrentUser = ...,
|
|
212
|
+
):
|
|
213
|
+
"""The full field catalogue for a spec, ordered by tag number."""
|
|
214
|
+
spec_ref = _resolve_spec_ref(begin_string, spec, user, spec_store)
|
|
215
|
+
field_defs = get_fields(spec_ref)
|
|
216
|
+
return [
|
|
217
|
+
FieldRefResponse(tag=fd.number, name=fd.name, type=fd.type,
|
|
218
|
+
required=False, values=fd.values)
|
|
219
|
+
for _, fd in sorted(field_defs.items())
|
|
220
|
+
]
|
|
@@ -309,6 +309,29 @@ class MessageDefResponse(BaseModel):
|
|
|
309
309
|
fields: list[FieldRefResponse]
|
|
310
310
|
|
|
311
311
|
|
|
312
|
+
# Group-preserving message tree (spec viewer). A node is a leaf field or a
|
|
313
|
+
# repeating group whose ``children`` recurse.
|
|
314
|
+
class FieldNodeResponse(BaseModel):
|
|
315
|
+
kind: str = "field"
|
|
316
|
+
tag: int
|
|
317
|
+
name: str
|
|
318
|
+
type: str
|
|
319
|
+
required: bool
|
|
320
|
+
values: dict[str, str]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class GroupNodeResponse(BaseModel):
|
|
324
|
+
kind: str = "group"
|
|
325
|
+
tag: int # the NoXxx counter tag
|
|
326
|
+
name: str
|
|
327
|
+
required: bool
|
|
328
|
+
delimiter: int
|
|
329
|
+
children: list["FieldNodeResponse | GroupNodeResponse"]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
GroupNodeResponse.model_rebuild()
|
|
333
|
+
|
|
334
|
+
|
|
312
335
|
# Templates
|
|
313
336
|
|
|
314
337
|
# Scenarios
|
|
@@ -60,6 +60,31 @@ class MessageDef:
|
|
|
60
60
|
fields: list[FieldRef] = field(default_factory=list)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
@dataclass
|
|
64
|
+
class FieldNode:
|
|
65
|
+
"""A leaf field in a message tree."""
|
|
66
|
+
tag: int
|
|
67
|
+
name: str
|
|
68
|
+
type: str
|
|
69
|
+
required: bool
|
|
70
|
+
values: dict[str, str]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class GroupNode:
|
|
75
|
+
"""A repeating group in a message tree; ``children`` preserves member order
|
|
76
|
+
and nests further ``GroupNode``s for nested groups."""
|
|
77
|
+
tag: int # the NoXxx counter tag
|
|
78
|
+
name: str
|
|
79
|
+
required: bool
|
|
80
|
+
delimiter: int # first tag inside each group instance
|
|
81
|
+
children: list["FieldNode | GroupNode"] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# A node is either a leaf field or a repeating group.
|
|
85
|
+
Node = "FieldNode | GroupNode"
|
|
86
|
+
|
|
87
|
+
|
|
63
88
|
# ---------------------------------------------------------------------------
|
|
64
89
|
# Internal cache (per spec ref). fixcore caches the parsed DataDictionary
|
|
65
90
|
# itself; this caches the adapter's per-message FieldRef lists.
|
|
@@ -69,6 +94,7 @@ class MessageDef:
|
|
|
69
94
|
class _Spec:
|
|
70
95
|
field_defs: dict[int, FieldDef]
|
|
71
96
|
messages: dict[str, MessageDef]
|
|
97
|
+
trees: dict[str, list] # msg_type → list[FieldNode | GroupNode]
|
|
72
98
|
|
|
73
99
|
|
|
74
100
|
_CACHE: dict[str, _Spec] = {}
|
|
@@ -117,6 +143,7 @@ def _build_spec(spec_ref: str) -> _Spec:
|
|
|
117
143
|
}
|
|
118
144
|
|
|
119
145
|
messages: dict[str, MessageDef] = {}
|
|
146
|
+
trees: dict[str, list] = {}
|
|
120
147
|
for m in dd.messages():
|
|
121
148
|
messages[m.msg_type] = MessageDef(
|
|
122
149
|
msg_type=m.msg_type,
|
|
@@ -124,7 +151,8 @@ def _build_spec(spec_ref: str) -> _Spec:
|
|
|
124
151
|
category="admin" if m.msg_cat == "admin" else "app",
|
|
125
152
|
fields=_message_field_refs(m, field_defs),
|
|
126
153
|
)
|
|
127
|
-
|
|
154
|
+
trees[m.msg_type] = _message_tree(m, field_defs)
|
|
155
|
+
return _Spec(field_defs=field_defs, messages=messages, trees=trees)
|
|
128
156
|
|
|
129
157
|
|
|
130
158
|
def _message_field_refs(msg, field_defs: dict[int, FieldDef]) -> list[FieldRef]:
|
|
@@ -168,13 +196,62 @@ def _message_field_refs(msg, field_defs: dict[int, FieldDef]) -> list[FieldRef]:
|
|
|
168
196
|
return refs
|
|
169
197
|
|
|
170
198
|
|
|
199
|
+
def _message_tree(msg, field_defs: dict[int, FieldDef]) -> list:
|
|
200
|
+
"""Build a group-preserving tree for one message.
|
|
201
|
+
|
|
202
|
+
fixcore stores body fields as required/optional *sets* (no source order) and
|
|
203
|
+
has already inlined ``<component>`` refs, so the tree faithfully reproduces
|
|
204
|
+
repeating-group nesting only. Top-level leaves are ordered required-first
|
|
205
|
+
then by tag; group members keep fixcore's ordered ``members`` list. The NoXxx
|
|
206
|
+
counter tags are rendered as group nodes, never duplicated as leaves.
|
|
207
|
+
"""
|
|
208
|
+
def leaf(tag: int, required: bool) -> FieldNode:
|
|
209
|
+
fd = field_defs.get(tag)
|
|
210
|
+
return FieldNode(
|
|
211
|
+
tag=tag,
|
|
212
|
+
name=fd.name if fd else f"Tag{tag}",
|
|
213
|
+
type=fd.type if fd else "STRING",
|
|
214
|
+
required=required,
|
|
215
|
+
values=fd.values if fd else {},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def group_node(gd: GroupDef, required: bool) -> GroupNode:
|
|
219
|
+
counter = field_defs.get(gd.number_tag)
|
|
220
|
+
children: list = []
|
|
221
|
+
for tag in gd.members:
|
|
222
|
+
nested = gd.nested_groups.get(tag)
|
|
223
|
+
if nested is not None:
|
|
224
|
+
children.append(group_node(nested, False))
|
|
225
|
+
else:
|
|
226
|
+
children.append(leaf(tag, False))
|
|
227
|
+
return GroupNode(
|
|
228
|
+
tag=gd.number_tag,
|
|
229
|
+
name=counter.name if counter else f"Tag{gd.number_tag}",
|
|
230
|
+
required=required,
|
|
231
|
+
delimiter=gd.delimiter,
|
|
232
|
+
children=children,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
counters = set(msg.groups)
|
|
236
|
+
nodes: list = []
|
|
237
|
+
for tag in sorted(msg.required):
|
|
238
|
+
if tag not in counters:
|
|
239
|
+
nodes.append(leaf(tag, True))
|
|
240
|
+
for tag in sorted(msg.optional):
|
|
241
|
+
if tag not in counters:
|
|
242
|
+
nodes.append(leaf(tag, False))
|
|
243
|
+
for counter, gd in msg.groups.items():
|
|
244
|
+
nodes.append(group_node(gd, counter in msg.required))
|
|
245
|
+
return nodes
|
|
246
|
+
|
|
247
|
+
|
|
171
248
|
def _get_spec(spec_ref: str) -> _Spec:
|
|
172
249
|
if spec_ref not in _CACHE:
|
|
173
250
|
try:
|
|
174
251
|
_CACHE[spec_ref] = _build_spec(spec_ref)
|
|
175
252
|
except Exception as e:
|
|
176
253
|
logger.error("Failed to load FIX spec %s: %s", spec_ref, e)
|
|
177
|
-
_CACHE[spec_ref] = _Spec(field_defs={}, messages={})
|
|
254
|
+
_CACHE[spec_ref] = _Spec(field_defs={}, messages={}, trees={})
|
|
178
255
|
return _CACHE[spec_ref]
|
|
179
256
|
|
|
180
257
|
|
|
@@ -195,6 +272,15 @@ def get_message(spec_ref: str, msg_type: str) -> MessageDef | None:
|
|
|
195
272
|
return _get_spec(spec_ref).messages.get(msg_type)
|
|
196
273
|
|
|
197
274
|
|
|
275
|
+
def get_message_tree(spec_ref: str, msg_type: str) -> list | None:
|
|
276
|
+
"""The group-preserving field tree for one MsgType, or None if unknown.
|
|
277
|
+
|
|
278
|
+
Returns a list of ``FieldNode``/``GroupNode`` (nested). See
|
|
279
|
+
:func:`_message_tree` for the ordering/nesting semantics.
|
|
280
|
+
"""
|
|
281
|
+
return _get_spec(spec_ref).trees.get(msg_type)
|
|
282
|
+
|
|
283
|
+
|
|
198
284
|
def get_fields(spec_ref: str) -> dict[int, FieldDef]:
|
|
199
285
|
"""All field definitions keyed by tag number."""
|
|
200
286
|
return _get_spec(spec_ref).field_defs
|