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.
Files changed (108) hide show
  1. {fixtureqa-0.11.0/fixtureqa.egg-info → fixtureqa-0.12.0}/PKG-INFO +1 -1
  2. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/fix_spec.py +80 -2
  3. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/schemas.py +23 -0
  4. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_spec_parser.py +88 -2
  5. fixtureqa-0.12.0/fixture/static/assets/index-C-Ig00O9.js +102 -0
  6. fixtureqa-0.11.0/fixture/static/assets/index-uoEGYkf_.css → fixtureqa-0.12.0/fixture/static/assets/index-D0a-mw9U.css +1 -1
  7. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/index.html +2 -2
  8. {fixtureqa-0.11.0 → fixtureqa-0.12.0/fixtureqa.egg-info}/PKG-INFO +1 -1
  9. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/SOURCES.txt +3 -2
  10. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/pyproject.toml +1 -1
  11. fixtureqa-0.12.0/tests/test_fix_spec_tree.py +131 -0
  12. fixtureqa-0.11.0/fixture/static/assets/index-CfkKS026.js +0 -102
  13. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/LICENSE +0 -0
  14. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/README.md +0 -0
  15. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/__init__.py +0 -0
  16. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/__main__.py +0 -0
  17. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/__init__.py +0 -0
  18. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/app.py +0 -0
  19. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/connection_manager.py +0 -0
  20. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/deps.py +0 -0
  21. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/__init__.py +0 -0
  22. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/admin.py +0 -0
  23. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/auth.py +0 -0
  24. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/branding.py +0 -0
  25. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/custom_tags.py +0 -0
  26. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/messages.py +0 -0
  27. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/perf.py +0 -0
  28. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/scenarios.py +0 -0
  29. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/sessions.py +0 -0
  30. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/setup.py +0 -0
  31. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/spec_overlay.py +0 -0
  32. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/specs.py +0 -0
  33. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/templates.py +0 -0
  34. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/api/routers/ws.py +0 -0
  35. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/config/__init__.py +0 -0
  36. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/__init__.py +0 -0
  37. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/atomic_io.py +0 -0
  38. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/auth.py +0 -0
  39. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/config_store.py +0 -0
  40. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/custom_tag_store.py +0 -0
  41. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/db_migrations.py +0 -0
  42. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/events.py +0 -0
  43. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_application.py +0 -0
  44. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_builder.py +0 -0
  45. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_parser.py +0 -0
  46. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_tags.py +0 -0
  47. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/fix_time.py +0 -0
  48. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/housekeeping.py +0 -0
  49. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/inbound.py +0 -0
  50. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/json_store.py +0 -0
  51. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/latency_csv_writer.py +0 -0
  52. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/message_log.py +0 -0
  53. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/message_store.py +0 -0
  54. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/models.py +0 -0
  55. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_charts.py +0 -0
  56. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_engine.py +0 -0
  57. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_models.py +0 -0
  58. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_payload.py +0 -0
  59. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_stats.py +0 -0
  60. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_store.py +0 -0
  61. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/perf_writer.py +0 -0
  62. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/scenario_runner.py +0 -0
  63. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/scenario_store.py +0 -0
  64. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/session.py +0 -0
  65. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/session_manager.py +0 -0
  66. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/spec_overlay_store.py +0 -0
  67. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/spec_store.py +0 -0
  68. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/template_store.py +0 -0
  69. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/user_store.py +0 -0
  70. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/value_gen.py +0 -0
  71. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/core/venue_responses.py +0 -0
  72. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/server.py +0 -0
  73. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  74. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  75. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  76. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/static/favicon.svg +0 -0
  77. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixture/ui/__init__.py +0 -0
  78. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
  79. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/entry_points.txt +0 -0
  80. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/requires.txt +0 -0
  81. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/fixtureqa.egg-info/top_level.txt +0 -0
  82. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/setup.cfg +0 -0
  83. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_atomic_io.py +0 -0
  84. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_auth.py +0 -0
  85. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_config_store.py +0 -0
  86. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_connection_manager.py +0 -0
  87. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_db_migrations.py +0 -0
  88. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_fix_builder.py +0 -0
  89. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_health.py +0 -0
  90. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_inbound.py +0 -0
  91. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_inbound_validation.py +0 -0
  92. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_message_store.py +0 -0
  93. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_non_stop_session.py +0 -0
  94. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_api.py +0 -0
  95. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_engine.py +0 -0
  96. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_models.py +0 -0
  97. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_payload.py +0 -0
  98. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_perf_rehydrate.py +0 -0
  99. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_scenarios.py +0 -0
  100. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_session_auto_response.py +0 -0
  101. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_session_lifecycle.py +0 -0
  102. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_session_manager_concurrency.py +0 -0
  103. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_sessions.py +0 -0
  104. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_specs.py +0 -0
  105. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_templates.py +0 -0
  106. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_timezone_data.py +0 -0
  107. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_value_gen.py +0 -0
  108. {fixtureqa-0.11.0 → fixtureqa-0.12.0}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -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, get_messages, spec_ref_for_version,
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 FieldRefResponse, MessageDefResponse, MessageSummaryResponse
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
- return _Spec(field_defs=field_defs, messages=messages)
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