mkfix 0.1.0__py3-none-any.whl
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.
- mkfix/__init__.py +8 -0
- mkfix/__main__.py +186 -0
- mkfix/fix/__init__.py +0 -0
- mkfix/fix/dictionary.py +62 -0
- mkfix/fix/dictionary_data/FIX42.json +1365 -0
- mkfix/fix/engine.py +627 -0
- mkfix/fix/message.py +287 -0
- mkfix/fix/parser.py +81 -0
- mkfix/fix/replay.py +182 -0
- mkfix/fix/session.py +313 -0
- mkfix/fix/transport.py +201 -0
- mkfix/mkfix.toml +132 -0
- mkfix/services/__init__.py +0 -0
- mkfix/services/fix_command.py +124 -0
- mkfix/static/app.json +155 -0
- mkfix/static/fix-dictionary.js +1126 -0
- mkfix/static/fix-formatter.js +82 -0
- mkfix/static/index.html +28 -0
- mkfix/static/mkfix.css +343 -0
- mkfix/static/panes/message-detail.js +69 -0
- mkfix/static/panes/order-pad.js +228 -0
- mkfix/static/panes/raw-messages.js +162 -0
- mkfix/static/panes/replay-control.js +189 -0
- mkfix/static/panes/session-manager.js +279 -0
- mkfix/static/panes/translated-messages.js +123 -0
- mkfix/static/widgets/form.js +140 -0
- mkfix-0.1.0.dist-info/METADATA +113 -0
- mkfix-0.1.0.dist-info/RECORD +31 -0
- mkfix-0.1.0.dist-info/WHEEL +4 -0
- mkfix-0.1.0.dist-info/entry_points.txt +2 -0
- mkfix-0.1.0.dist-info/licenses/LICENSE +338 -0
mkfix/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""mkfix — FIX protocol testing engine built on mkio and mkui."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def serve(config="mkfix.toml", host=None, port=None, db_path=None):
|
|
7
|
+
from mkfix.__main__ import serve as _serve
|
|
8
|
+
_serve(config, host=host, port=port, db_path=db_path)
|
mkfix/__main__.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""mkfix CLI and server entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from aiohttp import web
|
|
12
|
+
|
|
13
|
+
from mkfix import __version__
|
|
14
|
+
from mkio.config import load_config
|
|
15
|
+
from mkio.server import (
|
|
16
|
+
_on_startup as mkio_on_startup,
|
|
17
|
+
_on_shutdown as mkio_on_shutdown,
|
|
18
|
+
_api_services,
|
|
19
|
+
_api_service_detail,
|
|
20
|
+
_ws_handler,
|
|
21
|
+
_make_index_handler,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from mkfix.fix.engine import FixEngine
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def serve(
|
|
28
|
+
config: str | Path | dict[str, Any] = "mkfix.toml",
|
|
29
|
+
host: str | None = None,
|
|
30
|
+
port: int | None = None,
|
|
31
|
+
db_path: str | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Start the mkfix server. Blocks until shutdown."""
|
|
34
|
+
cfg = _load_config(config)
|
|
35
|
+
if host is not None:
|
|
36
|
+
cfg["host"] = host
|
|
37
|
+
if port is not None:
|
|
38
|
+
cfg["port"] = port
|
|
39
|
+
if db_path is not None:
|
|
40
|
+
cfg["db_path"] = db_path
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import uvloop
|
|
44
|
+
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
45
|
+
except ImportError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
app = web.Application()
|
|
49
|
+
app["config"] = cfg
|
|
50
|
+
|
|
51
|
+
app.on_startup.append(mkio_on_startup)
|
|
52
|
+
app.on_startup.append(_start_fix_engine)
|
|
53
|
+
app.on_shutdown.append(_stop_fix_engine)
|
|
54
|
+
app.on_shutdown.append(mkio_on_shutdown)
|
|
55
|
+
|
|
56
|
+
# API routes
|
|
57
|
+
app.router.add_get("/api/services", _api_services)
|
|
58
|
+
app.router.add_get("/api/services/{service_name}", _api_service_detail)
|
|
59
|
+
|
|
60
|
+
# WebSocket routes
|
|
61
|
+
app.router.add_get("/ws", _ws_handler)
|
|
62
|
+
app.router.add_get("/ws/{service_name}", _ws_handler)
|
|
63
|
+
|
|
64
|
+
# Serve mkio.js client library
|
|
65
|
+
js_path = Path(__import__("mkio").__file__).parent / "client" / "mkio.js"
|
|
66
|
+
if js_path.exists():
|
|
67
|
+
async def serve_js(request: web.Request) -> web.FileResponse:
|
|
68
|
+
return web.FileResponse(
|
|
69
|
+
js_path, headers={"Content-Type": "application/javascript"}
|
|
70
|
+
)
|
|
71
|
+
app.router.add_get("/mkio.js", serve_js)
|
|
72
|
+
|
|
73
|
+
# Static file routes — register "/" last so explicit routes take priority
|
|
74
|
+
deferred_root = None
|
|
75
|
+
for route, directory in cfg.get("static", {}).items():
|
|
76
|
+
path = Path(directory).resolve()
|
|
77
|
+
if route == "/":
|
|
78
|
+
deferred_root = path
|
|
79
|
+
else:
|
|
80
|
+
app.router.add_static(route, path)
|
|
81
|
+
|
|
82
|
+
if deferred_root is not None:
|
|
83
|
+
app.router.add_get("/", _make_index_handler(deferred_root))
|
|
84
|
+
app.router.add_static("/", deferred_root)
|
|
85
|
+
|
|
86
|
+
web.run_app(
|
|
87
|
+
app,
|
|
88
|
+
host=cfg.get("host", "0.0.0.0"),
|
|
89
|
+
port=cfg.get("port", 8080),
|
|
90
|
+
shutdown_timeout=cfg.get("shutdown_timeout", 0),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _load_config(config: str | Path | dict[str, Any]) -> dict[str, Any]:
|
|
95
|
+
"""Load config, resolving mkui static path and relative directories."""
|
|
96
|
+
config_dir = Path(config).parent.resolve() if isinstance(config, (str, Path)) else Path.cwd()
|
|
97
|
+
cfg = load_config(config)
|
|
98
|
+
|
|
99
|
+
statics = cfg.get("static", {})
|
|
100
|
+
for route, directory in list(statics.items()):
|
|
101
|
+
if directory == "__mkui__":
|
|
102
|
+
import mkui
|
|
103
|
+
statics[route] = str(mkui.static_dir)
|
|
104
|
+
else:
|
|
105
|
+
resolved = (config_dir / directory).resolve()
|
|
106
|
+
statics[route] = str(resolved)
|
|
107
|
+
|
|
108
|
+
return cfg
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def _start_fix_engine(app: web.Application) -> None:
|
|
112
|
+
"""Create and start the FIX engine after mkio services are ready."""
|
|
113
|
+
engine = FixEngine(
|
|
114
|
+
db=app["db"],
|
|
115
|
+
writer=app["writer"],
|
|
116
|
+
bus=app["bus"],
|
|
117
|
+
)
|
|
118
|
+
app["fix_engine"] = engine
|
|
119
|
+
|
|
120
|
+
# Wire the engine into the FixCommandService
|
|
121
|
+
from mkfix.services.fix_command import FixCommandService
|
|
122
|
+
for svc in app["services"].values():
|
|
123
|
+
if isinstance(svc, FixCommandService):
|
|
124
|
+
svc.set_engine(engine)
|
|
125
|
+
|
|
126
|
+
await engine.start()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def _stop_fix_engine(app: web.Application) -> None:
|
|
130
|
+
"""Stop the FIX engine before mkio shutdown."""
|
|
131
|
+
engine = app.get("fix_engine")
|
|
132
|
+
if engine:
|
|
133
|
+
await engine.stop()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main() -> None:
|
|
137
|
+
"""CLI entry point."""
|
|
138
|
+
parser = argparse.ArgumentParser(
|
|
139
|
+
prog="mkfix",
|
|
140
|
+
description="FIX protocol testing engine built on mkio and mkui",
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"config", nargs="?", default=None,
|
|
144
|
+
help="path to mkfix.toml config file (default: auto-detect)",
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"-p", "--port", type=int, default=None,
|
|
148
|
+
help="override listening port",
|
|
149
|
+
)
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"--host", default=None,
|
|
152
|
+
help="override listening host",
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"-d", "--db", default=None, metavar="PATH",
|
|
156
|
+
help="database filename (.db added if no extension; use ':memory:' for in-memory)",
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--version", action="version", version=f"mkfix {__version__}",
|
|
160
|
+
)
|
|
161
|
+
args = parser.parse_args()
|
|
162
|
+
|
|
163
|
+
db_path = args.db
|
|
164
|
+
if db_path is not None and db_path != ":memory:" and not Path(db_path).suffix:
|
|
165
|
+
db_path += ".db"
|
|
166
|
+
|
|
167
|
+
config_path = args.config or _find_config()
|
|
168
|
+
serve(config_path, host=args.host, port=args.port, db_path=db_path)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _find_config() -> str:
|
|
172
|
+
"""Look for mkfix.toml in current directory, then package directory."""
|
|
173
|
+
cwd = Path.cwd() / "mkfix.toml"
|
|
174
|
+
if cwd.exists():
|
|
175
|
+
return str(cwd)
|
|
176
|
+
|
|
177
|
+
pkg = Path(__file__).parent / "mkfix.toml"
|
|
178
|
+
if pkg.exists():
|
|
179
|
+
return str(pkg)
|
|
180
|
+
|
|
181
|
+
print("Error: mkfix.toml not found. Provide a config path as argument.", file=sys.stderr)
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
mkfix/fix/__init__.py
ADDED
|
File without changes
|
mkfix/fix/dictionary.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""FIX data dictionary: tag names, enum values, message types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
_DATA_DIR = Path(__file__).parent / "dictionary_data"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FixDictionary:
|
|
13
|
+
def __init__(self, version: str = "FIX.4.2"):
|
|
14
|
+
self.version = version
|
|
15
|
+
self._data = self._load(version)
|
|
16
|
+
self.fields: dict[str, dict[str, str]] = self._data.get("fields", {})
|
|
17
|
+
self.enums: dict[str, dict[str, str]] = self._data.get("enums", {})
|
|
18
|
+
self.messages: dict[str, dict[str, str]] = self._data.get("messages", {})
|
|
19
|
+
self.header_tags: list[str] = self._data.get("header", [])
|
|
20
|
+
self.trailer_tags: list[str] = self._data.get("trailer", [])
|
|
21
|
+
self._special_tags = {"8", "9", "10"}
|
|
22
|
+
self._header_set = set(self.header_tags)
|
|
23
|
+
self._trailer_set = set(self.trailer_tags)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _load(version: str) -> dict[str, Any]:
|
|
27
|
+
filename = version.replace(".", "") + ".json"
|
|
28
|
+
path = _DATA_DIR / filename
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return {"fields": {}, "enums": {}, "messages": {}, "header": [], "trailer": []}
|
|
31
|
+
with open(path) as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
|
|
34
|
+
def tag_name(self, tag: str) -> str:
|
|
35
|
+
info = self.fields.get(str(tag))
|
|
36
|
+
return info["name"] if info else str(tag)
|
|
37
|
+
|
|
38
|
+
def enum_name(self, tag: str, value: str) -> str:
|
|
39
|
+
tag_enums = self.enums.get(str(tag))
|
|
40
|
+
if tag_enums:
|
|
41
|
+
return tag_enums.get(str(value), str(value))
|
|
42
|
+
return str(value)
|
|
43
|
+
|
|
44
|
+
def msg_type_name(self, msg_type: str) -> str:
|
|
45
|
+
info = self.messages.get(msg_type)
|
|
46
|
+
return info["name"] if info else msg_type
|
|
47
|
+
|
|
48
|
+
def msg_category(self, msg_type: str) -> str:
|
|
49
|
+
info = self.messages.get(msg_type)
|
|
50
|
+
return info.get("category", "app").upper() if info else "APP"
|
|
51
|
+
|
|
52
|
+
def is_special(self, tag: str) -> bool:
|
|
53
|
+
return str(tag) in self._special_tags
|
|
54
|
+
|
|
55
|
+
def is_header(self, tag: str) -> bool:
|
|
56
|
+
return str(tag) in self._header_set
|
|
57
|
+
|
|
58
|
+
def is_trailer(self, tag: str) -> bool:
|
|
59
|
+
return str(tag) in self._trailer_set
|
|
60
|
+
|
|
61
|
+
def begin_string(self) -> str:
|
|
62
|
+
return self.version
|