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.
Files changed (107) hide show
  1. {fixtureqa-0.9.8/fixtureqa.egg-info → fixtureqa-0.10.0}/PKG-INFO +1 -1
  2. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/fix_spec.py +28 -10
  3. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/sessions.py +15 -5
  4. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/specs.py +3 -3
  5. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/schemas.py +6 -0
  6. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_spec_parser.py +50 -10
  7. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/models.py +18 -3
  8. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/session.py +54 -8
  9. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/session_manager.py +15 -1
  10. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/spec_store.py +21 -12
  11. fixtureqa-0.10.0/fixture/static/assets/index-DrDKHUxW.js +102 -0
  12. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/index.html +1 -1
  13. {fixtureqa-0.9.8 → fixtureqa-0.10.0/fixtureqa.egg-info}/PKG-INFO +1 -1
  14. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/SOURCES.txt +1 -1
  15. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/pyproject.toml +1 -1
  16. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_config_store.py +2 -0
  17. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_specs.py +8 -2
  18. fixtureqa-0.9.8/fixture/static/assets/index-B5sIIunI.js +0 -102
  19. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/LICENSE +0 -0
  20. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/README.md +0 -0
  21. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/__init__.py +0 -0
  22. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/__main__.py +0 -0
  23. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/__init__.py +0 -0
  24. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/app.py +0 -0
  25. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/connection_manager.py +0 -0
  26. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/deps.py +0 -0
  27. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/__init__.py +0 -0
  28. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/admin.py +0 -0
  29. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/auth.py +0 -0
  30. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/branding.py +0 -0
  31. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/custom_tags.py +0 -0
  32. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/messages.py +0 -0
  33. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/perf.py +0 -0
  34. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/scenarios.py +0 -0
  35. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/setup.py +0 -0
  36. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/spec_overlay.py +0 -0
  37. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/templates.py +0 -0
  38. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/api/routers/ws.py +0 -0
  39. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/config/__init__.py +0 -0
  40. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/__init__.py +0 -0
  41. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/atomic_io.py +0 -0
  42. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/auth.py +0 -0
  43. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/config_store.py +0 -0
  44. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/custom_tag_store.py +0 -0
  45. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/db_migrations.py +0 -0
  46. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/events.py +0 -0
  47. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_application.py +0 -0
  48. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_builder.py +0 -0
  49. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_parser.py +0 -0
  50. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_tags.py +0 -0
  51. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/fix_time.py +0 -0
  52. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/housekeeping.py +0 -0
  53. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/inbound.py +0 -0
  54. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/json_store.py +0 -0
  55. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/latency_csv_writer.py +0 -0
  56. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/message_log.py +0 -0
  57. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/message_store.py +0 -0
  58. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_charts.py +0 -0
  59. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_engine.py +0 -0
  60. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_models.py +0 -0
  61. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_payload.py +0 -0
  62. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_stats.py +0 -0
  63. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_store.py +0 -0
  64. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/perf_writer.py +0 -0
  65. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/scenario_runner.py +0 -0
  66. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/scenario_store.py +0 -0
  67. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/spec_overlay_store.py +0 -0
  68. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/template_store.py +0 -0
  69. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/user_store.py +0 -0
  70. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/value_gen.py +0 -0
  71. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/core/venue_responses.py +0 -0
  72. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/server.py +0 -0
  73. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  74. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/assets/index-B-hpSZWZ.css +0 -0
  75. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  76. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  77. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/static/favicon.svg +0 -0
  78. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixture/ui/__init__.py +0 -0
  79. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
  80. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/entry_points.txt +0 -0
  81. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/requires.txt +0 -0
  82. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/fixtureqa.egg-info/top_level.txt +0 -0
  83. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/setup.cfg +0 -0
  84. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_atomic_io.py +0 -0
  85. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_auth.py +0 -0
  86. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_connection_manager.py +0 -0
  87. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_db_migrations.py +0 -0
  88. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_fix_builder.py +0 -0
  89. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_health.py +0 -0
  90. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_inbound.py +0 -0
  91. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_inbound_validation.py +0 -0
  92. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_message_store.py +0 -0
  93. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_non_stop_session.py +0 -0
  94. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_api.py +0 -0
  95. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_engine.py +0 -0
  96. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_models.py +0 -0
  97. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_payload.py +0 -0
  98. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_perf_rehydrate.py +0 -0
  99. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_scenarios.py +0 -0
  100. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_session_auto_response.py +0 -0
  101. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_session_lifecycle.py +0 -0
  102. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_session_manager_concurrency.py +0 -0
  103. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_sessions.py +0 -0
  104. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_templates.py +0 -0
  105. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_timezone_data.py +0 -0
  106. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_value_gen.py +0 -0
  107. {fixtureqa-0.9.8 → fixtureqa-0.10.0}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.9.8
3
+ Version: 0.10.0
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -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
- router = APIRouter(tags=["fix-spec"])
14
+ from fixcore.message import bundled_app_specs, bundled_begin_strings
15
15
 
16
- SUPPORTED = {"FIX.4.2", "FIX.4.4"}
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 SUPPORTED:
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(SUPPORTED)}",
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, or a
36
- custom upload the caller owns) overrides the version-derived bundled spec.
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 in BUNDLED:
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
- # Bundled spec names fixcore resolves by itself (see core.spec_store).
21
- _BUNDLED_SPECS = {"FIX42.xml", "FIX44.xml"}
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
- if not value or value in _BUNDLED_SPECS:
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="data_dictionary_path must be a bundled spec name or one of your uploaded specs",
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 (FIX42.xml / FIX44.xml) ship inside fixcore; this router manages
4
- the per-user *custom* uploads and lists both together so the session builder can
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 0.9.x bundles the FIX 4.2 /
6
- 4.4 dictionaries and (crucially) expands ``<component>`` references, so the
7
- field lists here are complete for FIX 4.4 (NewOrderSingle's Instrument /
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 fixcore's ``load_data_dictionary`` accepts: a bundled
12
- spec name (``"FIX42.xml"`` / ``"FIX44.xml"``) or an absolute path to a custom
13
- uploaded spec.
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 load_data_dictionary
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 bundled spec name for a FIX version (fixcore resolves it)."""
68
- return "FIX42.xml" if "4.2" in begin_string else "FIX44.xml"
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 = load_data_dictionary(spec_ref)
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
- begin_string: str = "FIX.4.2" # "FIX.4.2" or "FIX.4.4"
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
- f"DataDictionary={dd_path}",
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 this session — a bundled spec name that
425
- fixcore resolves from its packaged dictionaries, or an absolute path to
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 "FIX42.xml" if "4.2" in self.config.begin_string else "FIX44.xml"
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
- from fixcore.message import load_data_dictionary
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
- if config.begin_string not in ("FIX.4.2", "FIX.4.4"):
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.2 / 4.4 dictionaries are *not* stored here — they ship inside
10
- fixcore (``fixcore.message.bundled_specs()``). :func:`SpecStore.list_specs`
11
- returns the bundled specs alongside the user's uploaded ones so the UI can show
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 bundled_specs, load_data_dictionary
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
- ``value`` is what gets stored in ``SessionConfig.data_dictionary_path``:
89
- a bare bundled name (e.g. ``"FIX44.xml"``, resolved by fixcore) for
90
- bundled specs, and an absolute path for custom ones.
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 bundled_specs()]
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)