fixtureqa 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.
- fixture/__init__.py +22 -0
- fixture/__main__.py +161 -0
- fixture/api/__init__.py +0 -0
- fixture/api/app.py +95 -0
- fixture/api/connection_manager.py +161 -0
- fixture/api/deps.py +73 -0
- fixture/api/routers/__init__.py +0 -0
- fixture/api/routers/admin.py +178 -0
- fixture/api/routers/auth.py +74 -0
- fixture/api/routers/branding.py +33 -0
- fixture/api/routers/fix_spec.py +41 -0
- fixture/api/routers/messages.py +137 -0
- fixture/api/routers/scenarios.py +65 -0
- fixture/api/routers/sessions.py +272 -0
- fixture/api/routers/setup.py +42 -0
- fixture/api/routers/templates.py +36 -0
- fixture/api/routers/ws.py +129 -0
- fixture/api/schemas.py +289 -0
- fixture/config/__init__.py +0 -0
- fixture/core/__init__.py +0 -0
- fixture/core/auth.py +68 -0
- fixture/core/config_store.py +85 -0
- fixture/core/events.py +22 -0
- fixture/core/fix_application.py +67 -0
- fixture/core/fix_parser.py +79 -0
- fixture/core/fix_spec_parser.py +172 -0
- fixture/core/fix_tags.py +297 -0
- fixture/core/housekeeping.py +107 -0
- fixture/core/message_log.py +115 -0
- fixture/core/message_store.py +246 -0
- fixture/core/models.py +87 -0
- fixture/core/scenario_runner.py +331 -0
- fixture/core/scenario_store.py +70 -0
- fixture/core/session.py +278 -0
- fixture/core/session_manager.py +173 -0
- fixture/core/template_store.py +70 -0
- fixture/core/user_store.py +186 -0
- fixture/core/venue_responses.py +94 -0
- fixture/fix_specs/FIX42.xml +2746 -0
- fixture/fix_specs/FIX44.xml +6593 -0
- fixture/server.py +37 -0
- fixture/static/assets/ag-grid-_QKprVdm.js +326 -0
- fixture/static/assets/index-B31-1dt-.css +1 -0
- fixture/static/assets/index-CTsKxGdI.js +87 -0
- fixture/static/assets/react-vendor-2eF0YfZT.js +1 -0
- fixture/static/favicon.svg +12 -0
- fixture/static/index.html +15 -0
- fixture/ui/__init__.py +0 -0
- fixtureqa-0.1.0.dist-info/METADATA +16 -0
- fixtureqa-0.1.0.dist-info/RECORD +54 -0
- fixtureqa-0.1.0.dist-info/WHEEL +5 -0
- fixtureqa-0.1.0.dist-info/entry_points.txt +2 -0
- fixtureqa-0.1.0.dist-info/licenses/LICENSE +21 -0
- fixtureqa-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite-backed user store.
|
|
3
|
+
|
|
4
|
+
Uses Python's built-in sqlite3 — no extra runtime dependency.
|
|
5
|
+
Thread-safe: every call opens and closes its own connection (sqlite3 handles
|
|
6
|
+
concurrent readers/single-writer serialisation via WAL mode).
|
|
7
|
+
"""
|
|
8
|
+
import sqlite3
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class User:
|
|
17
|
+
uid: str
|
|
18
|
+
username: str
|
|
19
|
+
password_hash: str
|
|
20
|
+
role: str # platform_admin | user_admin | user
|
|
21
|
+
created_at: float
|
|
22
|
+
is_active: bool
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def is_admin(self) -> bool:
|
|
26
|
+
return self.role == "platform_admin"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_DDL_USERS = """
|
|
30
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
31
|
+
uid TEXT PRIMARY KEY,
|
|
32
|
+
username TEXT UNIQUE NOT NULL,
|
|
33
|
+
password_hash TEXT NOT NULL,
|
|
34
|
+
role TEXT NOT NULL DEFAULT 'user',
|
|
35
|
+
created_at REAL NOT NULL,
|
|
36
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_DDL_SETTINGS = """
|
|
41
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
42
|
+
key TEXT PRIMARY KEY,
|
|
43
|
+
value TEXT NOT NULL
|
|
44
|
+
)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_DEFAULT_SETTINGS = {
|
|
48
|
+
"registration_enabled": "0",
|
|
49
|
+
"max_users": "50",
|
|
50
|
+
"housekeeping_enabled": "1",
|
|
51
|
+
"msg_retention_days": "90",
|
|
52
|
+
"log_retention_days": "30",
|
|
53
|
+
"branding_prefix": "", # e.g. "Google" → "Google FIXture"
|
|
54
|
+
"branding_accent": "", # --accent; "" = use CSS default
|
|
55
|
+
"branding_bg": "", # --bg (page background)
|
|
56
|
+
"branding_bg2": "", # --bg2 (header/panel background)
|
|
57
|
+
"branding_bg3": "", # --bg3 (card/input background)
|
|
58
|
+
"branding_border": "", # --border
|
|
59
|
+
"branding_text": "", # --text (primary text)
|
|
60
|
+
"branding_text2": "", # --text2 (secondary text)
|
|
61
|
+
"branding_logo_dark": "", # base64 data URI; "" = use bundled SVG
|
|
62
|
+
"branding_logo_light": "", # base64 data URI; "" = use bundled SVG
|
|
63
|
+
"branding_favicon": "", # base64 data URI; "" = use built-in favicon
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class UserStore:
|
|
68
|
+
def __init__(self, db_path: str):
|
|
69
|
+
self._path = db_path
|
|
70
|
+
self._init_db()
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Users
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def has_any_users(self) -> bool:
|
|
77
|
+
with self._connect() as conn:
|
|
78
|
+
row = conn.execute("SELECT 1 FROM users LIMIT 1").fetchone()
|
|
79
|
+
return row is not None
|
|
80
|
+
|
|
81
|
+
def count_users(self) -> int:
|
|
82
|
+
with self._connect() as conn:
|
|
83
|
+
row = conn.execute("SELECT COUNT(*) FROM users").fetchone()
|
|
84
|
+
return row[0]
|
|
85
|
+
|
|
86
|
+
def create_user(self, username: str, password_hash: str, role: str = "user") -> User:
|
|
87
|
+
uid = str(uuid.uuid4())
|
|
88
|
+
now = time.time()
|
|
89
|
+
with self._connect() as conn:
|
|
90
|
+
conn.execute(
|
|
91
|
+
"INSERT INTO users (uid, username, password_hash, role, created_at, is_active) "
|
|
92
|
+
"VALUES (?, ?, ?, ?, ?, 1)",
|
|
93
|
+
(uid, username, password_hash, role, now),
|
|
94
|
+
)
|
|
95
|
+
return User(uid=uid, username=username, password_hash=password_hash,
|
|
96
|
+
role=role, created_at=now, is_active=True)
|
|
97
|
+
|
|
98
|
+
def list_users(self) -> List[User]:
|
|
99
|
+
with self._connect() as conn:
|
|
100
|
+
rows = conn.execute(
|
|
101
|
+
"SELECT uid, username, password_hash, role, created_at, is_active "
|
|
102
|
+
"FROM users ORDER BY created_at ASC"
|
|
103
|
+
).fetchall()
|
|
104
|
+
return [_row_to_user(r) for r in rows]
|
|
105
|
+
|
|
106
|
+
def get_by_username(self, username: str) -> Optional[User]:
|
|
107
|
+
with self._connect() as conn:
|
|
108
|
+
row = conn.execute(
|
|
109
|
+
"SELECT uid, username, password_hash, role, created_at, is_active "
|
|
110
|
+
"FROM users WHERE username = ?", (username,)
|
|
111
|
+
).fetchone()
|
|
112
|
+
return _row_to_user(row) if row else None
|
|
113
|
+
|
|
114
|
+
def get_by_uid(self, uid: str) -> Optional[User]:
|
|
115
|
+
with self._connect() as conn:
|
|
116
|
+
row = conn.execute(
|
|
117
|
+
"SELECT uid, username, password_hash, role, created_at, is_active "
|
|
118
|
+
"FROM users WHERE uid = ?", (uid,)
|
|
119
|
+
).fetchone()
|
|
120
|
+
return _row_to_user(row) if row else None
|
|
121
|
+
|
|
122
|
+
def update_user(self, uid: str, **fields) -> Optional[User]:
|
|
123
|
+
"""Update allowed fields: is_active, role, password_hash, username."""
|
|
124
|
+
allowed = {"is_active", "role", "password_hash", "username"}
|
|
125
|
+
updates = {k: v for k, v in fields.items() if k in allowed}
|
|
126
|
+
if not updates:
|
|
127
|
+
return self.get_by_uid(uid)
|
|
128
|
+
cols = ", ".join(f"{k} = ?" for k in updates)
|
|
129
|
+
vals = list(updates.values()) + [uid]
|
|
130
|
+
with self._connect() as conn:
|
|
131
|
+
conn.execute(f"UPDATE users SET {cols} WHERE uid = ?", vals)
|
|
132
|
+
return self.get_by_uid(uid)
|
|
133
|
+
|
|
134
|
+
def delete_user(self, uid: str) -> None:
|
|
135
|
+
with self._connect() as conn:
|
|
136
|
+
conn.execute("DELETE FROM users WHERE uid = ?", (uid,))
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
# Settings
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def get_setting(self, key: str, default: str = "") -> str:
|
|
143
|
+
with self._connect() as conn:
|
|
144
|
+
row = conn.execute(
|
|
145
|
+
"SELECT value FROM settings WHERE key = ?", (key,)
|
|
146
|
+
).fetchone()
|
|
147
|
+
return row["value"] if row else default
|
|
148
|
+
|
|
149
|
+
def set_setting(self, key: str, value: str) -> None:
|
|
150
|
+
with self._connect() as conn:
|
|
151
|
+
conn.execute(
|
|
152
|
+
"INSERT INTO settings (key, value) VALUES (?, ?) "
|
|
153
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
154
|
+
(key, value),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# Internal
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _init_db(self) -> None:
|
|
162
|
+
with self._connect() as conn:
|
|
163
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
164
|
+
conn.execute(_DDL_USERS)
|
|
165
|
+
conn.execute(_DDL_SETTINGS)
|
|
166
|
+
# Seed defaults without overwriting existing values
|
|
167
|
+
for k, v in _DEFAULT_SETTINGS.items():
|
|
168
|
+
conn.execute(
|
|
169
|
+
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", (k, v)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _connect(self) -> sqlite3.Connection:
|
|
173
|
+
conn = sqlite3.connect(self._path)
|
|
174
|
+
conn.row_factory = sqlite3.Row
|
|
175
|
+
return conn
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _row_to_user(row: sqlite3.Row) -> User:
|
|
179
|
+
return User(
|
|
180
|
+
uid=row["uid"],
|
|
181
|
+
username=row["username"],
|
|
182
|
+
password_hash=row["password_hash"],
|
|
183
|
+
role=row["role"],
|
|
184
|
+
created_at=row["created_at"],
|
|
185
|
+
is_active=bool(row["is_active"]),
|
|
186
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build automatic Execution Report responses for venue sessions.
|
|
3
|
+
|
|
4
|
+
Both builders take:
|
|
5
|
+
order_fields — {tag_str: value_str} parsed from the inbound NewOrderSingle
|
|
6
|
+
order_id — venue-assigned OrderID (tag 37); shared across ack + fill
|
|
7
|
+
so both reports refer to the same order
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
from fixcore.message.message import Message
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _utc_ts() -> str:
|
|
17
|
+
return datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _new_exec_id() -> str:
|
|
21
|
+
return "E" + uuid.uuid4().hex[:8].upper()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def new_order_id() -> str:
|
|
25
|
+
return "O" + uuid.uuid4().hex[:8].upper()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _set(msg: Message, tag: int, value: str) -> None:
|
|
29
|
+
msg.set_field(tag, value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _copy(msg: Message, fields: dict, tag: int) -> None:
|
|
33
|
+
val = fields.get(str(tag)) or fields.get(tag)
|
|
34
|
+
if val:
|
|
35
|
+
_set(msg, tag, val)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_ack(order_fields: dict, order_id: str) -> Message:
|
|
39
|
+
"""ExecutionReport — OrdStatus=New (acknowledgement)."""
|
|
40
|
+
msg = Message()
|
|
41
|
+
msg.header.set(35, "8") # MsgType = ExecutionReport
|
|
42
|
+
|
|
43
|
+
_copy(msg, order_fields, 11) # ClOrdID
|
|
44
|
+
_copy(msg, order_fields, 55) # Symbol
|
|
45
|
+
_copy(msg, order_fields, 54) # Side
|
|
46
|
+
_copy(msg, order_fields, 38) # OrderQty
|
|
47
|
+
_copy(msg, order_fields, 40) # OrdType
|
|
48
|
+
_copy(msg, order_fields, 44) # Price
|
|
49
|
+
_copy(msg, order_fields, 1) # Account
|
|
50
|
+
|
|
51
|
+
qty = order_fields.get("38") or order_fields.get(38, "0")
|
|
52
|
+
|
|
53
|
+
_set(msg, 37, order_id) # OrderID
|
|
54
|
+
_set(msg, 17, _new_exec_id()) # ExecID
|
|
55
|
+
_set(msg, 20, "0") # ExecTransType = New (FIX 4.2)
|
|
56
|
+
_set(msg, 150, "0") # ExecType = New
|
|
57
|
+
_set(msg, 39, "0") # OrdStatus = New
|
|
58
|
+
_set(msg, 14, "0") # CumQty = 0
|
|
59
|
+
_set(msg, 151, qty) # LeavesQty = OrderQty
|
|
60
|
+
_set(msg, 6, "0") # AvgPx = 0
|
|
61
|
+
_set(msg, 60, _utc_ts()) # TransactTime
|
|
62
|
+
|
|
63
|
+
return msg
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_fill(order_fields: dict, order_id: str) -> Message:
|
|
67
|
+
"""ExecutionReport — OrdStatus=Filled (full fill)."""
|
|
68
|
+
msg = Message()
|
|
69
|
+
msg.header.set(35, "8") # MsgType = ExecutionReport
|
|
70
|
+
|
|
71
|
+
_copy(msg, order_fields, 11) # ClOrdID
|
|
72
|
+
_copy(msg, order_fields, 55) # Symbol
|
|
73
|
+
_copy(msg, order_fields, 54) # Side
|
|
74
|
+
_copy(msg, order_fields, 38) # OrderQty
|
|
75
|
+
_copy(msg, order_fields, 40) # OrdType
|
|
76
|
+
_copy(msg, order_fields, 44) # Price
|
|
77
|
+
_copy(msg, order_fields, 1) # Account
|
|
78
|
+
|
|
79
|
+
qty = order_fields.get("38") or order_fields.get(38, "0")
|
|
80
|
+
px = order_fields.get("44") or order_fields.get(44, "0")
|
|
81
|
+
|
|
82
|
+
_set(msg, 37, order_id) # OrderID
|
|
83
|
+
_set(msg, 17, _new_exec_id()) # ExecID
|
|
84
|
+
_set(msg, 20, "0") # ExecTransType = New (FIX 4.2)
|
|
85
|
+
_set(msg, 150, "2") # ExecType = Fill
|
|
86
|
+
_set(msg, 39, "2") # OrdStatus = Filled
|
|
87
|
+
_set(msg, 32, qty) # LastShares / LastQty
|
|
88
|
+
_set(msg, 31, px) # LastPx
|
|
89
|
+
_set(msg, 14, qty) # CumQty = OrderQty
|
|
90
|
+
_set(msg, 151, "0") # LeavesQty = 0
|
|
91
|
+
_set(msg, 6, px) # AvgPx = LastPx
|
|
92
|
+
_set(msg, 60, _utc_ts()) # TransactTime
|
|
93
|
+
|
|
94
|
+
return msg
|