fixtureqa 0.7.0__tar.gz → 0.8.1__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 (106) hide show
  1. {fixtureqa-0.7.0/fixtureqa.egg-info → fixtureqa-0.8.1}/PKG-INFO +3 -2
  2. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/sessions.py +18 -0
  3. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/schemas.py +54 -0
  4. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/models.py +59 -1
  5. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/session.py +47 -6
  6. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/session_manager.py +10 -0
  7. fixtureqa-0.8.1/fixture/static/assets/index-Cu3H3T9B.js +102 -0
  8. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/static/index.html +1 -1
  9. {fixtureqa-0.7.0 → fixtureqa-0.8.1/fixtureqa.egg-info}/PKG-INFO +3 -2
  10. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixtureqa.egg-info/SOURCES.txt +3 -1
  11. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixtureqa.egg-info/requires.txt +2 -1
  12. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/pyproject.toml +7 -2
  13. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_config_store.py +20 -0
  14. fixtureqa-0.8.1/tests/test_non_stop_session.py +177 -0
  15. fixtureqa-0.8.1/tests/test_timezone_data.py +39 -0
  16. fixtureqa-0.7.0/fixture/static/assets/index-BJKwfdd8.js +0 -102
  17. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/LICENSE +0 -0
  18. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/README.md +0 -0
  19. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/__init__.py +0 -0
  20. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/__main__.py +0 -0
  21. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/__init__.py +0 -0
  22. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/app.py +0 -0
  23. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/connection_manager.py +0 -0
  24. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/deps.py +0 -0
  25. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/__init__.py +0 -0
  26. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/admin.py +0 -0
  27. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/auth.py +0 -0
  28. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/branding.py +0 -0
  29. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/custom_tags.py +0 -0
  30. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/fix_spec.py +0 -0
  31. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/messages.py +0 -0
  32. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/perf.py +0 -0
  33. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/scenarios.py +0 -0
  34. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/setup.py +0 -0
  35. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/spec_overlay.py +0 -0
  36. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/templates.py +0 -0
  37. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/api/routers/ws.py +0 -0
  38. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/config/__init__.py +0 -0
  39. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/__init__.py +0 -0
  40. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/atomic_io.py +0 -0
  41. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/auth.py +0 -0
  42. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/config_store.py +0 -0
  43. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/custom_tag_store.py +0 -0
  44. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/db_migrations.py +0 -0
  45. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/events.py +0 -0
  46. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/fix_application.py +0 -0
  47. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/fix_builder.py +0 -0
  48. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/fix_parser.py +0 -0
  49. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/fix_spec_parser.py +0 -0
  50. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/fix_tags.py +0 -0
  51. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/fix_time.py +0 -0
  52. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/housekeeping.py +0 -0
  53. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/inbound.py +0 -0
  54. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/json_store.py +0 -0
  55. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/latency_csv_writer.py +0 -0
  56. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/message_log.py +0 -0
  57. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/message_store.py +0 -0
  58. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/perf_charts.py +0 -0
  59. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/perf_engine.py +0 -0
  60. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/perf_models.py +0 -0
  61. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/perf_payload.py +0 -0
  62. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/perf_stats.py +0 -0
  63. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/perf_store.py +0 -0
  64. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/perf_writer.py +0 -0
  65. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/scenario_runner.py +0 -0
  66. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/scenario_store.py +0 -0
  67. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/spec_overlay_store.py +0 -0
  68. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/template_store.py +0 -0
  69. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/user_store.py +0 -0
  70. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/value_gen.py +0 -0
  71. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/core/venue_responses.py +0 -0
  72. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/fix_specs/FIX42.xml +0 -0
  73. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/fix_specs/FIX44.xml +0 -0
  74. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/server.py +0 -0
  75. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  76. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/static/assets/index-B-hpSZWZ.css +0 -0
  77. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  78. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  79. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/static/favicon.svg +0 -0
  80. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixture/ui/__init__.py +0 -0
  81. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixtureqa.egg-info/dependency_links.txt +0 -0
  82. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixtureqa.egg-info/entry_points.txt +0 -0
  83. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/fixtureqa.egg-info/top_level.txt +0 -0
  84. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/setup.cfg +0 -0
  85. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_atomic_io.py +0 -0
  86. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_auth.py +0 -0
  87. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_connection_manager.py +0 -0
  88. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_db_migrations.py +0 -0
  89. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_fix_builder.py +0 -0
  90. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_health.py +0 -0
  91. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_inbound.py +0 -0
  92. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_inbound_validation.py +0 -0
  93. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_message_store.py +0 -0
  94. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_perf_api.py +0 -0
  95. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_perf_engine.py +0 -0
  96. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_perf_models.py +0 -0
  97. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_perf_payload.py +0 -0
  98. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_perf_rehydrate.py +0 -0
  99. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_scenarios.py +0 -0
  100. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_session_auto_response.py +0 -0
  101. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_session_lifecycle.py +0 -0
  102. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_session_manager_concurrency.py +0 -0
  103. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_sessions.py +0 -0
  104. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_templates.py +0 -0
  105. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_value_gen.py +0 -0
  106. {fixtureqa-0.7.0 → fixtureqa-0.8.1}/tests/test_ws.py +0 -0
