fixtureqa 0.1.5__tar.gz → 0.1.7__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 (68) hide show
  1. {fixtureqa-0.1.5/fixtureqa.egg-info → fixtureqa-0.1.7}/PKG-INFO +1 -1
  2. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/app.py +7 -1
  3. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/deps.py +10 -0
  4. fixtureqa-0.1.7/fixture/api/routers/custom_tags.py +41 -0
  5. fixtureqa-0.1.7/fixture/api/routers/fix_spec.py +96 -0
  6. fixtureqa-0.1.7/fixture/api/routers/spec_overlay.py +39 -0
  7. fixtureqa-0.1.7/fixture/core/custom_tag_store.py +53 -0
  8. fixtureqa-0.1.7/fixture/core/spec_overlay_store.py +33 -0
  9. fixtureqa-0.1.7/fixture/static/assets/index-DaomB9wG.js +102 -0
  10. fixtureqa-0.1.5/fixture/static/assets/index-DTYofspD.css → fixtureqa-0.1.7/fixture/static/assets/index-Dv0_KeqF.css +1 -1
  11. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/static/index.html +2 -2
  12. {fixtureqa-0.1.5 → fixtureqa-0.1.7/fixtureqa.egg-info}/PKG-INFO +1 -1
  13. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixtureqa.egg-info/SOURCES.txt +6 -2
  14. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/pyproject.toml +1 -1
  15. fixtureqa-0.1.5/fixture/api/routers/fix_spec.py +0 -41
  16. fixtureqa-0.1.5/fixture/static/assets/index-Bb9Ed1xd.js +0 -87
  17. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/LICENSE +0 -0
  18. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/README.md +0 -0
  19. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/__init__.py +0 -0
  20. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/__main__.py +0 -0
  21. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/__init__.py +0 -0
  22. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/connection_manager.py +0 -0
  23. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/__init__.py +0 -0
  24. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/admin.py +0 -0
  25. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/auth.py +0 -0
  26. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/branding.py +0 -0
  27. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/messages.py +0 -0
  28. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/scenarios.py +0 -0
  29. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/sessions.py +0 -0
  30. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/setup.py +0 -0
  31. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/templates.py +0 -0
  32. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/routers/ws.py +0 -0
  33. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/api/schemas.py +0 -0
  34. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/config/__init__.py +0 -0
  35. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/__init__.py +0 -0
  36. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/auth.py +0 -0
  37. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/config_store.py +0 -0
  38. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/events.py +0 -0
  39. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/fix_application.py +0 -0
  40. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/fix_parser.py +0 -0
  41. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/fix_spec_parser.py +0 -0
  42. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/fix_tags.py +0 -0
  43. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/housekeeping.py +0 -0
  44. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/message_log.py +0 -0
  45. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/message_store.py +0 -0
  46. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/models.py +0 -0
  47. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/scenario_runner.py +0 -0
  48. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/scenario_store.py +0 -0
  49. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/session.py +0 -0
  50. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/session_manager.py +0 -0
  51. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/template_store.py +0 -0
  52. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/user_store.py +0 -0
  53. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/core/venue_responses.py +0 -0
  54. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/fix_specs/FIX42.xml +0 -0
  55. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/fix_specs/FIX44.xml +0 -0
  56. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/server.py +0 -0
  57. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  58. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  59. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/static/favicon.svg +0 -0
  60. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixture/ui/__init__.py +0 -0
  61. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixtureqa.egg-info/dependency_links.txt +0 -0
  62. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixtureqa.egg-info/entry_points.txt +0 -0
  63. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixtureqa.egg-info/requires.txt +0 -0
  64. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/fixtureqa.egg-info/top_level.txt +0 -0
  65. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/setup.cfg +0 -0
  66. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/tests/test_auth.py +0 -0
  67. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/tests/test_sessions.py +0 -0
  68. {fixtureqa-0.1.5 → fixtureqa-0.1.7}/tests/test_templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -5,14 +5,16 @@ from fastapi import FastAPI
5
5
  from fastapi.staticfiles import StaticFiles
6
6
  from fastapi.responses import FileResponse
7
7
 
8
+ from ..core.custom_tag_store import CustomTagStore
8
9
  from ..core.housekeeping import HousekeepingService
9
10
  from ..core.session_manager import SessionManager
10
11
  from ..core.scenario_store import ScenarioStore
11
12
  from ..core.scenario_runner import ScenarioRunner
