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.
Files changed (54) hide show
  1. fixture/__init__.py +22 -0
  2. fixture/__main__.py +161 -0
  3. fixture/api/__init__.py +0 -0
  4. fixture/api/app.py +95 -0
  5. fixture/api/connection_manager.py +161 -0
  6. fixture/api/deps.py +73 -0
  7. fixture/api/routers/__init__.py +0 -0
  8. fixture/api/routers/admin.py +178 -0
  9. fixture/api/routers/auth.py +74 -0
  10. fixture/api/routers/branding.py +33 -0
  11. fixture/api/routers/fix_spec.py +41 -0
  12. fixture/api/routers/messages.py +137 -0
  13. fixture/api/routers/scenarios.py +65 -0
  14. fixture/api/routers/sessions.py +272 -0
  15. fixture/api/routers/setup.py +42 -0
  16. fixture/api/routers/templates.py +36 -0
  17. fixture/api/routers/ws.py +129 -0
  18. fixture/api/schemas.py +289 -0
  19. fixture/config/__init__.py +0 -0
  20. fixture/core/__init__.py +0 -0
  21. fixture/core/auth.py +68 -0
  22. fixture/core/config_store.py +85 -0
  23. fixture/core/events.py +22 -0
  24. fixture/core/fix_application.py +67 -0
  25. fixture/core/fix_parser.py +79 -0
  26. fixture/core/fix_spec_parser.py +172 -0
  27. fixture/core/fix_tags.py +297 -0
  28. fixture/core/housekeeping.py +107 -0
  29. fixture/core/message_log.py +115 -0
  30. fixture/core/message_store.py +246 -0
  31. fixture/core/models.py +87 -0
  32. fixture/core/scenario_runner.py +331 -0
  33. fixture/core/scenario_store.py +70 -0
  34. fixture/core/session.py +278 -0
  35. fixture/core/session_manager.py +173 -0
  36. fixture/core/template_store.py +70 -0
  37. fixture/core/user_store.py +186 -0
  38. fixture/core/venue_responses.py +94 -0
  39. fixture/fix_specs/FIX42.xml +2746 -0
  40. fixture/fix_specs/FIX44.xml +6593 -0
  41. fixture/server.py +37 -0
  42. fixture/static/assets/ag-grid-_QKprVdm.js +326 -0
  43. fixture/static/assets/index-B31-1dt-.css +1 -0
  44. fixture/static/assets/index-CTsKxGdI.js +87 -0
  45. fixture/static/assets/react-vendor-2eF0YfZT.js +1 -0
  46. fixture/static/favicon.svg +12 -0
  47. fixture/static/index.html +15 -0
  48. fixture/ui/__init__.py +0 -0
  49. fixtureqa-0.1.0.dist-info/METADATA +16 -0
  50. fixtureqa-0.1.0.dist-info/RECORD +54 -0
  51. fixtureqa-0.1.0.dist-info/WHEEL +5 -0
  52. fixtureqa-0.1.0.dist-info/entry_points.txt +2 -0
  53. fixtureqa-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. 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