@@ -1,16 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.7.0
3
+ Version: 0.8.1
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
7
- Requires-Dist: fixcore-engine==0.6.1
7
+ Requires-Dist: fixcore-engine==0.7.0
8
8
  Requires-Dist: fastapi>=0.111.0
9
9
  Requires-Dist: uvicorn[standard]>=0.29.0
10
10
  Requires-Dist: websockets>=12.0
11
11
  Requires-Dist: python-jose[cryptography]>=3.3.0
12
12
  Requires-Dist: passlib[bcrypt]>=1.7.4
13
13
  Requires-Dist: matplotlib>=3.7
14
+ Requires-Dist: tzdata>=2024.1
14
15
  Provides-Extra: dev
15
16
  Requires-Dist: pytest>=8.0; extra == "dev"
16
17
  Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
@@ -54,19 +54,37 @@ def _session_response(sm: SessionManager, session_id: str, us: Optional[UserStor
54
54
  begin_string=cfg.begin_string,
55
55
  sender_comp_id=cfg.sender_comp_id,
56
56
  target_comp_id=cfg.target_comp_id,
57
+ session_qualifier=cfg.session_qualifier,
57
58
  connection_type=cfg.connection_type.value,
58
59
  host=cfg.host,
59
60
  port=cfg.port,
61
+ socket_accept_host=cfg.socket_accept_host,
60
62
  heartbeat_interval=cfg.heartbeat_interval,
61
63
  reconnect_interval=cfg.reconnect_interval,
64
+ logon_timeout=cfg.logon_timeout,
65
+ logout_timeout=cfg.logout_timeout,
66
+ check_latency=cfg.check_latency,
67
+ max_latency=cfg.max_latency,
68
+ requires_orig_sending_time=cfg.requires_orig_sending_time,
69
+ resend_request_chunk_size=cfg.resend_request_chunk_size,
70
+ send_redundant_resend_requests=cfg.send_redundant_resend_requests,
62
71
  use_file_log=cfg.use_file_log,
63
72
  start_time=cfg.start_time,
64
73
  end_time=cfg.end_time,
74
+ start_day=cfg.start_day,
75
+ end_day=cfg.end_day,
76
+ weekdays=cfg.weekdays,
77
+ time_zone=cfg.time_zone,
78
+ non_stop_session=cfg.non_stop_session,
65
79
  reset_on_logon=cfg.reset_on_logon,
66
80
  reset_on_logout=cfg.reset_on_logout,
81
+ reset_on_disconnect=cfg.reset_on_disconnect,
67
82
  durable_seqnums=cfg.durable_seqnums,
68
83
  log_messages=cfg.log_messages,
69
84
  validate_inbound=cfg.validate_inbound,
85
+ validate_fields_have_values=cfg.validate_fields_have_values,
86
+ validate_user_defined_fields=cfg.validate_user_defined_fields,
87
+ validate_fields_out_of_range=cfg.validate_fields_out_of_range,
70
88
  session_role=cfg.session_role.value,
71
89
  auto_ack=cfg.auto_ack,
72
90
  auto_ack_delay_ms=cfg.auto_ack_delay_ms,
@@ -7,20 +7,38 @@ class SessionConfigRequest(BaseModel):
7
7
  begin_string: str = "FIX.4.2"
8
8
  sender_comp_id: str
9
9
  target_comp_id: str
10
+ session_qualifier: str = "" # SessionQualifier (disambiguate same triple)
10
11
  connection_type: str = "initiator" # "initiator" or "acceptor"
11
12
  host: str = "localhost"
12
13
  port: int = Field(default=9876, ge=1, le=65535)
14
+ socket_accept_host: str = "" # SocketAcceptHost bind addr ("" = 0.0.0.0)
13
15
  heartbeat_interval: int = Field(default=30, ge=1)
14
16
  reconnect_interval: int = Field(default=10, ge=1)
17
+ logon_timeout: int = Field(default=10, ge=1)
18
+ logout_timeout: int = Field(default=10, ge=1)
19
+ check_latency: bool = False # CheckLatency: reject stale SendingTime
20
+ max_latency: int = Field(default=120, ge=1) # MaxLatency (s) when check_latency
21
+ requires_orig_sending_time: bool = True # RequiresOrigSendingTime
22
+ resend_request_chunk_size: int = Field(default=0, ge=0) # 0 = unlimited
23
+ send_redundant_resend_requests: bool = False
15
24
  data_dictionary_path: Optional[str] = None
16
25
  use_file_log: bool = True
17
26
  start_time: str = "00:00:00"
18
27
  end_time: str = "00:00:00"
28
+ start_day: str = "" # StartDay/EndDay together = weekly window
29
+ end_day: str = ""
30
+ weekdays: str = "" # e.g. "Mon,Tue,Wed,Thu,Fri" (excl. start/end day)
31
+ time_zone: str = "" # IANA name; "" = UTC
32
+ non_stop_session: bool = False # True = NonStopSession=Y (no daily seqnum reset)
19
33
  reset_on_logon: bool = False
20
34
  reset_on_logout: bool = False
35
+ reset_on_disconnect: bool = False
21
36
  durable_seqnums: bool = True # False = in-memory store (no per-msg fsync; high-rate)
22
37
  log_messages: bool = True # False = in-memory log only (no per-msg SQLite; high-rate)
23
38
  validate_inbound: bool = False # True = reject non-conforming inbound (conformant-venue mode)
39
+ validate_fields_have_values: bool = True # reject "55=" (empty value)
40
+ validate_user_defined_fields: bool = False # reject unknown tag>=5000
41
+ validate_fields_out_of_range: bool = False # reject bad enums (e.g. Side=Z)
24
42
  session_role: str = "generic" # "client", "venue", or "generic"
25
43
  auto_ack: bool = False
26
44
  auto_ack_delay_ms: int = Field(default=100, ge=0)
@@ -35,19 +53,37 @@ class SessionResponse(BaseModel):
35
53
  begin_string: str
36
54
  sender_comp_id: str
37
55
  target_comp_id: str
56
+ session_qualifier: str
38
57
  connection_type: str
39
58
  host: str
40
59
  port: int
60
+ socket_accept_host: str
41
61
  heartbeat_interval: int
42
62
  reconnect_interval: int
63
+ logon_timeout: int
64
+ logout_timeout: int
65
+ check_latency: bool
66
+ max_latency: int
67
+ requires_orig_sending_time: bool
68
+ resend_request_chunk_size: int
69
+ send_redundant_resend_requests: bool
43
70
  use_file_log: bool
44
71
  start_time: str
45
72
  end_time: str
73
+ start_day: str
74
+ end_day: str
75
+ weekdays: str
76
+ time_zone: str
77
+ non_stop_session: bool
46
78
  reset_on_logon: bool
47
79
  reset_on_logout: bool
80
+ reset_on_disconnect: bool
48
81
  durable_seqnums: bool
49
82
  log_messages: bool
50
83
  validate_inbound: bool
84
+ validate_fields_have_values: bool
85
+ validate_user_defined_fields: bool
86
+ validate_fields_out_of_range: bool
51
87
  session_role: str
52
88
  auto_ack: bool
53
89
  auto_ack_delay_ms: int
@@ -68,15 +104,29 @@ class UpdateSessionRequest(BaseModel):
68
104
  begin_string: Optional[str] = None
69
105
  sender_comp_id: Optional[str] = None
70
106
  target_comp_id: Optional[str] = None
107
+ session_qualifier: Optional[str] = None
71
108
  connection_type: Optional[str] = None
72
109
  host: Optional[str] = None
73
110
  port: Optional[int] = Field(default=None, ge=1, le=65535)
111
+ socket_accept_host: Optional[str] = None
74
112
  heartbeat_interval: Optional[int] = Field(default=None, ge=1)
75
113
  reconnect_interval: Optional[int] = Field(default=None, ge=1)
114
+ logon_timeout: Optional[int] = Field(default=None, ge=1)
115
+ logout_timeout: Optional[int] = Field(default=None, ge=1)
116
+ check_latency: Optional[bool] = None
117
+ max_latency: Optional[int] = Field(default=None, ge=1)
118
+ requires_orig_sending_time: Optional[bool] = None
119
+ resend_request_chunk_size: Optional[int] = Field(default=None, ge=0)
120
+ send_redundant_resend_requests: Optional[bool] = None
76
121
  data_dictionary_path: Optional[str] = None
77
122
  use_file_log: Optional[bool] = None
78
123
  start_time: Optional[str] = None
79
124
  end_time: Optional[str] = None
125
+ start_day: Optional[str] = None
126
+ end_day: Optional[str] = None
127
+ weekdays: Optional[str] = None
128
+ time_zone: Optional[str] = None
129
+ non_stop_session: Optional[bool] = None
80
130
  session_role: Optional[str] = None
81
131
  auto_ack: Optional[bool] = None
82
132
  auto_ack_delay_ms: Optional[int] = Field(default=None, ge=0)
@@ -84,9 +134,13 @@ class UpdateSessionRequest(BaseModel):
84
134
  auto_fill_delay_ms: Optional[int] = Field(default=None, ge=0)
85
135
  reset_on_logon: Optional[bool] = None
86
136
  reset_on_logout: Optional[bool] = None
137
+ reset_on_disconnect: Optional[bool] = None
87
138
  durable_seqnums: Optional[bool] = None
88
139
  log_messages: Optional[bool] = None
89
140
  validate_inbound: Optional[bool] = None
141
+ validate_fields_have_values: Optional[bool] = None
142
+ validate_user_defined_fields: Optional[bool] = None
143
+ validate_fields_out_of_range: Optional[bool] = None
90
144
 
91
145
 
92
146
  class MessageEntryResponse(BaseModel):
@@ -39,12 +39,28 @@ class SessionConfig:
39
39
  sender_comp_id: str = ""
40
40
  target_comp_id: str = ""
41
41
 
42
+ # Disambiguates two sessions sharing the same BeginString/Sender/Target
43
+ # triple (fixcore SessionQualifier). "" = none.
44
+ session_qualifier: str = ""
45
+
42
46
  # Connection
43
47
  connection_type: ConnectionType = ConnectionType.INITIATOR
44
48
  host: str = "localhost" # ignored for acceptor
45
49
  port: int = 9876
46
50
  heartbeat_interval: int = 30
47
51
  reconnect_interval: int = 10 # initiator only
52
+ # Acceptor bind address (fixcore SocketAcceptHost). "" = 0.0.0.0 (all
53
+ # interfaces). Ignored for an initiator.
54
+ socket_accept_host: str = ""
55
+
56
+ # Liveness timers
57
+ logon_timeout: int = 10 # LogonTimeout (s): wait for Logon to complete
58
+ logout_timeout: int = 10 # LogoutTimeout (s): wait for Logout confirmation
59
+ # Reject inbound messages whose SendingTime(52) skews more than max_latency
60
+ # seconds from the local clock (fixcore CheckLatency). Off by default
61
+ # (fixcore's default; QuickFIX defaults this on). PossDup resends are exempt.
62
+ check_latency: bool = False
63
+ max_latency: int = 120 # MaxLatency (s): allowed SendingTime skew when check_latency
48
64
 
49
65
  # Data dictionary (None = use bundled spec)
50
66
  data_dictionary_path: Optional[str] = None
@@ -54,13 +70,43 @@ class SessionConfig:
54
70
  file_log_path: str = "./log"
55
71
  use_file_log: bool = True
56
72
 
57
- # Session schedule ("00:00:00" for both = always active)
73
+ # Session schedule ("00:00:00" for both = always active that day, with a
74
+ # daily sequence-number reset at midnight — fixcore's default daily window).
58
75
  start_time: str = "00:00:00"
59
76
  end_time: str = "00:00:00"
60
77
 
78
+ # Disable the daily session window entirely: when True, the engine runs as a
79
+ # single never-ending session (fixcore NonStopSession=Y) and never resets
80
+ # sequence numbers on a day boundary. False (default) keeps the daily window
81
+ # above, which performs the standard midnight seqnum reset. NonStopSession=Y
82
+ # is mutually exclusive with StartTime/EndTime, so those keys are omitted
83
+ # when this is set (see Session._build_settings).
84
+ non_stop_session: bool = False
85
+
86
+ # Extended schedule (fixcore SessionSchedule). All optional; combine with
87
+ # start_time/end_time. start_day+end_day => a *weekly* window (both required
88
+ # together). weekdays (e.g. "Mon,Tue,Wed,Thu,Fri") restricts the daily
89
+ # window to those days and is mutually exclusive with start_day/end_day.
90
+ # time_zone is an IANA name ("" = UTC). All ignored when non_stop_session.
91
+ start_day: str = ""
92
+ end_day: str = ""
93
+ weekdays: str = ""
94
+ time_zone: str = ""
95
+
61
96
  # Sequence number reset behaviour
62
97
  reset_on_logon: bool = False
63
98
  reset_on_logout: bool = False
99
+ reset_on_disconnect: bool = False # ResetOnDisconnect: reset to 1 on every drop
100
+
101
+ # Resend / duplicate handling (fixcore)
102
+ # Reject a resent app message (PossDupFlag=Y) missing OrigSendingTime(122).
103
+ requires_orig_sending_time: bool = True
104
+ # Cap messages requested per outbound ResendRequest (0 = unlimited / one
105
+ # open-ended request); a positive value chunks recovery of a large gap.
106
+ resend_request_chunk_size: int = 0
107
+ # Re-send a ResendRequest for every out-of-order message while one is already
108
+ # outstanding (default N = re-request only when the gap advances).
109
+ send_redundant_resend_requests: bool = False
64
110
 
65
111
  # Seqnum/message store durability. True (default) = fixcore FileStore, which
66
112
  # fsyncs per message (durable resend/recovery, but caps throughput at disk
@@ -85,6 +131,18 @@ class SessionConfig:
85
131
  # on regardless of this flag.
86
132
  validate_inbound: bool = False
87
133
 
134
+ # Finer-grained validation, effective only while validate_inbound is on
135
+ # (mapped to the matching fixcore keys):
136
+ # validate_fields_have_values -> ValidateFieldsHaveValues (reject "55=")
137
+ # validate_user_defined_fields -> ValidateUserDefinedFields (reject unknown tag>=5000)
138
+ # validate_fields_out_of_range -> ValidateFieldsOutOfRange (reject bad enums,
139
+ # e.g. Side=Z). This is the fixcore-native key; it subsumes QuickFIX/n's
140
+ # inverse AllowUnknownEnumValues (=N), so that alias is not exposed
141
+ # separately — leaving this off is equivalent to AllowUnknownEnumValues=Y.
142
+ validate_fields_have_values: bool = True
143
+ validate_user_defined_fields: bool = False
144
+ validate_fields_out_of_range: bool = False
145
+
88
146
  # Role / intent
89
147
  session_role: SessionRole = SessionRole.GENERIC
90
148
 
@@ -340,15 +340,49 @@ class Session:
340
340
  os.path.join(self.config.file_log_path, self.config.session_id)
341
341
  )
342
342
 
343
+ yn = lambda b: "Y" if b else "N" # noqa: E731 — local boolean->FIX cfg
344
+
345
+ # Session schedule. NonStopSession=Y is mutually exclusive with *any*
346
+ # schedule key in fixcore (it raises on the combination), so emit either
347
+ # NonStopSession or the StartTime/EndTime window (+ optional refinements):
348
+ # - TimeZone scopes the times/days;
349
+ # - Weekdays restricts the daily window to listed days and is itself
350
+ # exclusive with StartDay/EndDay, so prefer it when both are set;
351
+ # - StartDay+EndDay (only together) make it a weekly window.
352
+ if self.config.non_stop_session:
353
+ schedule_lines = ["NonStopSession=Y"]
354
+ else:
355
+ schedule_lines = [
356
+ f"StartTime={self.config.start_time}",
357
+ f"EndTime={self.config.end_time}",
358
+ ]
359
+ if self.config.time_zone:
360
+ schedule_lines.append(f"TimeZone={self.config.time_zone}")
361
+ if self.config.weekdays:
362
+ schedule_lines.append(f"Weekdays={self.config.weekdays}")
363
+ elif self.config.start_day and self.config.end_day:
364
+ schedule_lines += [
365
+ f"StartDay={self.config.start_day}",
366
+ f"EndDay={self.config.end_day}",
367
+ ]
368
+
343
369
  lines = [
344
370
  "[DEFAULT]",
345
371
  f"ConnectionType={ct}",
346
372
  f"BeginString={self.config.begin_string}",
347
373
  f"SenderCompID={self.config.sender_comp_id}",
348
374
  f"TargetCompID={self.config.target_comp_id}",
375
+ *([f"SessionQualifier={self.config.session_qualifier}"]
376
+ if self.config.session_qualifier else []),
349
377
  f"HeartBtInt={self.config.heartbeat_interval}",
350
- f"StartTime={self.config.start_time}",
351
- f"EndTime={self.config.end_time}",
378
+ f"LogonTimeout={self.config.logon_timeout}",
379
+ f"LogoutTimeout={self.config.logout_timeout}",
380
+ f"CheckLatency={yn(self.config.check_latency)}",
381
+ f"MaxLatency={self.config.max_latency}",
382
+ f"RequiresOrigSendingTime={yn(self.config.requires_orig_sending_time)}",
383
+ f"ResendRequestChunkSize={self.config.resend_request_chunk_size}",
384
+ f"SendRedundantResendRequests={yn(self.config.send_redundant_resend_requests)}",
385
+ *schedule_lines,
352
386
  f"DataDictionary={dd_path}",
353
387
  # Always load the dictionary so inbound repeating groups decode into
354
388
  # proper instances (otherwise duplicate group-member tags collapse to
@@ -357,12 +391,17 @@ class Session:
357
391
  "UseDataDictionary=Y",
358
392
  # Permissive by default: decode group-aware but don't auto-reject
359
393
  # non-conforming inbound. Flip on per session to behave like a
360
- # conformant counterparty. Requires fixcore >= 0.5.0.
361
- f"ValidateIncomingMessage={'Y' if self.config.validate_inbound else 'N'}",
394
+ # conformant counterparty. Requires fixcore >= 0.5.0. The finer
395
+ # toggles below only bite while ValidateIncomingMessage=Y.
396
+ f"ValidateIncomingMessage={yn(self.config.validate_inbound)}",
397
+ f"ValidateFieldsHaveValues={yn(self.config.validate_fields_have_values)}",
398
+ f"ValidateUserDefinedFields={yn(self.config.validate_user_defined_fields)}",
399
+ f"ValidateFieldsOutOfRange={yn(self.config.validate_fields_out_of_range)}",
362
400
  f"FileStorePath={store_path}",
363
401
  f"FileLogPath={log_path}",
364
- f"ResetOnLogon={'Y' if self.config.reset_on_logon else 'N'}",
365
- f"ResetOnLogout={'Y' if self.config.reset_on_logout else 'N'}",
402
+ f"ResetOnLogon={yn(self.config.reset_on_logon)}",
403
+ f"ResetOnLogout={yn(self.config.reset_on_logout)}",
404
+ f"ResetOnDisconnect={yn(self.config.reset_on_disconnect)}",
366
405
  "",
367
406
  "[SESSION]",
368
407
  ]
@@ -375,6 +414,8 @@ class Session:
375
414
  ]