13
+ from ..core.spec_overlay_store import SpecOverlayStore
12
14
  from ..core.template_store import TemplateStore
13
15
  from ..core.user_store import UserStore
14
16
  from .connection_manager import ConnectionManager
15
- from .routers import sessions, messages, ws, auth, setup, admin, fix_spec, templates, scenarios, branding
17
+ from .routers import sessions, messages, ws, auth, setup, admin, fix_spec, templates, scenarios, branding, custom_tags, spec_overlay
16
18
 
17
19
 
18
20
  @asynccontextmanager
@@ -56,6 +58,8 @@ def create_app(
56
58
  app.state.housekeeping = HousekeepingService(msg_db_path, log_dir, user_store)
57
59
  app.state.template_store = TemplateStore(data_dir)
58
60
  app.state.scenario_store = ScenarioStore(data_dir)
61
+ app.state.custom_tag_store = CustomTagStore(data_dir)
62
+ app.state.spec_overlay_store = SpecOverlayStore(data_dir)
59
63
  conn_manager: ConnectionManager = app.state.connection_manager
60
64
  app.state.scenario_runner = ScenarioRunner(
61
65
  session_manager, app.state.template_store, conn_manager.broadcast_scenario_event
@@ -71,6 +75,8 @@ def create_app(
71
75
  app.include_router(templates.router, prefix="/api/templates")
72
76
  app.include_router(scenarios.router, prefix="/api/scenarios")
73
77
  app.include_router(branding.router, prefix="/api")
78
+ app.include_router(custom_tags.router, prefix="/api/custom-tags")
79
+ app.include_router(spec_overlay.router, prefix="/api/spec-overlay")
74
80
  app.include_router(ws.router)
75
81
 
76
82
  # Serve built React app from fixture/static/
@@ -10,6 +10,8 @@ from ..core.housekeeping import HousekeepingService
10
10
  from ..core.template_store import TemplateStore
11
11
  from ..core.scenario_store import ScenarioStore
12
12
  from ..core.scenario_runner import ScenarioRunner
13
+ from ..core.custom_tag_store import CustomTagStore
14
+ from ..core.spec_overlay_store import SpecOverlayStore
13
15
  from .connection_manager import ConnectionManager
14
16
 
15
17
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
@@ -71,3 +73,11 @@ def get_scenario_store(request: HTTPConnection) -> ScenarioStore:
71
73
 
72
74
  def get_scenario_runner(request: HTTPConnection) -> ScenarioRunner:
73
75
  return request.app.state.scenario_runner
76
+
77
+
78
+ def get_custom_tag_store(request: HTTPConnection) -> CustomTagStore:
79
+ return request.app.state.custom_tag_store
80
+
81
+
82
+ def get_spec_overlay_store(request: HTTPConnection) -> SpecOverlayStore:
83
+ return request.app.state.spec_overlay_store
@@ -0,0 +1,41 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException
4
+ from pydantic import BaseModel
5
+
6
+ from ...core.custom_tag_store import CustomTagStore
7
+ from ...core.user_store import User
8
+ from ..deps import get_current_user, get_custom_tag_store
9
+
10
+ router = APIRouter(tags=["custom-tags"])
11
+
12
+ CT = Annotated[CustomTagStore, Depends(get_custom_tag_store)]
13
+ CurrentUser = Annotated[User, Depends(get_current_user)]
14
+
15
+ FIELD_TYPES = ["STRING", "CHAR", "INT", "PRICE", "QTY", "BOOLEAN", "UTCTIMESTAMP", "NUMINGROUP", "FLOAT", "SEQNUM"]
16
+
17
+
18
+ class CustomTagRequest(BaseModel):
19
+ number: int
20
+ name: str
21
+ type: str = "STRING"
22
+ values: dict[str, str] = {}
23
+
24
+
25
+ @router.get("")
26
+ def list_custom_tags(store: CT, user: CurrentUser) -> list[dict]:
27
+ return store.list_tags(user.uid)
28
+
29
+
30
+ @router.post("")
31
+ def upsert_custom_tag(body: CustomTagRequest, store: CT, user: CurrentUser) -> dict:
32
+ if body.number < 1:
33
+ raise HTTPException(status_code=422, detail="Tag number must be >= 1")
34
+ return store.upsert_tag(user.uid, body.number, body.name, body.type, body.values)
35
+
36
+
37
+ @router.delete("/{number}")
38
+ def delete_custom_tag(number: int, store: CT, user: CurrentUser) -> dict:
39
+ if not store.delete_tag(user.uid, number):
40
+ raise HTTPException(status_code=404, detail="Tag not found")
41
+ return {"ok": True}
@@ -0,0 +1,96 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, Query
4
+
5
+ from ...core.fix_spec_parser import get_fields, get_message, get_messages
6
+ from ...core.spec_overlay_store import SpecOverlayStore
7
+ from ...core.user_store import User
8
+ from ..deps import get_current_user, get_spec_overlay_store
9
+ from ..schemas import FieldRefResponse, MessageDefResponse, MessageSummaryResponse
10
+
11
+ router = APIRouter(tags=["fix-spec"])
12
+
13
+ SUPPORTED = {"FIX.4.2", "FIX.4.4"}
14
+
15
+ SO = Annotated[SpecOverlayStore, Depends(get_spec_overlay_store)]
16
+ CurrentUser = Annotated[User, Depends(get_current_user)]
17
+
18
+
19
+ def _validate_begin_string(begin_string: str) -> None:
20
+ if begin_string not in SUPPORTED:
21
+ raise HTTPException(
22
+ status_code=400,
23
+ detail=f"Unsupported begin_string '{begin_string}'. Use: {sorted(SUPPORTED)}",
24
+ )
25
+
26
+
27
+ @router.get("/messages", response_model=list[MessageSummaryResponse])
28
+ def list_messages(
29
+ begin_string: str = Query("FIX.4.2"),
30
+ overlay_store: SO = ...,
31
+ user: CurrentUser = ...,
32
+ ):
33
+ _validate_begin_string(begin_string)
34
+ spec = [MessageSummaryResponse(msg_type=m.msg_type, name=m.name, category=m.category)
35
+ for m in get_messages(begin_string)]
36
+ spec_types = {m.msg_type for m in spec}
37
+
38
+ overlay = overlay_store.get_overlay(user.uid)
39
+ extra = [
40
+ MessageSummaryResponse(
41
+ msg_type=om["msg_type"],
42
+ name=om["name"],
43
+ category=om.get("category", "app"),
44
+ )
45
+ for om in overlay.get("messages", [])
46
+ if om.get("msg_type") and om["msg_type"] not in spec_types
47
+ ]
48
+ return spec + extra
49
+
50
+
51
+ @router.get("/messages/{msg_type}", response_model=MessageDefResponse)
52
+ def get_message_def(
53
+ msg_type: str,
54
+ begin_string: str = Query("FIX.4.2"),
55
+ overlay_store: SO = ...,
56
+ user: CurrentUser = ...,
57
+ ):
58
+ _validate_begin_string(begin_string)
59
+
60
+ msg = get_message(begin_string, msg_type)
61
+ if msg is not None:
62
+ return MessageDefResponse(
63
+ msg_type=msg.msg_type,
64
+ name=msg.name,
65
+ category=msg.category,
66
+ fields=[
67
+ FieldRefResponse(tag=f.tag, name=f.name, type=f.type, required=f.required, values=f.values)
68
+ for f in msg.fields
69
+ ],
70
+ )
71
+
72
+ # Check user's spec overlay for custom message types
73
+ overlay = overlay_store.get_overlay(user.uid)
74
+ field_defs = get_fields(begin_string)
75
+ for om in overlay.get("messages", []):
76
+ if om.get("msg_type") != msg_type:
77
+ continue
78
+ fields = []
79
+ for f in om.get("fields", []):
80
+ tag = f.get("tag", 0)
81
+ fd = field_defs.get(tag)
82
+ fields.append(FieldRefResponse(
83
+ tag=tag,
84
+ name=fd.name if fd else f"Tag{tag}",
85
+ type=fd.type if fd else "STRING",
86
+ required=f.get("required", False),
87
+ values=fd.values if fd else {},
88
+ ))
89
+ return MessageDefResponse(
90
+ msg_type=msg_type,
91
+ name=om["name"],
92
+ category=om.get("category", "app"),
93
+ fields=fields,
94
+ )
95
+
96
+ raise HTTPException(status_code=404, detail=f"Message type '{msg_type}' not found in {begin_string}")
@@ -0,0 +1,39 @@
1
+ from typing import Annotated, Any
2
+
3
+ from fastapi import APIRouter, Depends
4
+ from pydantic import BaseModel
5
+
6
+ from ...core.spec_overlay_store import SpecOverlayStore
7
+ from ...core.user_store import User
8
+ from ..deps import get_current_user, get_spec_overlay_store
9
+
10
+ router = APIRouter(tags=["spec-overlay"])
11
+
12
+ SO = Annotated[SpecOverlayStore, Depends(get_spec_overlay_store)]
13
+ CurrentUser = Annotated[User, Depends(get_current_user)]
14
+
15
+
16
+ class OverlayField(BaseModel):
17
+ tag: int
18
+ required: bool = False
19
+
20
+
21
+ class OverlayMessage(BaseModel):
22
+ msg_type: str
23
+ name: str
24
+ category: str = "app"
25
+ fields: list[OverlayField] = []
26
+
27
+
28
+ class SpecOverlayRequest(BaseModel):
29
+ messages: list[OverlayMessage] = []
30
+
31
+
32
+ @router.get("")
33
+ def get_overlay(store: SO, user: CurrentUser) -> dict:
34
+ return store.get_overlay(user.uid)
35
+
36
+
37
+ @router.put("")
38
+ def set_overlay(body: SpecOverlayRequest, store: SO, user: CurrentUser) -> dict:
39
+ return store.set_overlay(user.uid, body.model_dump())
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from threading import Lock
6
+
7
+
8
+ class CustomTagStore:
9
+ def __init__(self, data_dir: str) -> None:
10
+ self._data_dir = data_dir
11
+ self._lock = Lock()
12
+
13
+ def _path(self, uid: str) -> str:
14
+ return os.path.join(self._data_dir, uid, "custom_tags.json")
15
+
16
+ def _load(self, uid: str) -> dict[int, dict]:
17
+ try:
18
+ with open(self._path(uid)) as f:
19
+ return {int(k): v for k, v in json.load(f).items()}
20
+ except (FileNotFoundError, json.JSONDecodeError, ValueError):
21
+ return {}
22
+
23
+ def _save(self, uid: str, tags: dict[int, dict]) -> None:
24
+ path = self._path(uid)
25
+ os.makedirs(os.path.dirname(path), exist_ok=True)
26
+ tmp = path + ".tmp"
27
+ with open(tmp, "w") as f:
28
+ json.dump({str(k): v for k, v in tags.items()}, f, indent=2)
29
+ os.replace(tmp, path)
30
+
31
+ def list_tags(self, uid: str) -> list[dict]:
32
+ with self._lock:
33
+ tags = self._load(uid)
34
+ return sorted(
35
+ [{"number": n, **v} for n, v in tags.items()],
36
+ key=lambda x: x["number"],
37
+ )
38
+
39
+ def upsert_tag(self, uid: str, number: int, name: str, type_: str, values: dict[str, str]) -> dict:
40
+ with self._lock:
41
+ tags = self._load(uid)
42
+ tags[number] = {"name": name, "type": type_, "values": values}
43
+ self._save(uid, tags)
44
+ return {"number": number, "name": name, "type": type_, "values": values}
45
+
46
+ def delete_tag(self, uid: str, number: int) -> bool:
47
+ with self._lock:
48
+ tags = self._load(uid)
49
+ if number not in tags:
50
+ return False
51
+ del tags[number]
52
+ self._save(uid, tags)
53
+ return True
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from threading import Lock
6
+
7
+ _EMPTY: dict = {"messages": []}
8
+
9
+
10
+ class SpecOverlayStore:
11
+ def __init__(self, data_dir: str) -> None:
12
+ self._data_dir = data_dir
13
+ self._lock = Lock()
14
+
15
+ def _path(self, uid: str) -> str:
16
+ return os.path.join(self._data_dir, uid, "spec_overlay.json")
17
+
18
+ def get_overlay(self, uid: str) -> dict:
19
+ try:
20
+ with open(self._path(uid)) as f:
21
+ return json.load(f)
22
+ except (FileNotFoundError, json.JSONDecodeError):
23
+ return dict(_EMPTY)
24
+
25
+ def set_overlay(self, uid: str, overlay: dict) -> dict:
26
+ path = self._path(uid)
27
+ os.makedirs(os.path.dirname(path), exist_ok=True)
28
+ tmp = path + ".tmp"
29
+ with self._lock:
30
+ with open(tmp, "w") as f:
31
+ json.dump(overlay, f, indent=2)
32
+ os.replace(tmp, path)
33
+ return overlay