376
415
  else:
377
416
  lines.append(f"SocketAcceptPort={self.config.port}")
417
+ if self.config.socket_accept_host:
418
+ lines.append(f"SocketAcceptHost={self.config.socket_accept_host}")
378
419
 
379
420
  return SessionSettings.from_string("\n".join(lines))
380
421
 
@@ -264,6 +264,16 @@ class SessionManager:
264
264
  raise ValueError("target_comp_id is required")
265
265
  if config.begin_string not in ("FIX.4.2", "FIX.4.4"):
266
266
  raise ValueError(f"Unsupported FIX version: {config.begin_string!r}")
267
+ # Schedule-combo rules (mirror fixcore so a bad combo fails on save with a
268
+ # clear message instead of at engine start). Only meaningful when a
269
+ # windowed schedule is in play, i.e. not non-stop.
270
+ if not config.non_stop_session:
271
+ if config.weekdays and (config.start_day or config.end_day):
272
+ raise ValueError("Weekdays cannot be combined with StartDay/EndDay")
273
+ if bool(config.start_day) != bool(config.end_day):
274
+ raise ValueError("StartDay and EndDay must both be set (weekly window)")
275
+ if config.check_latency and config.max_latency < 1:
276
+ raise ValueError("max_latency must be >= 1 when check_latency is on")
267
277
  from .models import ConnectionType
268
278
  if config.connection_type == ConnectionType.ACCEPTOR:
269
279
  for sid, s in self._sessions.items():